Skip to content

Commit 55dea59

Browse files
committed
Extend unmarshal hook to check map key string to any TextUnmarshaler not only ComponentID
Signed-off-by: Bogdan Drutu <[email protected]>
1 parent dd4096b commit 55dea59

File tree

3 files changed

+128
-54
lines changed

3 files changed

+128
-54
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626

2727
### 💡 Enhancements 💡
2828

29+
- Extend config.Map.Unmarshal hook to check map key string to any TextUnmarshaler not only ComponentID (#5244)
30+
2931
### 🧰 Bug fixes 🧰
3032
- Fix translation from otlp.Request to pdata representation, changes to the returned pdata not all reflected to the otlp.Request (#5197)
3133
- `exporterhelper` now properly consumes any remaining items on stop (#5203)

config/configmap.go

+52-48
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package config // import "go.opentelemetry.io/collector/config"
1616

1717
import (
1818
"context"
19+
"encoding"
1920
"fmt"
2021
"reflect"
2122

@@ -139,23 +140,15 @@ func decoderConfig(result interface{}) *mapstructure.DecoderConfig {
139140
TagName: "mapstructure",
140141
WeaklyTypedInput: true,
141142
DecodeHook: mapstructure.ComposeDecodeHookFunc(
142-
expandNilStructPointersFunc,
143-
stringToSliceHookFunc,
144-
mapStringToMapComponentIDHookFunc,
145-
stringToTimeDurationHookFunc,
146-
textUnmarshallerHookFunc,
143+
expandNilStructPointersHookFunc(),
144+
mapstructure.StringToSliceHookFunc(","),
145+
mapKeyStringToMapKeyTextUnmarshalerHookFunc(),
146+
mapstructure.StringToTimeDurationHookFunc(),
147+
mapstructure.TextUnmarshallerHookFunc(),
147148
),
148149
}
149150
}
150151

151-
var (
152-
stringToSliceHookFunc = mapstructure.StringToSliceHookFunc(",")
153-
stringToTimeDurationHookFunc = mapstructure.StringToTimeDurationHookFunc()
154-
textUnmarshallerHookFunc = mapstructure.TextUnmarshallerHookFunc()
155-
156-
componentIDType = reflect.TypeOf(NewComponentID("foo"))
157-
)
158-
159152
// In cases where a config has a mapping of something to a struct pointers
160153
// we want nil values to resolve to a pointer to the zero value of the
161154
// underlying struct just as we want nil values of a mapping of something
@@ -170,51 +163,62 @@ var (
170163
//
171164
// we want an unmarshaled Config to be equivalent to
172165
// Config{Thing: &SomeStruct{}} instead of Config{Thing: nil}
173-
var expandNilStructPointersFunc = func(from reflect.Value, to reflect.Value) (interface{}, error) {
174-
// ensure we are dealing with map to map comparison
175-
if from.Kind() == reflect.Map && to.Kind() == reflect.Map {
176-
toElem := to.Type().Elem()
177-
// ensure that map values are pointers to a struct
178-
// (that may be nil and require manual setting w/ zero value)
179-
if toElem.Kind() == reflect.Ptr && toElem.Elem().Kind() == reflect.Struct {
180-
fromRange := from.MapRange()
181-
for fromRange.Next() {
182-
fromKey := fromRange.Key()
183-
fromValue := fromRange.Value()
184-
// ensure that we've run into a nil pointer instance
185-
if fromValue.IsNil() {
186-
newFromValue := reflect.New(toElem.Elem())
187-
from.SetMapIndex(fromKey, newFromValue)
166+
func expandNilStructPointersHookFunc() mapstructure.DecodeHookFuncValue {
167+
return func(from reflect.Value, to reflect.Value) (interface{}, error) {
168+
// ensure we are dealing with map to map comparison
169+
if from.Kind() == reflect.Map && to.Kind() == reflect.Map {
170+
toElem := to.Type().Elem()
171+
// ensure that map values are pointers to a struct
172+
// (that may be nil and require manual setting w/ zero value)
173+
if toElem.Kind() == reflect.Ptr && toElem.Elem().Kind() == reflect.Struct {
174+
fromRange := from.MapRange()
175+
for fromRange.Next() {
176+
fromKey := fromRange.Key()
177+
fromValue := fromRange.Value()
178+
// ensure that we've run into a nil pointer instance
179+
if fromValue.IsNil() {
180+
newFromValue := reflect.New(toElem.Elem())
181+
from.SetMapIndex(fromKey, newFromValue)
182+
}
188183
}
189184
}
190185
}
186+
return from.Interface(), nil
191187
}
192-
return from.Interface(), nil
193188
}
194189

195-
// mapStringToMapComponentIDHookFunc returns a DecodeHookFunc that converts a map[string]interface{} to
196-
// map[ComponentID]interface{}.
197-
// This is needed in combination since the ComponentID.UnmarshalText may produce equal IDs for different strings,
190+
// mapKeyStringToMapKeyTextUnmarshalerHookFunc returns a DecodeHookFuncType that checks that a conversion from
191+
// map[string]interface{} to map[encoding.TextUnmarshaler]interface{} does not overwrite keys,
192+
// when UnmarshalText produces equal elements from different strings (e.g. trims whitespaces).
193+
//
194+
// This is needed in combination with ComponentID, which may produce equal IDs for different strings,
198195
// and an error needs to be returned in that case, otherwise the last equivalent ID overwrites the previous one.
199-
var mapStringToMapComponentIDHookFunc = func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
200-
if f.Kind() != reflect.Map || f.Key().Kind() != reflect.String {
201-
return data, nil
202-
}
196+
func mapKeyStringToMapKeyTextUnmarshalerHookFunc() mapstructure.DecodeHookFuncType {
197+
return func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
198+
if f.Kind() != reflect.Map || f.Key().Kind() != reflect.String {
199+
return data, nil
200+
}
203201

204-
if t.Kind() != reflect.Map || t.Key() != componentIDType {
205-
return data, nil
206-
}
202+
if t.Kind() != reflect.Map {
203+
return data, nil
204+
}
207205

208-
m := make(map[ComponentID]interface{})
209-
for k, v := range data.(map[string]interface{}) {
210-
id, err := NewComponentIDFromString(k)
211-
if err != nil {
212-
return nil, err
206+
if _, ok := reflect.New(t.Key()).Interface().(encoding.TextUnmarshaler); !ok {
207+
return data, nil
213208
}
214-
if _, ok := m[id]; ok {
215-
return nil, fmt.Errorf("duplicate name %q after trimming spaces %v", k, id)
209+
210+
m := reflect.MakeMap(reflect.MapOf(t.Key(), reflect.TypeOf(true)))
211+
for k := range data.(map[string]interface{}) {
212+
tKey := reflect.New(t.Key())
213+
if err := tKey.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(k)); err != nil {
214+
return nil, err
215+
}
216+
217+
if m.MapIndex(reflect.Indirect(tKey)).IsValid() {
218+
return nil, fmt.Errorf("duplicate name %q after trimming spaces %v", k, tKey)
219+
}
220+
m.SetMapIndex(reflect.Indirect(tKey), reflect.ValueOf(true))
216221
}
217-
m[id] = v
222+
return data, nil
218223
}
219-
return m, nil
220224
}

config/configmap_test.go

+74-6
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515
package config
1616

1717
import (
18+
"errors"
1819
"fmt"
1920
"io/ioutil"
2021
"path/filepath"
22+
"strings"
2123
"testing"
2224

2325
"github.com/stretchr/testify/assert"
@@ -124,18 +126,18 @@ func newMapFromFile(fileName string) (*Map, error) {
124126
return NewMapFromStringMap(data), nil
125127
}
126128

127-
func TestExpandNilStructPointersFunc(t *testing.T) {
129+
func TestExpandNilStructPointersHookFunc(t *testing.T) {
128130
stringMap := map[string]interface{}{
129131
"boolean": nil,
130132
"struct": nil,
131133
"map_struct": map[string]interface{}{
132134
"struct": nil,
133135
},
134136
}
135-
parser := NewMapFromStringMap(stringMap)
137+
cfgMap := NewMapFromStringMap(stringMap)
136138
cfg := &TestConfig{}
137139
assert.Nil(t, cfg.Struct)
138-
assert.NoError(t, parser.UnmarshalExact(cfg))
140+
assert.NoError(t, cfgMap.UnmarshalExact(cfg))
139141
assert.Nil(t, cfg.Boolean)
140142
// assert.False(t, *cfg.Boolean)
141143
assert.Nil(t, cfg.Struct)
@@ -144,15 +146,15 @@ func TestExpandNilStructPointersFunc(t *testing.T) {
144146
assert.Equal(t, &Struct{}, cfg.MapStruct["struct"])
145147
}
146148

147-
func TestExpandNilStructPointersFunc_DefaultNotNilConfigNil(t *testing.T) {
149+
func TestExpandNilStructPointersHookFuncDefaultNotNilConfigNil(t *testing.T) {
148150
stringMap := map[string]interface{}{
149151
"boolean": nil,
150152
"struct": nil,
151153
"map_struct": map[string]interface{}{
152154
"struct": nil,
153155
},
154156
}
155-
parser := NewMapFromStringMap(stringMap)
157+
cfgMap := NewMapFromStringMap(stringMap)
156158
varBool := true
157159
s1 := &Struct{Name: "s1"}
158160
s2 := &Struct{Name: "s2"}
@@ -161,7 +163,7 @@ func TestExpandNilStructPointersFunc_DefaultNotNilConfigNil(t *testing.T) {
161163
Struct: s1,
162164
MapStruct: map[string]*Struct{"struct": s2},
163165
}
164-
assert.NoError(t, parser.UnmarshalExact(cfg))
166+
assert.NoError(t, cfgMap.UnmarshalExact(cfg))
165167
assert.NotNil(t, cfg.Boolean)
166168
assert.True(t, *cfg.Boolean)
167169
assert.NotNil(t, cfg.Struct)
@@ -180,3 +182,69 @@ type TestConfig struct {
180182
type Struct struct {
181183
Name string
182184
}
185+
186+
type TestID string
187+
188+
func (tID *TestID) UnmarshalText(text []byte) error {
189+
strID := string(text)
190+
if strings.HasSuffix(strID, "_") {
191+
strID = strID[:len(strID)-1]
192+
}
193+
*tID = TestID(strID)
194+
return nil
195+
}
196+
197+
type TestIDConfig struct {
198+
Boolean bool `mapstructure:"bool"`
199+
Map map[TestID]string `mapstructure:"map"`
200+
}
201+
202+
func TestMapKeyStringToMapKeyTextUnmarshalerHookFunc(t *testing.T) {
203+
stringMap := map[string]interface{}{
204+
"bool": true,
205+
"map": map[string]interface{}{
206+
"string": "this is a string",
207+
},
208+
}
209+
cfgMap := NewMapFromStringMap(stringMap)
210+
cfg := &TestIDConfig{}
211+
assert.NoError(t, cfgMap.UnmarshalExact(cfg))
212+
assert.True(t, cfg.Boolean)
213+
assert.Equal(t, map[TestID]string{"string": "this is a string"}, cfg.Map)
214+
}
215+
216+
func TestMapKeyStringToMapKeyTextUnmarshalerHookFuncDuplicateID(t *testing.T) {
217+
stringMap := map[string]interface{}{
218+
"bool": true,
219+
"map": map[string]interface{}{
220+
"string": "this is a string",
221+
"string_": "this is another string",
222+
},
223+
}
224+
cfgMap := NewMapFromStringMap(stringMap)
225+
cfg := &TestIDConfig{}
226+
assert.Error(t, cfgMap.UnmarshalExact(cfg))
227+
}
228+
229+
type ErrorID string
230+
231+
func (*ErrorID) UnmarshalText(text []byte) error {
232+
return errors.New("parsing error")
233+
}
234+
235+
type ErrorIDConfig struct {
236+
Boolean bool `mapstructure:"bool"`
237+
Map map[ErrorID]string `mapstructure:"map"`
238+
}
239+
240+
func TestMapKeyStringToMapKeyTextUnmarshalerHookFuncErrorUnmarshal(t *testing.T) {
241+
stringMap := map[string]interface{}{
242+
"bool": true,
243+
"map": map[string]interface{}{
244+
"string": "this is a string",
245+
},
246+
}
247+
cfgMap := NewMapFromStringMap(stringMap)
248+
cfg := &ErrorIDConfig{}
249+
assert.Error(t, cfgMap.UnmarshalExact(cfg))
250+
}

0 commit comments

Comments
 (0)