Skip to content

Commit bd13c61

Browse files
authored
Simplify unmarshal logic by adding more supported hooks (#4237)
* Simplify unmarshal logic by adding more supported hooks * Add hook that supports "String -> encoding.TextUnmarshaler", e.g. zapcore.Level no longer need special unmarshaling * Add hook that supports "String -> ComponentID" * Add a special hook for map[string]interface{} -> map[ComponentID]interface{} to determine duplicates after space trimming, not sure if this error needs this special treatment. Signed-off-by: Bogdan Drutu <[email protected]> * Fix review comments Signed-off-by: Bogdan Drutu <[email protected]> * Improve error messages for unmarshaling errors of the ComponentID Signed-off-by: Bogdan Drutu <[email protected]>
1 parent b927925 commit bd13c61

12 files changed

+150
-236
lines changed

config/config.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ type Service struct {
159159

160160
// ServiceTelemetry defines the configurable settings for service telemetry.
161161
type ServiceTelemetry struct {
162-
Logs ServiceTelemetryLogs
162+
Logs ServiceTelemetryLogs `mapstructure:"logs"`
163163
}
164164

165165
func (srvT *ServiceTelemetry) validate() error {
@@ -171,15 +171,15 @@ func (srvT *ServiceTelemetry) validate() error {
171171
// the collector uses mapstructure and not yaml tags.
172172
type ServiceTelemetryLogs struct {
173173
// Level is the minimum enabled logging level.
174-
Level zapcore.Level
174+
Level zapcore.Level `mapstructure:"level"`
175175

176176
// Development puts the logger in development mode, which changes the
177177
// behavior of DPanicLevel and takes stacktraces more liberally.
178-
Development bool
178+
Development bool `mapstructure:"development"`
179179

180180
// Encoding sets the logger's encoding.
181181
// Valid values are "json" and "console".
182-
Encoding string
182+
Encoding string `mapstructure:"encoding"`
183183
}
184184

185185
func (srvTL *ServiceTelemetryLogs) validate() error {
@@ -226,9 +226,9 @@ const (
226226
type Pipeline struct {
227227
Name string
228228
InputType DataType
229-
Receivers []ComponentID
230-
Processors []ComponentID
231-
Exporters []ComponentID
229+
Receivers []ComponentID `mapstructure:"receivers"`
230+
Processors []ComponentID `mapstructure:"processors"`
231+
Exporters []ComponentID `mapstructure:"exporters"`
232232
}
233233

234234
// Pipelines is a map of names to Pipelines.

config/configgrpc/configgrpc_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ func TestGRPCClientSettingsError(t *testing.T) {
209209
},
210210
},
211211
{
212-
err: "idStr must have non empty type",
212+
err: "id must not be empty",
213213
settings: GRPCClientSettings{
214214
Endpoint: "localhost:1234",
215215
Auth: &configauth.Authentication{},

config/confighttp/confighttp_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ func TestHTTPClientSettingsError(t *testing.T) {
135135
},
136136
},
137137
{
138-
err: "idStr must have non empty type",
138+
err: "id must not be empty",
139139
settings: HTTPClientSettings{
140140
Endpoint: "https://localhost:1234/v1/traces",
141141
Auth: &configauth.Authentication{AuthenticatorName: ""},

config/configmap.go

+58-23
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,8 @@ func (l *Map) Unmarshal(rawVal interface{}) error {
9696
}
9797

9898
// UnmarshalExact unmarshalls the config into a struct, erroring if a field is nonexistent.
99-
func (l *Map) UnmarshalExact(intoCfg interface{}) error {
100-
dc := decoderConfig(intoCfg)
99+
func (l *Map) UnmarshalExact(rawVal interface{}) error {
100+
dc := decoderConfig(rawVal)
101101
dc.ErrorUnused = true
102102
decoder, err := mapstructure.NewDecoder(dc)
103103
if err != nil {
@@ -164,13 +164,23 @@ func decoderConfig(result interface{}) *mapstructure.DecoderConfig {
164164
TagName: "mapstructure",
165165
WeaklyTypedInput: true,
166166
DecodeHook: mapstructure.ComposeDecodeHookFunc(
167-
expandNilStructPointers(),
168-
mapstructure.StringToTimeDurationHookFunc(),
169-
mapstructure.StringToSliceHookFunc(","),
167+
expandNilStructPointersFunc,
168+
stringToSliceHookFunc,
169+
mapStringToMapComponentIDHookFunc,
170+
stringToTimeDurationHookFunc,
171+
textUnmarshallerHookFunc,
170172
),
171173
}
172174
}
173175

176+
var (
177+
stringToSliceHookFunc = mapstructure.StringToSliceHookFunc(",")
178+
stringToTimeDurationHookFunc = mapstructure.StringToTimeDurationHookFunc()
179+
textUnmarshallerHookFunc = mapstructure.TextUnmarshallerHookFunc()
180+
181+
componentIDType = reflect.TypeOf(NewComponentID("foo"))
182+
)
183+
174184
// In cases where a config has a mapping of something to a struct pointers
175185
// we want nil values to resolve to a pointer to the zero value of the
176186
// underlying struct just as we want nil values of a mapping of something
@@ -185,26 +195,51 @@ func decoderConfig(result interface{}) *mapstructure.DecoderConfig {
185195
//
186196
// we want an unmarshaled Config to be equivalent to
187197
// Config{Thing: &SomeStruct{}} instead of Config{Thing: nil}
188-
func expandNilStructPointers() mapstructure.DecodeHookFunc {
189-
return func(from reflect.Value, to reflect.Value) (interface{}, error) {
190-
// ensure we are dealing with map to map comparison
191-
if from.Kind() == reflect.Map && to.Kind() == reflect.Map {
192-
toElem := to.Type().Elem()
193-
// ensure that map values are pointers to a struct
194-
// (that may be nil and require manual setting w/ zero value)
195-
if toElem.Kind() == reflect.Ptr && toElem.Elem().Kind() == reflect.Struct {
196-
fromRange := from.MapRange()
197-
for fromRange.Next() {
198-
fromKey := fromRange.Key()
199-
fromValue := fromRange.Value()
200-
// ensure that we've run into a nil pointer instance
201-
if fromValue.IsNil() {
202-
newFromValue := reflect.New(toElem.Elem())
203-
from.SetMapIndex(fromKey, newFromValue)
204-
}
198+
var expandNilStructPointersFunc = func(from reflect.Value, to reflect.Value) (interface{}, error) {
199+
// ensure we are dealing with map to map comparison
200+
if from.Kind() == reflect.Map && to.Kind() == reflect.Map {
201+
toElem := to.Type().Elem()
202+
// ensure that map values are pointers to a struct
203+
// (that may be nil and require manual setting w/ zero value)
204+
if toElem.Kind() == reflect.Ptr && toElem.Elem().Kind() == reflect.Struct {
205+
fromRange := from.MapRange()
206+
for fromRange.Next() {
207+
fromKey := fromRange.Key()
208+
fromValue := fromRange.Value()
209+
// ensure that we've run into a nil pointer instance
210+
if fromValue.IsNil() {
211+
newFromValue := reflect.New(toElem.Elem())
212+
from.SetMapIndex(fromKey, newFromValue)
205213
}
206214
}
207215
}
208-
return from.Interface(), nil
209216
}
217+
return from.Interface(), nil
218+
}
219+
220+
// mapStringToMapComponentIDHookFunc returns a DecodeHookFunc that converts a map[string]interface{} to
221+
// map[ComponentID]interface{}.
222+
// This is needed in combination since the ComponentID.UnmarshalText may produce equal IDs for different strings,
223+
// and an error needs to be returned in that case, otherwise the last equivalent ID overwrites the previous one.
224+
var mapStringToMapComponentIDHookFunc = func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
225+
if f.Kind() != reflect.Map || f.Key().Kind() != reflect.String {
226+
return data, nil
227+
}
228+
229+
if t.Kind() != reflect.Map || t.Key() != componentIDType {
230+
return data, nil
231+
}
232+
233+
m := make(map[ComponentID]interface{})
234+
for k, v := range data.(map[string]interface{}) {
235+
id, err := NewComponentIDFromString(k)
236+
if err != nil {
237+
return nil, err
238+
}
239+
if _, ok := m[id]; ok {
240+
return nil, fmt.Errorf("duplicate name %q after trimming spaces %v", k, id)
241+
}
242+
m[id] = v
243+
}
244+
return m, nil
210245
}

0 commit comments

Comments
 (0)