Skip to content

Commit

Permalink
Update config loading to implicitly call PostLoad on supported config…
Browse files Browse the repository at this point in the history
…s, and update to a more modern error handling pattern.
  • Loading branch information
aphistic committed Apr 29, 2023
1 parent 9eec7fd commit 48975f8
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 27 deletions.
17 changes: 10 additions & 7 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,25 +67,28 @@ func (c *Config) Load(target interface{}, modifiers ...TagModifier) error {
for i := 0; i < len(sourceFields); i++ {
targetFields[i].Field.Set(sourceFields[i].Field)
}
}

if err := loadError(errors); err != nil {
} else {
c.dumpSource()
return err
return newLoadError(errors)
}

chunk, err := dumpChunk(target)
if err != nil {
return fmt.Errorf("failed to serialize config (%s)", err.Error())
return fmt.Errorf("failed to serialize config: %w", err)
}

if err := c.postLoad(target); err != nil {
return newPostLoadError(err)
}

c.logger.Printf("Config loaded: %s", normalizeChunk(chunk))

return nil
}

// Call the PostLoad method of the given target if it conforms to
// the PostLoadConfig interface.
func (c *Config) PostLoad(target interface{}) error {
func (c *Config) postLoad(target interface{}) error {
if plc, ok := target.(PostLoadConfig); ok {
return plc.PostLoad()
}
Expand Down Expand Up @@ -288,5 +291,5 @@ func loadError(errors []error) error {
messages = append(messages, err.Error())
}

return fmt.Errorf("failed to load config (%s)", strings.Join(messages, ", "))
return fmt.Errorf("failed to load config: (%s)", strings.Join(messages, ", "))
}
102 changes: 84 additions & 18 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ func TestConfigRequired(t *testing.T) {

config := NewConfig(NewFakeSourcer("app", nil))
chunk := &C{}
assert.EqualError(t, config.Load(chunk), "failed to load config (no value supplied for field 'X')")
assert.EqualError(t,
config.Load(chunk),
"failed to load config: no value supplied for field 'X'",
)
}

func TestConfigRequiredBadTag(t *testing.T) {
Expand All @@ -70,7 +73,10 @@ func TestConfigRequiredBadTag(t *testing.T) {

config := NewConfig(NewFakeSourcer("app", nil))
chunk := &C{}
assert.EqualError(t, config.Load(chunk), "failed to load config (field 'X' has an invalid required tag)")
assert.EqualError(t,
config.Load(chunk),
"failed to load config: field 'X' has an invalid required tag",
)
}

func TestConfigDefault(t *testing.T) {
Expand Down Expand Up @@ -101,10 +107,16 @@ func TestConfigBadType(t *testing.T) {
}))

chunk := &C{}
assert.EqualError(t, config.Load(chunk), fmt.Sprintf("failed to load config (%s)", strings.Join([]string{
"value supplied for field 'Y' cannot be coerced into the expected type",
"value supplied for field 'Z' cannot be coerced into the expected type",
}, ", ")))

var errLoad *LoadError
require.ErrorAs(t, config.Load(chunk), &errLoad)

Check failure on line 112 in config_test.go

View workflow job for this annotation

GitHub Actions / test

undefined: require.ErrorAs
assert.ElementsMatch(t,
errLoad.Errors,
[]error{
fmt.Errorf("value supplied for field 'Y' cannot be coerced into the expected type"),
fmt.Errorf("value supplied for field 'Z' cannot be coerced into the expected type"),
},
)
}

func TestConfigBadDefaultType(t *testing.T) {
Expand All @@ -114,7 +126,15 @@ func TestConfigBadDefaultType(t *testing.T) {

config := NewConfig(NewFakeSourcer("app", nil))
chunk := &C{}
assert.EqualError(t, config.Load(chunk), "failed to load config (default value for field 'X' cannot be coerced into the expected type)")

var errLoad *LoadError
require.ErrorAs(t, config.Load(chunk), &errLoad)

Check failure on line 131 in config_test.go

View workflow job for this annotation

GitHub Actions / test

undefined: require.ErrorAs
assert.ElementsMatch(t,
errLoad.Errors,
[]error{
fmt.Errorf("default value for field 'X' cannot be coerced into the expected type"),
},
)
}

func TestConfigPostLoadConfig(t *testing.T) {
Expand All @@ -124,14 +144,17 @@ func TestConfigPostLoadConfig(t *testing.T) {

chunk := &testPostLoadConfig{}
require.Nil(t, config.Load(chunk))
require.Nil(t, config.PostLoad(chunk))

config = NewConfig(NewFakeSourcer("app", map[string]string{
"APP_X": "-4",
}))

assert.Nil(t, config.Load(chunk))
assert.EqualError(t, config.PostLoad(chunk), "X must be positive")
var postErr *PostLoadError
require.ErrorAs(t, config.Load(chunk), &postErr)

Check failure on line 153 in config_test.go

View workflow job for this annotation

GitHub Actions / test

undefined: require.ErrorAs
assert.EqualError(t,
postErr.Unwrap(),
"X must be positive",
)
}

type testPostLoadConfig struct {
Expand All @@ -153,7 +176,15 @@ func TestConfigUnsettableFields(t *testing.T) {

config := NewConfig(NewFakeSourcer("app", nil))
chunk := &C{}
assert.EqualError(t, config.Load(chunk), "failed to load config (field 'x' can not be set)")

var errLoad *LoadError
require.ErrorAs(t, config.Load(chunk), &errLoad)

Check failure on line 181 in config_test.go

View workflow job for this annotation

GitHub Actions / test

undefined: require.ErrorAs
assert.ElementsMatch(t,
errLoad.Errors,
[]error{
fmt.Errorf("field 'x' can not be set"),
},
)
}

func TestConfigLoad(t *testing.T) {
Expand Down Expand Up @@ -205,7 +236,6 @@ func TestConfigLoadPostLoadWithConversion(t *testing.T) {

chunk := &testPostLoadConversion{}
require.Nil(t, config.Load(chunk))
require.Nil(t, config.PostLoad(chunk))
assert.Equal(t, time.Second*3, chunk.Duration)
}

Expand All @@ -216,7 +246,6 @@ func TestConfigLoadPostLoadWithTags(t *testing.T) {

chunk := &testPostLoadConversion{}
require.Nil(t, config.Load(chunk, NewEnvTagPrefixer("foo")))
require.Nil(t, config.PostLoad(chunk))
assert.Equal(t, time.Second*3, chunk.Duration)
}

Expand All @@ -231,8 +260,32 @@ func (c *testPostLoadConversion) PostLoad() error {
}

func TestConfigBadConfigObjectTypes(t *testing.T) {
assert.EqualError(t, NewConfig(NewFakeSourcer("app", nil)).Load(nil), "failed to load config (configuration target is not a pointer to struct)")
assert.EqualError(t, NewConfig(NewFakeSourcer("app", nil)).Load("foo"), "failed to load config (configuration target is not a pointer to struct)")
t.Run("nil config object", func(t *testing.T) {
var errLoad *LoadError
require.ErrorAs(t,

Check failure on line 265 in config_test.go

View workflow job for this annotation

GitHub Actions / test

undefined: require.ErrorAs
NewConfig(NewFakeSourcer("app", nil)).Load(nil),
&errLoad,
)
assert.ElementsMatch(t,
errLoad.Errors,
[]error{
fmt.Errorf("configuration target is not a pointer to struct"),
},
)
})
t.Run("string config object", func(t *testing.T) {
var errLoad *LoadError
require.ErrorAs(t,

Check failure on line 278 in config_test.go

View workflow job for this annotation

GitHub Actions / test

undefined: require.ErrorAs
NewConfig(NewFakeSourcer("app", nil)).Load("foo"),
&errLoad,
)
assert.ElementsMatch(t,
errLoad.Errors,
[]error{
fmt.Errorf("configuration target is not a pointer to struct"),
},
)
})
}

func TestConfigEmbeddedConfig(t *testing.T) {
Expand Down Expand Up @@ -281,8 +334,13 @@ func TestConfigEmbeddedConfigPostLoad(t *testing.T) {
}))

chunk := &testParentConfig{}
assert.Nil(t, config.Load(chunk))
assert.EqualError(t, config.PostLoad(chunk), "fields must be increasing")

var postErr *PostLoadError
require.ErrorAs(t, config.Load(chunk), &postErr)

Check failure on line 339 in config_test.go

View workflow job for this annotation

GitHub Actions / test

undefined: require.ErrorAs
assert.EqualError(t,
postErr.Unwrap(),
"fields must be increasing",
)
}

type testParentConfig struct {
Expand Down Expand Up @@ -314,7 +372,15 @@ func TestConfigBadEmbeddedObjectType(t *testing.T) {

config := NewConfig(NewFakeSourcer("app", nil))
chunk := &C{}
assert.EqualError(t, config.Load(chunk), "failed to load config (invalid embedded type in configuration struct)")

var loadErr *LoadError
require.ErrorAs(t, config.Load(chunk), &loadErr)

Check failure on line 377 in config_test.go

View workflow job for this annotation

GitHub Actions / test

undefined: require.ErrorAs
assert.ElementsMatch(t,
loadErr.Errors,
[]error{
fmt.Errorf("invalid embedded type in configuration struct"),
},
)
}

//
Expand Down
70 changes: 70 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package config

import (
"errors"
"fmt"
"strings"
)

var (
ErrInvalidMaskTag = errors.New("invalid mask tag")
)

// LoadError is an error returned when a config fails to load. It contains Errors, a slice of
// any errors that occurred during config load.
type LoadError struct {
// Errors that were found while loading a config.
Errors []error
}

func newLoadError(errs []error) *LoadError {
return &LoadError{Errors: errs}
}

func (le *LoadError) Error() string {
var errs []string
for _, errStr := range le.Errors {
errs = append(errs, errStr.Error())
}

return "failed to load config: " + strings.Join(errs, ", ")
}

// SerializeError is an error returned when a config loader fails to serialize a field
// in a config struct.
type SerializeError struct {
FieldName string
Err error
}

func newSerializeError(fieldName string, serializeErr error) *SerializeError {
return &SerializeError{
FieldName: fieldName,
Err: serializeErr,
}
}

func (se *SerializeError) Error() string {
return fmt.Sprintf("field '%s': %s", se.FieldName, se.Err)
}

func (se *SerializeError) Unwrap() error {
return se.Err
}

// PostLoadError is an error returned when a config's PostLoad method fails.
type PostLoadError struct {
Err error
}

func newPostLoadError(err error) *PostLoadError {
return &PostLoadError{Err: err}
}

func (ple *PostLoadError) Error() string {
return "post load callback failed: " + ple.Err.Error()
}

func (ple *PostLoadError) Unwrap() error {
return ple.Err
}
57 changes: 57 additions & 0 deletions errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package config

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestLoadError(t *testing.T) {
t.Run("error string includes error slice", func(t *testing.T) {
err := newLoadError([]error{
fmt.Errorf("err1"),
fmt.Errorf("err2"),
})

assert.EqualError(t,
err,
"failed to load config: err1, err2",
)
})
}

func TestSerializeError(t *testing.T) {
t.Run("unwrap error", func(t *testing.T) {
innerErr := fmt.Errorf("inner")
err := newSerializeError("X", innerErr)
assert.Equal(t, "X", err.FieldName)
assert.ErrorIs(t, err, innerErr)

Check failure on line 30 in errors_test.go

View workflow job for this annotation

GitHub Actions / test

undefined: assert.ErrorIs
})
t.Run("error string", func(t *testing.T) {
innerErr := fmt.Errorf("inner")
err := newSerializeError("X", innerErr)
assert.EqualError(t,
err,
"field 'X': inner",
)
})
}

func TestPostLoadError(t *testing.T) {
t.Run("error string", func(t *testing.T) {
err := newPostLoadError(fmt.Errorf("inner err"))
assert.EqualError(t,
err,
"post load callback failed: inner err",
)
})
t.Run("unwrap error", func(t *testing.T) {
err := newPostLoadError(fmt.Errorf("inner err"))

innerErr := err.Unwrap()
require.NotNil(t, innerErr)
assert.EqualError(t, innerErr, "inner err")
})
}
2 changes: 1 addition & 1 deletion logging_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func dumpChunk(obj interface{}) (map[string]interface{}, error) {
if maskTagValue != "" {
val, err := strconv.ParseBool(maskTagValue)
if err != nil {
return nil, fmt.Errorf("field '%s' has an invalid mask tag", fieldType.Name)
return nil, newSerializeError(fieldType.Name, ErrInvalidMaskTag)
}

if val {
Expand Down
6 changes: 5 additions & 1 deletion logging_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,9 @@ func TestLoggingConfigBadMaskTag(t *testing.T) {
lc := NewConfig(NewEnvSourcer(""), WithLogger(logger))

chunk := &C{}
assert.EqualError(t, lc.Load(chunk), "failed to serialize config (field 'X' has an invalid mask tag)")

var serErr *SerializeError
require.ErrorAs(t, lc.Load(chunk), &serErr)

Check failure on line 61 in logging_config_test.go

View workflow job for this annotation

GitHub Actions / test

undefined: require.ErrorAs
assert.Equal(t, "X", serErr.FieldName)
assert.ErrorIs(t, serErr, ErrInvalidMaskTag)
}

0 comments on commit 48975f8

Please sign in to comment.