// Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 package mapstructure // import "go.opentelemetry.io/collector/confmap/internal/mapstructure" import ( "encoding" "errors" "fmt" "reflect" "strings" "github.com/go-viper/mapstructure/v2" yaml "sigs.k8s.io/yaml/goyaml.v3" ) const ( tagNameMapStructure = "mapstructure" optionSeparator = "," optionOmitEmpty = "omitempty" optionSquash = "squash" optionRemain = "remain" optionSkip = "-" ) var errNonStringEncodedKey = errors.New("non string-encoded key") // tagInfo stores the mapstructure tag details. type tagInfo struct { name string omitEmpty bool squash bool } // An Encoder takes structured data and converts it into an // interface following the mapstructure tags. type Encoder struct { config *EncoderConfig } // EncoderConfig is the configuration used to create a new encoder. type EncoderConfig struct { // EncodeHook, if set, is a way to provide custom encoding. It // will be called before structs and primitive types. EncodeHook mapstructure.DecodeHookFunc } // New returns a new encoder for the configuration. func New(cfg *EncoderConfig) *Encoder { return &Encoder{config: cfg} } // Encode takes the input and uses reflection to encode it to // an interface based on the mapstructure spec. func (e *Encoder) Encode(input any) (any, error) { return e.encode(reflect.ValueOf(input)) } // encode processes the value based on the reflect.Kind. func (e *Encoder) encode(value reflect.Value) (any, error) { if value.IsValid() { switch value.Kind() { case reflect.Interface, reflect.Ptr: return e.encode(value.Elem()) case reflect.Map: return e.encodeMap(value) case reflect.Slice: return e.encodeSlice(value) case reflect.Struct: return e.encodeStruct(value) default: return e.encodeHook(value) } } return nil, nil } // encodeHook calls the EncodeHook in the EncoderConfig with the value passed in. // This is called before processing structs and for primitive data types. func (e *Encoder) encodeHook(value reflect.Value) (any, error) { if e.config != nil && e.config.EncodeHook != nil { out, err := mapstructure.DecodeHookExec(e.config.EncodeHook, value, value) if err != nil { return nil, fmt.Errorf("error running encode hook: %w", err) } return out, nil } return value.Interface(), nil } // encodeStruct encodes the struct by iterating over the fields, getting the // mapstructure tagInfo for each exported field, and encoding the value. func (e *Encoder) encodeStruct(value reflect.Value) (any, error) { if value.Kind() != reflect.Struct { return nil, &reflect.ValueError{ Method: "encodeStruct", Kind: value.Kind(), } } out, err := e.encodeHook(value) if err != nil { return nil, err } value = reflect.ValueOf(out) // if the output of encodeHook is no longer a struct, // call encode against it. if value.Kind() != reflect.Struct { return e.encode(value) } result := make(map[string]any) for i := 0; i < value.NumField(); i++ { field := value.Field(i) if field.CanInterface() { info := getTagInfo(value.Type().Field(i)) if (info.omitEmpty && field.IsZero()) || info.name == optionSkip { continue } encoded, err := e.encode(field) if err != nil { return nil, fmt.Errorf("error encoding field %q: %w", info.name, err) } if info.squash { if m, ok := encoded.(map[string]any); ok { for k, v := range m { result[k] = v } } } else { result[info.name] = encoded } } } return result, nil } // encodeSlice iterates over the slice and encodes each of the elements. func (e *Encoder) encodeSlice(value reflect.Value) (any, error) { if value.Kind() != reflect.Slice { return nil, &reflect.ValueError{ Method: "encodeSlice", Kind: value.Kind(), } } if value.IsNil() { return []any(nil), nil } result := make([]any, value.Len()) for i := 0; i < value.Len(); i++ { var err error if result[i], err = e.encode(value.Index(i)); err != nil { return nil, fmt.Errorf("error encoding element in slice at index %d: %w", i, err) } } return result, nil } // encodeMap encodes a map by encoding the key and value. Returns errNonStringEncodedKey // if the key is not encoded into a string. func (e *Encoder) encodeMap(value reflect.Value) (any, error) { if value.Kind() != reflect.Map { return nil, &reflect.ValueError{ Method: "encodeMap", Kind: value.Kind(), } } result := make(map[string]any) iterator := value.MapRange() for iterator.Next() { encoded, err := e.encode(iterator.Key()) if err != nil { return nil, fmt.Errorf("error encoding key: %w", err) } v := reflect.ValueOf(encoded) var key string switch v.Kind() { case reflect.String: key = v.String() default: return nil, fmt.Errorf("%w, key: %q, kind: %v, type: %T", errNonStringEncodedKey, iterator.Key().Interface(), iterator.Key().Kind(), encoded) } if _, ok := result[key]; ok { return nil, fmt.Errorf("duplicate key %q while encoding", key) } if result[key], err = e.encode(iterator.Value()); err != nil { return nil, fmt.Errorf("error encoding map value for key %q: %w", key, err) } } return result, nil } // getTagInfo looks up the mapstructure tag and uses that if available. // Uses the lowercase field if not found. Checks for omitempty and squash. func getTagInfo(field reflect.StructField) *tagInfo { info := tagInfo{} if tag, ok := field.Tag.Lookup(tagNameMapStructure); ok { options := strings.Split(tag, optionSeparator) info.name = options[0] if len(options) > 1 { for _, option := range options[1:] { switch option { case optionOmitEmpty: info.omitEmpty = true case optionSquash, optionRemain: info.squash = true } } } } else { info.name = strings.ToLower(field.Name) } return &info } // TextMarshalerHookFunc returns a DecodeHookFuncValue that checks // for the encoding.TextMarshaler interface and calls the MarshalText // function if found. func TextMarshalerHookFunc() mapstructure.DecodeHookFuncValue { return func(from reflect.Value, _ reflect.Value) (any, error) { marshaler, ok := from.Interface().(encoding.TextMarshaler) if !ok { return from.Interface(), nil } out, err := marshaler.MarshalText() if err != nil { return nil, err } return string(out), nil } } // YamlMarshalerHookFunc returns a DecodeHookFuncValue that checks for structs // that have yaml tags but no mapstructure tags. If found, it will convert the struct // to map[string]any using the yaml package, which respects the yaml tags. Ultimately, // this allows mapstructure to later marshal the map[string]any in a generic way. func YamlMarshalerHookFunc() mapstructure.DecodeHookFuncValue { return func(from reflect.Value, _ reflect.Value) (any, error) { if from.Kind() == reflect.Struct { for i := 0; i < from.NumField(); i++ { if _, ok := from.Type().Field(i).Tag.Lookup("mapstructure"); ok { // The struct has at least one mapstructure tag so don't do anything. return from.Interface(), nil } if _, ok := from.Type().Field(i).Tag.Lookup("yaml"); ok { // The struct has at least one yaml tag, so convert it to map[string]any using yaml. yamlBytes, err := yaml.Marshal(from.Interface()) if err != nil { return nil, err } var m map[string]any err = yaml.Unmarshal(yamlBytes, &m) if err != nil { return nil, err } return m, nil } } } return from.Interface(), nil } }