Skip to content

Commit

Permalink
[confmap] confmap honors Unmarshal methods on config embedded structs.
Browse files Browse the repository at this point in the history
  • Loading branch information
atoulme committed Feb 25, 2024
1 parent 6be5fe9 commit bb5b428
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 4 deletions.
25 changes: 25 additions & 0 deletions .chloggen/support_embedded_structs_confmap.yaml
@@ -0,0 +1,25 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: bug_fix

# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver)
component: confmap

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: confmap honors `Unmarshal` methods on config embedded structs.

# One or more tracking issues or pull requests related to the change
issues: [6671]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext:

# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: []
4 changes: 2 additions & 2 deletions cmd/mdatagen/metricdata.go
Expand Up @@ -125,7 +125,7 @@ func (d *gauge) Unmarshal(parser *confmap.Conf) error {
if err := d.MetricValueType.Unmarshal(parser); err != nil {
return err
}
return parser.Unmarshal(d)
return parser.Unmarshal(d, confmap.WithIgnoreUnused())
}

func (d gauge) Type() string {
Expand Down Expand Up @@ -155,7 +155,7 @@ func (d *sum) Unmarshal(parser *confmap.Conf) error {
if err := d.MetricValueType.Unmarshal(parser); err != nil {
return err
}
return parser.Unmarshal(d)
return parser.Unmarshal(d, confmap.WithIgnoreUnused())
}

// TODO: Currently, this func will not be called because of https://github.com/open-telemetry/opentelemetry-collector/issues/6671. Uncomment function and
Expand Down
36 changes: 36 additions & 0 deletions confmap/confmap.go
Expand Up @@ -157,6 +157,7 @@ func decodeConfig(m *Conf, result any, errorUnused bool) error {
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.TextUnmarshallerHookFunc(),
unmarshalerHookFunc(result),
embeddedStructsHookFunc(result),
zeroSliceHookFunc(),
),
}
Expand Down Expand Up @@ -261,6 +262,41 @@ func mapKeyStringToMapKeyTextUnmarshalerHookFunc() mapstructure.DecodeHookFuncTy
}
}

func embeddedStructsHookFunc(_ any) mapstructure.DecodeHookFuncValue {
return func(from reflect.Value, to reflect.Value) (any, error) {
if to.Type().Kind() != reflect.Struct {
return from.Interface(), nil
}

finalFrom := from.Interface()

for i := 0; i < to.Type().NumField(); i++ {
if to.Type().Field(i).IsExported() && to.Type().Field(i).Anonymous {
f := to.Field(i)
if unmarshaler, ok := f.Addr().Interface().(Unmarshaler); ok {
fromMap, ok := finalFrom.(map[string]any)
if !ok {
return from.Interface(), nil
}

Check warning on line 280 in confmap/confmap.go

View check run for this annotation

Codecov / codecov/patch

confmap/confmap.go#L279-L280

Added lines #L279 - L280 were not covered by tests
if err := unmarshaler.Unmarshal(NewFromStringMap(fromMap)); err != nil {
return nil, err
}

Check warning on line 283 in confmap/confmap.go

View check run for this annotation

Codecov / codecov/patch

confmap/confmap.go#L282-L283

Added lines #L282 - L283 were not covered by tests
conf := New()
if err := conf.Marshal(unmarshaler); err != nil {
return nil, err
}

Check warning on line 287 in confmap/confmap.go

View check run for this annotation

Codecov / codecov/patch

confmap/confmap.go#L286-L287

Added lines #L286 - L287 were not covered by tests
resultMap := conf.ToStringMap()
for k, v := range resultMap {
fromMap[k] = v
}
finalFrom = fromMap
}
}
}
return finalFrom, nil
}
}

// Provides a mechanism for individual structs to define their own unmarshal logic,
// by implementing the Unmarshaler interface.
func unmarshalerHookFunc(result any) mapstructure.DecodeHookFuncValue {
Expand Down
59 changes: 57 additions & 2 deletions confmap/confmap_test.go
Expand Up @@ -309,8 +309,41 @@ func newConfFromFile(t testing.TB, fileName string) map[string]any {
}

type testConfig struct {
Next *nextConfig `mapstructure:"next"`
Another string `mapstructure:"another"`
Next *nextConfig `mapstructure:"next"`
Another string `mapstructure:"another"`
EmbeddedConfig `mapstructure:",squash"`
EmbeddedConfig2 `mapstructure:",squash"`
}

type testConfigWithoutUnmarshaler struct {
Next *nextConfig `mapstructure:"next"`
Another string `mapstructure:"another"`
EmbeddedConfig `mapstructure:",squash"`
EmbeddedConfig2 `mapstructure:",squash"`
}

type EmbeddedConfig struct {
Some string `mapstructure:"some"`
}

func (ec *EmbeddedConfig) Unmarshal(component *Conf) error {
if err := component.Unmarshal(ec, WithIgnoreUnused()); err != nil {
return err
}
ec.Some += " is also called"
return nil
}

type EmbeddedConfig2 struct {
Some2 string `mapstructure:"some_2"`
}

func (ec *EmbeddedConfig2) Unmarshal(component *Conf) error {
if err := component.Unmarshal(ec, WithIgnoreUnused()); err != nil {
return err
}
ec.Some2 += " also called2"
return nil
}

func (tc *testConfig) Unmarshal(component *Conf) error {
Expand Down Expand Up @@ -340,12 +373,34 @@ func TestUnmarshaler(t *testing.T) {
"string": "make sure this",
},
"another": "make sure this",
"some": "make sure this",
"some_2": "this better be",
})

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)
assert.Equal(t, "make sure this is also called", tc.EmbeddedConfig.Some)
assert.Equal(t, "this better be also called2", tc.EmbeddedConfig2.Some2)
}

func TestEmbeddedUnmarshaler(t *testing.T) {
cfgMap := NewFromStringMap(map[string]any{
"next": map[string]any{
"string": "make sure this",
},
"another": "make sure this",
"some": "make sure this",
"some_2": "this better be",
})

tc := &testConfigWithoutUnmarshaler{}
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)
assert.Equal(t, "make sure this is also called", tc.EmbeddedConfig.Some)
assert.Equal(t, "this better be also called2", tc.EmbeddedConfig2.Some2)
}

func TestUnmarshalerKeepAlreadyInitialized(t *testing.T) {
Expand Down

0 comments on commit bb5b428

Please sign in to comment.