feat: Load* and Save functions panic if data is not a pointer to a struct
This commit is contained in:
parent
8509f98d69
commit
81dfdacb33
3 changed files with 122 additions and 6 deletions
|
@ -5,6 +5,9 @@
|
|||
- Add support for TOML configuration files
|
||||
- Add support for HCL configuration files
|
||||
- Use stdlib for tests instead of convey
|
||||
- Public functions now panic when the data to be marshaled or unmarshaled is not
|
||||
a pointer to a struct. These errors should be caught during deelopment (with
|
||||
unit tests).
|
||||
- Update golangci-lint configuration
|
||||
|
||||
## v0.3.0 (2025-01-14)
|
||||
|
|
43
config.go
43
config.go
|
@ -15,14 +15,30 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// ErrUnsupportedFileType is returned when the type of the config file is not
|
||||
// supported.
|
||||
var ErrUnsupportedFileType = errors.New("unsupported config type")
|
||||
var (
|
||||
// ErrUnsupportedFileType is returned when the type of the config file is
|
||||
// not supported.
|
||||
ErrUnsupportedFileType = errors.New("unsupported config type")
|
||||
|
||||
// ErrInvalidMarshalData is returned when the marshaled value is not a
|
||||
// struct.
|
||||
ErrInvalidMarshalData = errors.New("the marshaled value must be a struct")
|
||||
|
||||
// ErrInvalidUnmarshalData is returned when the marshaled value is not a
|
||||
// struct.
|
||||
ErrInvalidUnmarshalData = errors.New(
|
||||
"the unmarshaled value must be a pointer to a struct",
|
||||
)
|
||||
)
|
||||
|
||||
// LoadFile reads the file at path, parses its json content and fills the struct
|
||||
// with the content of the file.
|
||||
//
|
||||
// LoadFile panics with [ErrInvalidUnmarshalData] if data is not a pointer to a
|
||||
// struct, as this error should be caught during dev.
|
||||
func LoadFile(path string, data any) error {
|
||||
return read(path, data)
|
||||
}
|
||||
|
@ -33,6 +49,9 @@ func LoadFile(path string, data any) error {
|
|||
//
|
||||
// It returns an error only if the content of a file is invalid, i.e. it
|
||||
// cannot be unmarshaled to the struct.
|
||||
//
|
||||
// LoadFiles panics with [ErrInvalidUnmarshalData] if data is not a pointer to a
|
||||
// struct, as this error should be caught during dev.
|
||||
func LoadFiles(data any, paths ...string) error {
|
||||
for _, p := range paths {
|
||||
err := read(p, data)
|
||||
|
@ -45,6 +64,9 @@ func LoadFiles(data any, paths ...string) error {
|
|||
}
|
||||
|
||||
// SaveFile writes the given data serialized in JSON in the given path.
|
||||
//
|
||||
// SaveFile panics with [ErrInvalidMarshalData] if data is not a struct, as
|
||||
// this error should be caught during dev.
|
||||
func SaveFile(path string, data any) error {
|
||||
return write(path, data)
|
||||
}
|
||||
|
@ -59,6 +81,9 @@ func SaveFile(path string, data any) error {
|
|||
//
|
||||
// An error is returned only if the config file cannot be
|
||||
// written.
|
||||
//
|
||||
// LoadAndUpdateFile panics with [ErrInvalidUnmarshalData] if data is not a
|
||||
// pointer to a struct, as this error should be caught during dev.
|
||||
func LoadAndUpdateFile(path string, data any) error {
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
err2 := read(path, data)
|
||||
|
@ -85,6 +110,13 @@ type Updater interface {
|
|||
}
|
||||
|
||||
func read(path string, data any) error {
|
||||
val := reflect.ValueOf(data)
|
||||
indVal := reflect.Indirect(val)
|
||||
|
||||
if val.Kind() != reflect.Ptr || indVal.Kind() != reflect.Struct {
|
||||
panic(ErrInvalidUnmarshalData)
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filepath.Clean(path))
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot read config file: %w", err)
|
||||
|
@ -94,6 +126,11 @@ func read(path string, data any) error {
|
|||
}
|
||||
|
||||
func write(path string, data any) error {
|
||||
v := reflect.Indirect(reflect.ValueOf(data))
|
||||
if v.Kind() != reflect.Struct {
|
||||
panic(ErrInvalidMarshalData)
|
||||
}
|
||||
|
||||
content, err := marshal(getType(path), data)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -117,6 +117,31 @@ func testLoadFile(t *testing.T, ext string) {
|
|||
assert.Equal(t, "should not change", c.Invariant)
|
||||
})
|
||||
|
||||
t.Run("with a valid file and invalid data", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
file := "test_data/valid." + ext
|
||||
|
||||
assert.PanicsWithError(t, conf.ErrInvalidUnmarshalData.Error(), func() {
|
||||
conf.LoadFile(file, func() {})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with a valid file and data is not a pointer", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
file := "test_data/valid." + ext
|
||||
c := testconf{
|
||||
String: "default string",
|
||||
Int: 1,
|
||||
Invariant: "should not change",
|
||||
}
|
||||
|
||||
assert.PanicsWithError(t, conf.ErrInvalidUnmarshalData.Error(), func() {
|
||||
conf.LoadFile(file, c)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with an invalid file", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
@ -206,6 +231,31 @@ func testLoadFiles(t *testing.T, ext string) {
|
|||
|
||||
assert.Equal(t, "", c.String)
|
||||
})
|
||||
|
||||
t.Run("with a valid file and invalid data", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
file := "test_data/valid." + ext
|
||||
|
||||
assert.PanicsWithError(t, conf.ErrInvalidUnmarshalData.Error(), func() {
|
||||
conf.LoadFiles(func() {}, file)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with a valid file and data is not a pointer", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
file := "test_data/valid." + ext
|
||||
c := testconf{
|
||||
String: "default string",
|
||||
Int: 1,
|
||||
Invariant: "should not change",
|
||||
}
|
||||
|
||||
assert.PanicsWithError(t, conf.ErrInvalidUnmarshalData.Error(), func() {
|
||||
conf.LoadFiles(c, file)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -245,8 +295,9 @@ func testSaveFile(t *testing.T, ext string) {
|
|||
tmpDir := t.TempDir()
|
||||
file := filepath.Join(tmpDir, "test."+ext)
|
||||
|
||||
err := conf.SaveFile(file, func() error { return nil })
|
||||
require.Error(t, err)
|
||||
assert.PanicsWithError(t, conf.ErrInvalidMarshalData.Error(), func() {
|
||||
conf.SaveFile(file, func() error { return nil })
|
||||
})
|
||||
|
||||
assert.NoFileExists(t, file)
|
||||
})
|
||||
|
@ -437,7 +488,32 @@ func testLoadAndUpdateFile(t *testing.T, ext string) {
|
|||
require.NoError(t, err)
|
||||
assert.NotContains(t, string(newContent), "Unknown")
|
||||
|
||||
assert.True(t, updated)
|
||||
assert.True(t, updated, "the config file has not been updated")
|
||||
})
|
||||
|
||||
t.Run("with a valid file and invalid data", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
file := "test_data/valid." + ext
|
||||
|
||||
assert.PanicsWithError(t, conf.ErrInvalidUnmarshalData.Error(), func() {
|
||||
conf.LoadAndUpdateFile(file, func() {})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with a valid file and data is not a pointer", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
file := "test_data/valid." + ext
|
||||
c := testconf{
|
||||
String: "default string",
|
||||
Int: 1,
|
||||
Invariant: "should not change",
|
||||
}
|
||||
|
||||
assert.PanicsWithError(t, conf.ErrInvalidUnmarshalData.Error(), func() {
|
||||
conf.LoadAndUpdateFile(file, c)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue