// Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 package mapstructure import ( "encoding" "errors" "reflect" "strings" "testing" "github.com/go-viper/mapstructure/v2" "github.com/stretchr/testify/require" ) type TestComplexStruct struct { Skipped TestEmptyStruct `mapstructure:",squash"` Nested TestSimpleStruct `mapstructure:",squash"` Slice []TestSimpleStruct `mapstructure:"slice,omitempty"` Pointer *TestSimpleStruct `mapstructure:"ptr"` Map map[string]TestSimpleStruct `mapstructure:"map,omitempty"` Remain map[string]any `mapstructure:",remain"` TranslatedYaml TestYamlStruct `mapstructure:"translated"` SquashedYaml TestYamlStruct `mapstructure:",squash"` PointerTranslatedYaml *TestPtrToYamlStruct `mapstructure:"translated_ptr"` PointerSquashedYaml *TestPtrToYamlStruct `mapstructure:",squash"` Interface encoding.TextMarshaler } type TestSimpleStruct struct { Value string `mapstructure:"value"` skipped string err error } type TestEmptyStruct struct { Value string `mapstructure:"-"` } type TestYamlStruct struct { YamlValue string `yaml:"yaml_value"` YamlOmitEmpty string `yaml:"yaml_omit,omitempty"` YamlInline TestYamlSimpleStruct `yaml:",inline"` } type TestPtrToYamlStruct struct { YamlValue string `yaml:"yaml_value_ptr"` YamlOmitEmpty string `yaml:"yaml_omit_ptr,omitempty"` YamlInline *TestYamlPtrToSimpleStruct `yaml:",inline"` } type TestYamlSimpleStruct struct { Inline string `yaml:"yaml_inline"` } type TestYamlPtrToSimpleStruct struct { InlinePtr string `yaml:"yaml_inline_ptr"` } type TestID string func (tID TestID) MarshalText() (text []byte, err error) { out := string(tID) if out == "error" { return nil, errors.New("parsing error") } if !strings.HasSuffix(out, "_") { out += "_" } return []byte(out), nil } type TestStringLike string func TestEncode(t *testing.T) { enc := New(&EncoderConfig{ EncodeHook: mapstructure.ComposeDecodeHookFunc( YamlMarshalerHookFunc(), TextMarshalerHookFunc(), ), }) testCases := map[string]struct { input any want any }{ "WithString": { input: "test", want: "test", }, "WithTextMarshaler": { input: TestID("type"), want: "type_", }, "MapWithTextMarshalerKey": { input: map[TestID]TestSimpleStruct{ TestID("type"): {Value: "value"}, }, want: map[string]any{ "type_": map[string]any{"value": "value"}, }, }, "MapWithoutTextMarshalerKey": { input: map[TestStringLike]TestSimpleStruct{ TestStringLike("key"): {Value: "value"}, }, want: map[string]any{ "key": map[string]any{"value": "value"}, }, }, "WithSlice": { input: []TestID{ TestID("nop"), TestID("type_"), }, want: []any{"nop_", "type_"}, }, "WithSimpleStruct": { input: TestSimpleStruct{Value: "test", skipped: "skipped"}, want: map[string]any{ "value": "test", }, }, "WithComplexStruct": { input: &TestComplexStruct{ Skipped: TestEmptyStruct{ Value: "omitted", }, Nested: TestSimpleStruct{ Value: "nested", }, Slice: []TestSimpleStruct{ {Value: "slice"}, }, Map: map[string]TestSimpleStruct{ "Key": {Value: "map"}, }, Pointer: &TestSimpleStruct{ Value: "pointer", }, Remain: map[string]any{ "remain1": 23, "remain2": "value", }, Interface: TestID("value"), TranslatedYaml: TestYamlStruct{ YamlValue: "foo_translated", YamlOmitEmpty: "", YamlInline: TestYamlSimpleStruct{ Inline: "bar_translated", }, }, SquashedYaml: TestYamlStruct{ YamlValue: "foo_squashed", YamlOmitEmpty: "", YamlInline: TestYamlSimpleStruct{ Inline: "bar_squashed", }, }, PointerTranslatedYaml: &TestPtrToYamlStruct{ YamlValue: "foo_translated_ptr", YamlOmitEmpty: "", YamlInline: &TestYamlPtrToSimpleStruct{ InlinePtr: "bar_translated_ptr", }, }, PointerSquashedYaml: &TestPtrToYamlStruct{ YamlValue: "foo_squashed_ptr", YamlOmitEmpty: "", YamlInline: &TestYamlPtrToSimpleStruct{ InlinePtr: "bar_squashed_ptr", }, }, }, want: map[string]any{ "value": "nested", "slice": []any{map[string]any{"value": "slice"}}, "map": map[string]any{ "Key": map[string]any{"value": "map"}, }, "ptr": map[string]any{"value": "pointer"}, "interface": "value_", "yaml_value": "foo_squashed", "yaml_inline": "bar_squashed", "translated": map[string]any{ "yaml_value": "foo_translated", "yaml_inline": "bar_translated", }, "yaml_value_ptr": "foo_squashed_ptr", "yaml_inline_ptr": "bar_squashed_ptr", "translated_ptr": map[string]any{ "yaml_value_ptr": "foo_translated_ptr", "yaml_inline_ptr": "bar_translated_ptr", }, "remain1": 23, "remain2": "value", }, }, } for name, testCase := range testCases { t.Run(name, func(t *testing.T) { got, err := enc.Encode(testCase.input) require.NoError(t, err) require.Equal(t, testCase.want, got) }) } // without the TextMarshalerHookFunc enc.config.EncodeHook = nil testCase := TestID("test") got, err := enc.Encode(testCase) require.NoError(t, err) require.Equal(t, testCase, got) } func TestGetTagInfo(t *testing.T) { testCases := []struct { name string field reflect.StructField wantName string wantOmit bool wantSquash bool }{ { name: "WithoutTags", field: reflect.StructField{ Name: "Test", }, wantName: "test", }, { name: "WithoutMapStructureTag", field: reflect.StructField{ Tag: `yaml:"hello,inline"`, Name: "YAML", }, wantName: "yaml", }, { name: "WithRename", field: reflect.StructField{ Tag: `mapstructure:"hello"`, Name: "Test", }, wantName: "hello", }, { name: "WithOmitEmpty", field: reflect.StructField{ Tag: `mapstructure:"hello,omitempty"`, Name: "Test", }, wantName: "hello", wantOmit: true, }, { name: "WithSquash", field: reflect.StructField{ Tag: `mapstructure:",squash"`, Name: "Test", }, wantSquash: true, }, { name: "WithRemain", field: reflect.StructField{ Tag: `mapstructure:",remain"`, Name: "Test", }, wantSquash: true, }, } for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { got := getTagInfo(tt.field) require.Equal(t, tt.wantName, got.name) require.Equal(t, tt.wantOmit, got.omitEmpty) require.Equal(t, tt.wantSquash, got.squash) }) } } func TestEncodeValueError(t *testing.T) { enc := New(nil) testValue := reflect.ValueOf("") testCases := []struct { encodeFn func(value reflect.Value) (any, error) wantErr error }{ {encodeFn: enc.encodeMap, wantErr: &reflect.ValueError{Method: "encodeMap", Kind: reflect.String}}, {encodeFn: enc.encodeStruct, wantErr: &reflect.ValueError{Method: "encodeStruct", Kind: reflect.String}}, {encodeFn: enc.encodeSlice, wantErr: &reflect.ValueError{Method: "encodeSlice", Kind: reflect.String}}, } for _, tt := range testCases { got, err := tt.encodeFn(testValue) require.Error(t, err) require.Equal(t, tt.wantErr, err) require.Nil(t, got) } } func TestEncodeNonStringEncodedKey(t *testing.T) { enc := New(nil) testCase := []struct { Test map[string]any }{ { Test: map[string]any{ "test": map[TestEmptyStruct]TestSimpleStruct{ {Value: "key"}: {Value: "value"}, }, }, }, } got, err := enc.Encode(testCase) require.Error(t, err) require.ErrorIs(t, err, errNonStringEncodedKey) require.Nil(t, got) } func TestDuplicateKey(t *testing.T) { enc := New(&EncoderConfig{ EncodeHook: TextMarshalerHookFunc(), }) testCase := map[TestID]string{ "test": "value", "test_": "other value", } got, err := enc.Encode(testCase) require.Error(t, err) require.Nil(t, got) } func TestTextMarshalerError(t *testing.T) { enc := New(&EncoderConfig{ EncodeHook: TextMarshalerHookFunc(), }) testCase := map[TestID]string{ "error": "value", } got, err := enc.Encode(testCase) require.Error(t, err) require.Nil(t, got) } func TestEncodeStruct(t *testing.T) { enc := New(&EncoderConfig{ EncodeHook: testHookFunc(), }) testCase := TestSimpleStruct{ Value: "original", skipped: "final", } got, err := enc.Encode(testCase) require.NoError(t, err) require.Equal(t, "final", got) } func TestEncodeStructError(t *testing.T) { enc := New(&EncoderConfig{ EncodeHook: testHookFunc(), }) wantErr := errors.New("test") testCase := map[TestSimpleStruct]string{ {err: wantErr}: "value", } got, err := enc.Encode(testCase) require.Error(t, err) require.ErrorIs(t, err, wantErr) require.Nil(t, got) } func TestEncodeNil(t *testing.T) { enc := New(nil) got, err := enc.Encode(nil) require.NoError(t, err) require.Nil(t, got) } func testHookFunc() mapstructure.DecodeHookFuncValue { return func(from reflect.Value, _ reflect.Value) (any, error) { if from.Kind() != reflect.Struct { return from.Interface(), nil } got, ok := from.Interface().(TestSimpleStruct) if !ok { return from.Interface(), nil } if got.err != nil { return nil, got.err } return got.skipped, nil } }