Skip to content

Commit cfebf0b

Browse files
committed
Fully override default slice fields of config (open-telemetry#4001)
mapstructure library doesn't override full slice during unmarshalling. Origin issue: mitchellh/mapstructure#74 (comment) To address this we empty every slice before unmarshalling unless user provided slice is nil.
1 parent b45d0ed commit cfebf0b

File tree

3 files changed

+154
-0
lines changed

3 files changed

+154
-0
lines changed

.chloggen/config-slice-fields.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
change_type: "bug_fix"
2+
component: "config"
3+
note: "Fully override default slice fields of config"
4+
issues: [4001]
5+
subtext:

confmap/confmap.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ func decodeConfig(m *Conf, result interface{}, errorUnused bool) error {
168168
mapstructure.StringToTimeDurationHookFunc(),
169169
mapstructure.TextUnmarshallerHookFunc(),
170170
unmarshalerHookFunc(result),
171+
zeroesSliceHookFunc(),
171172
),
172173
}
173174
decoder, err := mapstructure.NewDecoder(dc)
@@ -320,6 +321,23 @@ func marshalerHookFunc(orig interface{}) mapstructure.DecodeHookFuncValue {
320321
}
321322
}
322323

324+
// mapstructure library doesn't override full slice during unmarshalling.
325+
// Origin issue: https://github.com/mitchellh/mapstructure/issues/74#issuecomment-279886492
326+
// To address this we empty every slice before unmarshalling unless user provided slice is nil.
327+
func zeroesSliceHookFunc() mapstructure.DecodeHookFuncValue {
328+
return func(from reflect.Value, to reflect.Value) (interface{}, error) {
329+
if from.Kind() == reflect.Slice && from.IsNil() {
330+
return from.Interface(), nil
331+
}
332+
333+
if to.CanSet() && to.Kind() == reflect.Slice {
334+
to.Set(reflect.MakeSlice(reflect.SliceOf(to.Type().Elem()), 0, 0))
335+
}
336+
337+
return from.Interface(), nil
338+
}
339+
}
340+
323341
// Unmarshaler interface may be implemented by types to customize their behavior when being unmarshaled from a Conf.
324342
type Unmarshaler interface {
325343
// Unmarshal a Conf into the struct in a custom way.

confmap/confmap_test.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,3 +400,134 @@ func TestUnmarshalerErr(t *testing.T) {
400400
assert.EqualError(t, cfgMap.Unmarshal(tc), expectErr)
401401
assert.Empty(t, tc.Err.Foo)
402402
}
403+
404+
func TestUnmarshalerSlices(t *testing.T) {
405+
type innerStructWithSlices struct {
406+
Ints []int `mapstructure:"ints"`
407+
}
408+
type structWithSlices struct {
409+
Strings []string `mapstructure:"strings"`
410+
Nested innerStructWithSlices `mapstructure:"nested"`
411+
}
412+
413+
tests := []struct {
414+
name string
415+
cfg map[string]any
416+
provided any
417+
expected any
418+
}{
419+
{
420+
name: "overridden by slice",
421+
cfg: map[string]any{
422+
"strings": []string{"111"},
423+
},
424+
provided: &structWithSlices{
425+
Strings: []string{"xxx", "yyyy", "zzzz"},
426+
},
427+
expected: &structWithSlices{
428+
Strings: []string{"111"},
429+
},
430+
},
431+
{
432+
name: "overridden by a bigger slice",
433+
cfg: map[string]any{
434+
"strings": []string{"111", "222", "333"},
435+
},
436+
provided: &structWithSlices{
437+
Strings: []string{"xxx", "yyyy"},
438+
},
439+
expected: &structWithSlices{
440+
Strings: []string{"111", "222", "333"},
441+
},
442+
},
443+
{
444+
name: "overridden by an empty slice",
445+
cfg: map[string]any{
446+
"strings": []string{},
447+
},
448+
provided: &structWithSlices{
449+
Strings: []string{"xxx", "yyyy"},
450+
},
451+
expected: &structWithSlices{
452+
Strings: []string{},
453+
},
454+
},
455+
{
456+
name: "not overridden by nil slice",
457+
cfg: map[string]any{
458+
"strings": []string(nil),
459+
},
460+
provided: &structWithSlices{
461+
Strings: []string{"xxx", "yyyy"},
462+
},
463+
expected: &structWithSlices{
464+
Strings: []string{"xxx", "yyyy"},
465+
},
466+
},
467+
{
468+
name: "not overridden by nil",
469+
cfg: map[string]any{
470+
"strings": nil,
471+
},
472+
provided: &structWithSlices{
473+
Strings: []string{"xxx", "yyyy"},
474+
},
475+
expected: &structWithSlices{
476+
Strings: []string{"xxx", "yyyy"},
477+
},
478+
},
479+
{
480+
name: "not overridden by missing value",
481+
cfg: map[string]any{},
482+
provided: &structWithSlices{
483+
Strings: []string{"xxx", "yyyy"},
484+
},
485+
expected: &structWithSlices{
486+
Strings: []string{"xxx", "yyyy"},
487+
},
488+
},
489+
{
490+
name: "overridden by nested slice",
491+
cfg: map[string]any{
492+
"nested": map[string]any{
493+
"ints": []int{777},
494+
},
495+
},
496+
provided: &structWithSlices{
497+
Nested: innerStructWithSlices{
498+
Ints: []int{1, 2, 3},
499+
},
500+
},
501+
expected: &structWithSlices{
502+
Nested: innerStructWithSlices{
503+
Ints: []int{777},
504+
},
505+
},
506+
},
507+
{
508+
name: "overridden by weakly typed input",
509+
cfg: map[string]any{
510+
"strings": "111",
511+
},
512+
provided: &structWithSlices{
513+
Strings: []string{"xxx", "yyyy", "zzzz"},
514+
},
515+
expected: &structWithSlices{
516+
Strings: []string{"111"},
517+
},
518+
},
519+
}
520+
521+
for _, tt := range tests {
522+
tt := tt
523+
524+
t.Run(tt.name, func(t *testing.T) {
525+
cfg := NewFromStringMap(tt.cfg)
526+
527+
err := cfg.Unmarshal(tt.provided)
528+
if assert.NoError(t, err) {
529+
assert.Equal(t, tt.expected, tt.provided)
530+
}
531+
})
532+
}
533+
}

0 commit comments

Comments
 (0)