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 TOML configuration files
|
||||||
- Add support for HCL configuration files
|
- Add support for HCL configuration files
|
||||||
- Use stdlib for tests instead of convey
|
- 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
|
- Update golangci-lint configuration
|
||||||
|
|
||||||
## v0.3.0 (2025-01-14)
|
## v0.3.0 (2025-01-14)
|
||||||
|
|
43
config.go
43
config.go
|
@ -15,14 +15,30 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrUnsupportedFileType is returned when the type of the config file is not
|
var (
|
||||||
// supported.
|
// ErrUnsupportedFileType is returned when the type of the config file is
|
||||||
var ErrUnsupportedFileType = errors.New("unsupported config type")
|
// 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
|
// LoadFile reads the file at path, parses its json content and fills the struct
|
||||||
// with the content of the file.
|
// 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 {
|
func LoadFile(path string, data any) error {
|
||||||
return read(path, data)
|
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
|
// It returns an error only if the content of a file is invalid, i.e. it
|
||||||
// cannot be unmarshaled to the struct.
|
// 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 {
|
func LoadFiles(data any, paths ...string) error {
|
||||||
for _, p := range paths {
|
for _, p := range paths {
|
||||||
err := read(p, data)
|
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 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 {
|
func SaveFile(path string, data any) error {
|
||||||
return write(path, data)
|
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
|
// An error is returned only if the config file cannot be
|
||||||
// written.
|
// 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 {
|
func LoadAndUpdateFile(path string, data any) error {
|
||||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||||
err2 := read(path, data)
|
err2 := read(path, data)
|
||||||
|
@ -85,6 +110,13 @@ type Updater interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
func read(path string, data any) error {
|
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))
|
content, err := os.ReadFile(filepath.Clean(path))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot read config file: %w", err)
|
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 {
|
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)
|
content, err := marshal(getType(path), data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -117,6 +117,31 @@ func testLoadFile(t *testing.T, ext string) {
|
||||||
assert.Equal(t, "should not change", c.Invariant)
|
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.Run("with an invalid file", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
@ -206,6 +231,31 @@ func testLoadFiles(t *testing.T, ext string) {
|
||||||
|
|
||||||
assert.Equal(t, "", c.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()
|
tmpDir := t.TempDir()
|
||||||
file := filepath.Join(tmpDir, "test."+ext)
|
file := filepath.Join(tmpDir, "test."+ext)
|
||||||
|
|
||||||
err := conf.SaveFile(file, func() error { return nil })
|
assert.PanicsWithError(t, conf.ErrInvalidMarshalData.Error(), func() {
|
||||||
require.Error(t, err)
|
conf.SaveFile(file, func() error { return nil })
|
||||||
|
})
|
||||||
|
|
||||||
assert.NoFileExists(t, file)
|
assert.NoFileExists(t, file)
|
||||||
})
|
})
|
||||||
|
@ -437,7 +488,32 @@ func testLoadAndUpdateFile(t *testing.T, ext string) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotContains(t, string(newContent), "Unknown")
|
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