Skip to content

Commit ca970c1

Browse files
committed
Fully overrides 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 zeroes every slice before unmarshalling unless user provided slice is nil.
1 parent e9311f6 commit ca970c1

File tree

3 files changed

+138
-0
lines changed

3 files changed

+138
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
- Expose `AsRaw` and `FromRaw` `pcommon.Value` methods (#6090)
6161
- Convert `ValueTypeBytes` attributes in logging exporter (#6153)
6262
- Updated how `telemetryInitializer` is created so it's instanced per Collector instance rather than global to the process (#6138)
63+
- Fully overrides default slice fields of config (#4001)
6364

6465
## v0.60.0 Beta
6566

confmap/confmap.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ func decodeConfig(m *Conf, result interface{}, errorUnused bool) error {
140140
mapstructure.StringToTimeDurationHookFunc(),
141141
mapstructure.TextUnmarshallerHookFunc(),
142142
unmarshalerHookFunc(result),
143+
zeroesSliceHookFunc(),
143144
),
144145
}
145146
decoder, err := mapstructure.NewDecoder(dc)
@@ -292,6 +293,23 @@ func marshalerHookFunc(orig interface{}) mapstructure.DecodeHookFuncValue {
292293
}
293294
}
294295

296+
// mapstructure library doesn't override full slice during unmarshalling.
297+
// Origin issue: https://github.com/mitchellh/mapstructure/issues/74#issuecomment-279886492
298+
// To address this we zeroes every slice before unmarshalling unless user provided slice is nil.
299+
func zeroesSliceHookFunc() mapstructure.DecodeHookFuncValue {
300+
return func(from reflect.Value, to reflect.Value) (interface{}, error) {
301+
if from.Kind() == reflect.Slice && from.IsNil() {
302+
return from.Interface(), nil
303+
}
304+
305+
if to.CanSet() && to.Kind() == reflect.Slice {
306+
to.Set(reflect.MakeSlice(reflect.SliceOf(to.Type().Elem()), 0, 0))
307+
}
308+
309+
return from.Interface(), nil
310+
}
311+
}
312+
295313
// Unmarshaler interface may be implemented by types to customize their behavior when being unmarshaled from a Conf.
296314
type Unmarshaler interface {
297315
// Unmarshal a Conf into the struct in a custom way.

confmap/confmap_test.go

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

0 commit comments

Comments
 (0)