Skip to content

Commit fbc0ce0

Browse files
authored
[confmap] confmap honors Unmarshal methods on config embedded structs. (#9635)
**Description:** This implements support for calling `Unmarshal` on embedded structs of structs being decoded. **Link to tracking Issue:** Fixes #6671 **Testing:** Unit tests. Contrib fix is open: open-telemetry/opentelemetry-collector-contrib#31406
1 parent f0473ca commit fbc0ce0

File tree

4 files changed

+210
-2
lines changed

4 files changed

+210
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: bug_fix
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver)
7+
component: confmap
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: confmap honors `Unmarshal` methods on config embedded structs.
11+
12+
# One or more tracking issues or pull requests related to the change
13+
issues: [6671]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# Optional: The change log or logs in which this entry should be included.
21+
# e.g. '[user]' or '[user, api]'
22+
# Include 'user' if the change is relevant to end users.
23+
# Include 'api' if there is a change to a library API.
24+
# Default: '[user]'
25+
change_logs: []

component/config_test.go

+27
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111

1212
"github.com/stretchr/testify/assert"
1313
"github.com/stretchr/testify/require"
14+
15+
"go.opentelemetry.io/collector/confmap"
1416
)
1517

1618
var _ fmt.Stringer = Type{}
@@ -417,3 +419,28 @@ func TestNewType(t *testing.T) {
417419
})
418420
}
419421
}
422+
423+
type configWithEmbeddedStruct struct {
424+
String string `mapstructure:"string"`
425+
Num int `mapstructure:"num"`
426+
embeddedUnmarshallingConfig
427+
}
428+
429+
type embeddedUnmarshallingConfig struct {
430+
}
431+
432+
func (euc *embeddedUnmarshallingConfig) Unmarshal(_ *confmap.Conf) error {
433+
return nil // do nothing.
434+
}
435+
func TestStructWithEmbeddedUnmarshaling(t *testing.T) {
436+
t.Skip("Skipping, to be fixed with https://github.com/open-telemetry/opentelemetry-collector/issues/7102")
437+
cfgMap := confmap.NewFromStringMap(map[string]any{
438+
"string": "foo",
439+
"num": 123,
440+
})
441+
tc := &configWithEmbeddedStruct{}
442+
err := UnmarshalConfig(cfgMap, tc)
443+
require.NoError(t, err)
444+
assert.Equal(t, "foo", tc.String)
445+
assert.Equal(t, 123, tc.Num)
446+
}

confmap/confmap.go

+39
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ func decodeConfig(m *Conf, result any, errorUnused bool) error {
157157
mapstructure.StringToTimeDurationHookFunc(),
158158
mapstructure.TextUnmarshallerHookFunc(),
159159
unmarshalerHookFunc(result),
160+
// after the main unmarshaler hook is called,
161+
// we unmarshal the embedded structs if present to merge with the result:
162+
unmarshalerEmbeddedStructsHookFunc(),
160163
zeroSliceHookFunc(),
161164
),
162165
}
@@ -261,6 +264,42 @@ func mapKeyStringToMapKeyTextUnmarshalerHookFunc() mapstructure.DecodeHookFuncTy
261264
}
262265
}
263266

267+
// unmarshalerEmbeddedStructsHookFunc provides a mechanism for embedded structs to define their own unmarshal logic,
268+
// by implementing the Unmarshaler interface.
269+
func unmarshalerEmbeddedStructsHookFunc() mapstructure.DecodeHookFuncValue {
270+
return func(from reflect.Value, to reflect.Value) (any, error) {
271+
if to.Type().Kind() != reflect.Struct {
272+
return from.Interface(), nil
273+
}
274+
fromAsMap, ok := from.Interface().(map[string]any)
275+
if !ok {
276+
return from.Interface(), nil
277+
}
278+
for i := 0; i < to.Type().NumField(); i++ {
279+
// embedded structs passed in via `squash` cannot be pointers. We just check if they are structs:
280+
if to.Type().Field(i).IsExported() && to.Type().Field(i).Anonymous {
281+
if unmarshaler, ok := to.Field(i).Addr().Interface().(Unmarshaler); ok {
282+
if err := unmarshaler.Unmarshal(NewFromStringMap(fromAsMap)); err != nil {
283+
return nil, err
284+
}
285+
// the struct we receive from this unmarshaling only contains fields related to the embedded struct.
286+
// we merge this partially unmarshaled struct with the rest of the result.
287+
// note we already unmarshaled the main struct earlier, and therefore merge with it.
288+
conf := New()
289+
if err := conf.Marshal(unmarshaler); err != nil {
290+
return nil, err
291+
}
292+
resultMap := conf.ToStringMap()
293+
for k, v := range resultMap {
294+
fromAsMap[k] = v
295+
}
296+
}
297+
}
298+
}
299+
return fromAsMap, nil
300+
}
301+
}
302+
264303
// Provides a mechanism for individual structs to define their own unmarshal logic,
265304
// by implementing the Unmarshaler interface.
266305
func unmarshalerHookFunc(result any) mapstructure.DecodeHookFuncValue {

confmap/confmap_test.go

+119-2
Original file line numberDiff line numberDiff line change
@@ -309,8 +309,78 @@ func newConfFromFile(t testing.TB, fileName string) map[string]any {
309309
}
310310

311311
type testConfig struct {
312-
Next *nextConfig `mapstructure:"next"`
313-
Another string `mapstructure:"another"`
312+
Next *nextConfig `mapstructure:"next"`
313+
Another string `mapstructure:"another"`
314+
EmbeddedConfig `mapstructure:",squash"`
315+
EmbeddedConfig2 `mapstructure:",squash"`
316+
}
317+
318+
type testConfigWithoutUnmarshaler struct {
319+
Next *nextConfig `mapstructure:"next"`
320+
Another string `mapstructure:"another"`
321+
EmbeddedConfig `mapstructure:",squash"`
322+
EmbeddedConfig2 `mapstructure:",squash"`
323+
}
324+
325+
type testConfigWithEmbeddedError struct {
326+
Next *nextConfig `mapstructure:"next"`
327+
Another string `mapstructure:"another"`
328+
EmbeddedConfigWithError `mapstructure:",squash"`
329+
}
330+
331+
type testConfigWithMarshalError struct {
332+
Next *nextConfig `mapstructure:"next"`
333+
Another string `mapstructure:"another"`
334+
EmbeddedConfigWithMarshalError `mapstructure:",squash"`
335+
}
336+
337+
func (tc *testConfigWithEmbeddedError) Unmarshal(component *Conf) error {
338+
if err := component.Unmarshal(tc, WithIgnoreUnused()); err != nil {
339+
return err
340+
}
341+
return nil
342+
}
343+
344+
type EmbeddedConfig struct {
345+
Some string `mapstructure:"some"`
346+
}
347+
348+
func (ec *EmbeddedConfig) Unmarshal(component *Conf) error {
349+
if err := component.Unmarshal(ec, WithIgnoreUnused()); err != nil {
350+
return err
351+
}
352+
ec.Some += " is also called"
353+
return nil
354+
}
355+
356+
type EmbeddedConfig2 struct {
357+
Some2 string `mapstructure:"some_2"`
358+
}
359+
360+
func (ec *EmbeddedConfig2) Unmarshal(component *Conf) error {
361+
if err := component.Unmarshal(ec, WithIgnoreUnused()); err != nil {
362+
return err
363+
}
364+
ec.Some2 += " also called2"
365+
return nil
366+
}
367+
368+
type EmbeddedConfigWithError struct {
369+
}
370+
371+
func (ecwe *EmbeddedConfigWithError) Unmarshal(_ *Conf) error {
372+
return errors.New("embedded error")
373+
}
374+
375+
type EmbeddedConfigWithMarshalError struct {
376+
}
377+
378+
func (ecwe EmbeddedConfigWithMarshalError) Marshal(_ *Conf) error {
379+
return errors.New("marshaling error")
380+
}
381+
382+
func (ecwe EmbeddedConfigWithMarshalError) Unmarshal(_ *Conf) error {
383+
return nil
314384
}
315385

316386
func (tc *testConfig) Unmarshal(component *Conf) error {
@@ -340,12 +410,59 @@ func TestUnmarshaler(t *testing.T) {
340410
"string": "make sure this",
341411
},
342412
"another": "make sure this",
413+
"some": "make sure this",
414+
"some_2": "this better be",
343415
})
344416

345417
tc := &testConfig{}
346418
assert.NoError(t, cfgMap.Unmarshal(tc))
347419
assert.Equal(t, "make sure this", tc.Another)
348420
assert.Equal(t, "make sure this is called", tc.Next.String)
421+
assert.Equal(t, "make sure this is also called", tc.EmbeddedConfig.Some)
422+
assert.Equal(t, "this better be also called2", tc.EmbeddedConfig2.Some2)
423+
}
424+
425+
func TestEmbeddedUnmarshaler(t *testing.T) {
426+
cfgMap := NewFromStringMap(map[string]any{
427+
"next": map[string]any{
428+
"string": "make sure this",
429+
},
430+
"another": "make sure this",
431+
"some": "make sure this",
432+
"some_2": "this better be",
433+
})
434+
435+
tc := &testConfigWithoutUnmarshaler{}
436+
assert.NoError(t, cfgMap.Unmarshal(tc))
437+
assert.Equal(t, "make sure this", tc.Another)
438+
assert.Equal(t, "make sure this is called", tc.Next.String)
439+
assert.Equal(t, "make sure this is also called", tc.EmbeddedConfig.Some)
440+
assert.Equal(t, "this better be also called2", tc.EmbeddedConfig2.Some2)
441+
}
442+
443+
func TestEmbeddedUnmarshalerError(t *testing.T) {
444+
cfgMap := NewFromStringMap(map[string]any{
445+
"next": map[string]any{
446+
"string": "make sure this",
447+
},
448+
"another": "make sure this",
449+
"some": "make sure this",
450+
})
451+
452+
tc := &testConfigWithEmbeddedError{}
453+
assert.EqualError(t, cfgMap.Unmarshal(tc), "error decoding '': embedded error")
454+
}
455+
456+
func TestEmbeddedMarshalerError(t *testing.T) {
457+
cfgMap := NewFromStringMap(map[string]any{
458+
"next": map[string]any{
459+
"string": "make sure this",
460+
},
461+
"another": "make sure this",
462+
})
463+
464+
tc := &testConfigWithMarshalError{}
465+
assert.EqualError(t, cfgMap.Unmarshal(tc), "error decoding '': error running encode hook: marshaling error")
349466
}
350467

351468
func TestUnmarshalerKeepAlreadyInitialized(t *testing.T) {

0 commit comments

Comments
 (0)