Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[confmap] Add mapstructure hook function for unmarshalling confmap.Conf #6029

Merged
merged 2 commits into from
Sep 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@

- Add `skip-get-modules` builder flag to support isolated environment executions (#6009)
- Skip unnecessary Go binary path validation when the builder is used with `skip-compilation` and `skip-get-modules` flags (#6026)
- Add mapstructure hook function for confmap.Unmarshaler interface (#6029)
bogdandrutu marked this conversation as resolved.
Show resolved Hide resolved

## v0.59.0 Beta

Expand Down
69 changes: 47 additions & 22 deletions confmap/confmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,23 +57,13 @@ func (l *Conf) AllKeys() []string {

// Unmarshal unmarshalls the config into a struct.
// Tags on the fields of the structure must be properly set.
func (l *Conf) Unmarshal(rawVal interface{}) error {
decoder, err := mapstructure.NewDecoder(decoderConfig(rawVal))
if err != nil {
return err
}
return decoder.Decode(l.ToStringMap())
func (l *Conf) Unmarshal(result interface{}) error {
return decodeConfig(l, result, false)
}

// UnmarshalExact unmarshalls the config into a struct, erroring if a field is nonexistent.
func (l *Conf) UnmarshalExact(rawVal interface{}) error {
dc := decoderConfig(rawVal)
dc.ErrorUnused = true
decoder, err := mapstructure.NewDecoder(dc)
if err != nil {
return err
}
return decoder.Decode(l.ToStringMap())
func (l *Conf) UnmarshalExact(result interface{}) error {
return decodeConfig(l, result, true)
}

// Get can retrieve any value given the key to use.
Expand Down Expand Up @@ -114,15 +104,17 @@ func (l *Conf) ToStringMap() map[string]interface{} {
return maps.Unflatten(l.k.All(), KeyDelimiter)
}

// decoderConfig returns a default mapstructure.DecoderConfig capable of parsing time.Duration
// and weakly converting config field values to primitive types. It also ensures that maps
// whose values are nil pointer structs resolved to the zero value of the target struct (see
// expandNilStructPointers). A decoder created from this mapstructure.DecoderConfig will decode
// its contents to the result argument.
func decoderConfig(result interface{}) *mapstructure.DecoderConfig {
return &mapstructure.DecoderConfig{
// decodeConfig decodes the contents of the Conf into the result argument, using a
// mapstructure decoder with the following notable behaviors. Ensures that maps whose
// values are nil pointer structs resolved to the zero value of the target struct (see
// expandNilStructPointers). Converts string to []string by splitting on ','. Ensures
// uniqueness of component IDs (see mapKeyStringToMapKeyTextUnmarshalerHookFunc).
// Decodes time.Duration from strings. Allows custom unmarshaling for structs implementing
// encoding.TextUnmarshaler. Allows custom unmarshaling for structs implementing confmap.Unmarshaler.
func decodeConfig(m *Conf, result interface{}, errorUnused bool) error {
dc := &mapstructure.DecoderConfig{
ErrorUnused: errorUnused,
Result: result,
Metadata: nil,
TagName: "mapstructure",
WeaklyTypedInput: true,
DecodeHook: mapstructure.ComposeDecodeHookFunc(
Expand All @@ -131,8 +123,14 @@ func decoderConfig(result interface{}) *mapstructure.DecoderConfig {
mapKeyStringToMapKeyTextUnmarshalerHookFunc(),
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.TextUnmarshallerHookFunc(),
unmarshalerHookFunc(result),
),
}
decoder, err := mapstructure.NewDecoder(dc)
if err != nil {
return err
}
return decoder.Decode(m.ToStringMap())
}

// In cases where a config has a mapping of something to a struct pointers
Expand Down Expand Up @@ -210,6 +208,33 @@ func mapKeyStringToMapKeyTextUnmarshalerHookFunc() mapstructure.DecodeHookFuncTy
}
}

// Provides a mechanism for individual structs to define their own unmarshal logic,
// by implementing the Unmarshaler interface.
func unmarshalerHookFunc(result interface{}) mapstructure.DecodeHookFuncValue {
return func(from reflect.Value, to reflect.Value) (interface{}, error) {
toPtr := to.Addr().Interface()
if _, ok := toPtr.(Unmarshaler); !ok {
return from.Interface(), nil
}

if _, ok := from.Interface().(map[string]interface{}); !ok {
return from.Interface(), nil
}

// Need to ignore the top structure to avoid circular dependency.
if toPtr == result {
return from.Interface(), nil
}

unmarshaler := reflect.New(to.Type()).Interface().(Unmarshaler)
if err := unmarshaler.Unmarshal(NewFromStringMap(from.Interface().(map[string]interface{}))); err != nil {
return nil, err
}

return unmarshaler, nil
}
}

// Unmarshaler interface may be implemented by types to customize their behavior when being unmarshaled from a Conf.
type Unmarshaler interface {
// Unmarshal a Conf into the struct in a custom way.
Expand Down
112 changes: 109 additions & 3 deletions confmap/confmap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,14 @@ func TestMapKeyStringToMapKeyTextUnmarshalerHookFunc(t *testing.T) {
},
}
conf := NewFromStringMap(stringMap)

cfgExact := &TestIDConfig{}
assert.NoError(t, conf.UnmarshalExact(cfgExact))
assert.True(t, cfgExact.Boolean)
assert.Equal(t, map[TestID]string{"string": "this is a string"}, cfgExact.Map)

cfg := &TestIDConfig{}
assert.NoError(t, conf.UnmarshalExact(cfg))
assert.NoError(t, conf.Unmarshal(cfg))
assert.True(t, cfg.Boolean)
assert.Equal(t, map[TestID]string{"string": "this is a string"}, cfg.Map)
}
Expand All @@ -202,8 +208,12 @@ func TestMapKeyStringToMapKeyTextUnmarshalerHookFuncDuplicateID(t *testing.T) {
},
}
conf := NewFromStringMap(stringMap)

cfgExact := &TestIDConfig{}
assert.Error(t, conf.UnmarshalExact(cfgExact))

cfg := &TestIDConfig{}
assert.Error(t, conf.UnmarshalExact(cfg))
assert.Error(t, conf.Unmarshal(cfg))
}

func TestMapKeyStringToMapKeyTextUnmarshalerHookFuncErrorUnmarshal(t *testing.T) {
Expand All @@ -214,8 +224,12 @@ func TestMapKeyStringToMapKeyTextUnmarshalerHookFuncErrorUnmarshal(t *testing.T)
},
}
conf := NewFromStringMap(stringMap)

cfgExact := &TestIDConfig{}
assert.Error(t, conf.UnmarshalExact(cfgExact))

cfg := &TestIDConfig{}
assert.Error(t, conf.UnmarshalExact(cfg))
assert.Error(t, conf.Unmarshal(cfg))
}

// newConfFromFile creates a new Conf by reading the given file.
Expand All @@ -228,3 +242,95 @@ func newConfFromFile(t testing.TB, fileName string) map[string]interface{} {

return NewFromStringMap(data).ToStringMap()
}

type testConfig struct {
Next nextConfig `mapstructure:"next"`
Another string `mapstructure:"another"`
}

func (tc *testConfig) Unmarshal(component *Conf) error {
if err := component.UnmarshalExact(tc); err != nil {
return err
}
tc.Another += " is only called directly"
return nil
}

type nextConfig struct {
String string `mapstructure:"string"`
}

func (nc *nextConfig) Unmarshal(component *Conf) error {
if err := component.UnmarshalExact(nc); err != nil {
return err
}
nc.String += " is called"
return nil
}

func TestUnmarshaler(t *testing.T) {
cfgMap := NewFromStringMap(map[string]interface{}{
"next": map[string]interface{}{
"string": "make sure this",
},
"another": "make sure this",
})

tc := &testConfig{}
assert.NoError(t, cfgMap.Unmarshal(tc))
assert.Equal(t, "make sure this", tc.Another)
assert.Equal(t, "make sure this is called", tc.Next.String)

tce := &testConfig{}
assert.NoError(t, cfgMap.UnmarshalExact(tce))
assert.Equal(t, "make sure this", tce.Another)
assert.Equal(t, "make sure this is called", tce.Next.String)
}

func TestDirectUnmarshaler(t *testing.T) {
cfgMap := NewFromStringMap(map[string]interface{}{
"next": map[string]interface{}{
"string": "make sure this",
},
"another": "make sure this",
})

tc := &testConfig{}
assert.NoError(t, tc.Unmarshal(cfgMap))
assert.Equal(t, "make sure this is only called directly", tc.Another)
assert.Equal(t, "make sure this is called", tc.Next.String)
}

type testErrConfig struct {
Err errConfig `mapstructure:"err"`
}

func (tc *testErrConfig) Unmarshal(component *Conf) error {
return component.UnmarshalExact(tc)
}

type errConfig struct {
Foo string `mapstructure:"foo"`
}

func (tc *errConfig) Unmarshal(component *Conf) error {
return errors.New("never works")
}

func TestUnmarshalerErr(t *testing.T) {
cfgMap := NewFromStringMap(map[string]interface{}{
"err": map[string]interface{}{
"foo": "will not unmarshal due to error",
},
})

expectErr := "1 error(s) decoding:\n\n* error decoding 'err': never works"

tc := &testErrConfig{}
assert.EqualError(t, cfgMap.Unmarshal(tc), expectErr)
assert.Empty(t, tc.Err.Foo)

tce := &testErrConfig{}
assert.EqualError(t, cfgMap.UnmarshalExact(tce), expectErr)
assert.Empty(t, tc.Err.Foo)
}