// Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 package configoptional import ( "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/confmap" ) type Config[T any] struct { Sub1 Optional[T] `mapstructure:"sub"` } type Sub struct { Foo string `mapstructure:"foo"` } type WithEnabled struct { Enabled bool `mapstructure:"enabled"` } type WithEnabledOmitEmpty struct { Enabled bool `mapstructure:"enabled,omitempty"` } type OmitEmpty struct { Foo int `mapstructure:",omitempty"` } type NoMapstructure struct { Foo string } var subDefault = Sub{ Foo: "foobar", } func ptr[T any](v T) *T { return &v } func TestDefaultPanics(t *testing.T) { assert.Panics(t, func() { _ = Default(1) }) assert.Panics(t, func() { _ = Default(ptr(1)) }) assert.Panics(t, func() { _ = Default(WithEnabled{}) }) assert.Panics(t, func() { _ = Default(WithEnabledOmitEmpty{}) }) assert.Panics(t, func() { _ = Some(WithEnabled{}) }) assert.Panics(t, func() { _ = None[WithEnabled]() }) assert.NotPanics(t, func() { _ = Default(NoMapstructure{}) }) assert.NotPanics(t, func() { _ = Default(OmitEmpty{}) }) assert.NotPanics(t, func() { _ = Default(subDefault) }) assert.NotPanics(t, func() { _ = Default(ptr(subDefault)) }) } func TestEqualityDefault(t *testing.T) { defaultOne := Default(subDefault) defaultTwo := Default(subDefault) assert.Equal(t, defaultOne, defaultTwo) } func TestNoneZeroVal(t *testing.T) { var none Optional[Sub] require.False(t, none.HasValue()) require.Nil(t, none.Get()) } func TestNone(t *testing.T) { none := None[Sub]() require.False(t, none.HasValue()) require.Nil(t, none.Get()) } func TestSome(t *testing.T) { some := Some(Sub{ Foo: "foobar", }) require.True(t, some.HasValue()) assert.Equal(t, "foobar", some.Get().Foo) } func TestDefault(t *testing.T) { defaultSub := Default(&subDefault) require.False(t, defaultSub.HasValue()) require.Nil(t, defaultSub.Get()) } func TestUnmarshalOptional(t *testing.T) { tests := []struct { name string config map[string]any defaultCfg Config[Sub] expectedSub bool expectedFoo string }{ { name: "none_no_config", defaultCfg: Config[Sub]{ Sub1: None[Sub](), }, expectedSub: false, }, { name: "none_with_config", config: map[string]any{ "sub": map[string]any{ "foo": "bar", }, }, defaultCfg: Config[Sub]{ Sub1: None[Sub](), }, expectedSub: true, expectedFoo: "bar", // input overrides default }, { // nil is treated as an empty map because of the hooks we use. name: "none_with_config_no_foo", config: map[string]any{ "sub": nil, }, defaultCfg: Config[Sub]{ Sub1: None[Sub](), }, expectedSub: false, }, { name: "none_with_config_no_foo_empty_map", config: map[string]any{ "sub": map[string]any{}, }, defaultCfg: Config[Sub]{ Sub1: None[Sub](), }, expectedSub: true, expectedFoo: "", }, { name: "default_no_config", defaultCfg: Config[Sub]{ Sub1: Default(subDefault), }, expectedSub: false, }, { name: "default_with_config", config: map[string]any{ "sub": map[string]any{ "foo": "bar", }, }, defaultCfg: Config[Sub]{ Sub1: Default(subDefault), }, expectedSub: true, expectedFoo: "bar", // input overrides default }, { name: "default_with_config_no_foo", config: map[string]any{ "sub": nil, }, defaultCfg: Config[Sub]{ Sub1: Default(subDefault), }, expectedSub: true, expectedFoo: "foobar", // default applies }, { name: "default_with_config_no_foo_empty_map", config: map[string]any{ "sub": map[string]any{}, }, defaultCfg: Config[Sub]{ Sub1: Default(subDefault), }, expectedSub: true, expectedFoo: "foobar", // default applies }, { name: "some_no_config", defaultCfg: Config[Sub]{ Sub1: Some(Sub{ Foo: "foobar", }), }, expectedSub: true, expectedFoo: "foobar", // value is not modified }, { name: "some_with_config", config: map[string]any{ "sub": map[string]any{ "foo": "bar", }, }, defaultCfg: Config[Sub]{ Sub1: Some(Sub{ Foo: "foobar", }), }, expectedSub: true, expectedFoo: "bar", // input overrides previous value }, { name: "some_with_config_no_foo", config: map[string]any{ "sub": nil, }, defaultCfg: Config[Sub]{ Sub1: Some(Sub{ Foo: "foobar", }), }, expectedSub: true, expectedFoo: "foobar", // default applies }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { cfg := test.defaultCfg conf := confmap.NewFromStringMap(test.config) require.NoError(t, conf.Unmarshal(&cfg)) require.Equal(t, test.expectedSub, cfg.Sub1.HasValue()) if test.expectedSub { require.Equal(t, test.expectedFoo, cfg.Sub1.Get().Foo) } }) } } func TestUnmarshalErrorEnabledField(t *testing.T) { cm := confmap.NewFromStringMap(map[string]any{ "enabled": true, }) // Use zero value to avoid panic on constructor. var none Optional[WithEnabled] require.Error(t, cm.Unmarshal(&none)) } func TestUnmarshalConfigPointer(t *testing.T) { cm := confmap.NewFromStringMap(map[string]any{ "sub": map[string]any{ "foo": "bar", }, }) var cfg Config[*Sub] err := cm.Unmarshal(&cfg) require.NoError(t, err) assert.True(t, cfg.Sub1.HasValue()) assert.Equal(t, "bar", (*cfg.Sub1.Get()).Foo) } func TestUnmarshalErr(t *testing.T) { cm := confmap.NewFromStringMap(map[string]any{ "field": "value", }) cfg := Config[Sub]{ Sub1: Default(subDefault), } assert.False(t, cfg.Sub1.HasValue()) err := cm.Unmarshal(&cfg) require.Error(t, err) require.ErrorContains(t, err, "has invalid keys: field") assert.False(t, cfg.Sub1.HasValue()) } type MyIntConfig struct { Val int `mapstructure:"my_int"` } type MyConfig struct { Optional[MyIntConfig] `mapstructure:",squash"` } var myIntDefault = MyIntConfig{ Val: 1, } func TestSquashedOptional(t *testing.T) { cm := confmap.NewFromStringMap(map[string]any{ "my_int": 42, }) cfg := MyConfig{ Default(myIntDefault), } err := cm.Unmarshal(&cfg) require.NoError(t, err) assert.True(t, cfg.HasValue()) assert.Equal(t, 42, cfg.Get().Val) } func confFromYAML(t *testing.T, yaml string) *confmap.Conf { t.Helper() cm, err := confmap.NewRetrievedFromYAML([]byte(yaml)) require.NoError(t, err) conf, err := cm.AsConf() require.NoError(t, err) return conf } func TestComparePointerUnmarshal(t *testing.T) { tests := []struct { yaml string }{ {yaml: ""}, {yaml: "sub: "}, {yaml: "sub: null"}, {yaml: "sub: {}"}, {yaml: "sub: {foo: bar}"}, } for _, test := range tests { t.Run(test.yaml, func(t *testing.T) { var optCfg Config[Sub] conf := confFromYAML(t, test.yaml) optErr := conf.Unmarshal(&optCfg) require.NoError(t, optErr) var ptrCfg struct { Sub1 *Sub `mapstructure:"sub"` } ptrErr := conf.Unmarshal(&ptrCfg) require.NoError(t, ptrErr) assert.Equal(t, optCfg.Sub1.Get(), ptrCfg.Sub1) }) } } func TestOptionalMarshal(t *testing.T) { tests := []struct { name string value Config[Sub] expected map[string]any }{ { name: "none (zero value)", value: Config[Sub]{}, expected: map[string]any{"sub": nil}, }, { name: "none", value: Config[Sub]{Sub1: None[Sub]()}, expected: map[string]any{"sub": nil}, }, { name: "default", value: Config[Sub]{Sub1: Default(subDefault)}, expected: map[string]any{"sub": nil}, }, { name: "some", value: Config[Sub]{Sub1: Some(Sub{ Foo: "bar", })}, expected: map[string]any{ "sub": map[string]any{ "foo": "bar", }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { conf := confmap.New() require.NoError(t, conf.Marshal(test.value)) assert.Equal(t, test.expected, conf.ToStringMap()) }) } } func TestComparePointerMarshal(t *testing.T) { type Wrap[T any] struct { // Note: passes without requiring "squash". Sub1 T `mapstructure:"sub"` } type WrapOmitEmpty[T any] struct { // Note: passes without requiring "squash", except with Default-flavored Optional values. Sub1 T `mapstructure:"sub,omitempty"` } tests := []struct { pointer *Sub optional Optional[Sub] skipOmitEmpty bool }{ {pointer: nil, optional: None[Sub]()}, {pointer: nil, optional: Default(subDefault), skipOmitEmpty: true}, // does not work with omitempty {pointer: &Sub{Foo: "bar"}, optional: Some(Sub{Foo: "bar"})}, } for _, test := range tests { t.Run(fmt.Sprintf("%v vs %v", test.pointer, test.optional), func(t *testing.T) { wrapPointer := Wrap[*Sub]{Sub1: test.pointer} confPointer := confmap.NewFromStringMap(nil) require.NoError(t, confPointer.Marshal(wrapPointer)) wrapOptional := Wrap[Optional[Sub]]{Sub1: test.optional} confOptional := confmap.NewFromStringMap(nil) require.NoError(t, confOptional.Marshal(wrapOptional)) assert.Equal(t, confPointer.ToStringMap(), confOptional.ToStringMap()) }) if test.skipOmitEmpty { continue } t.Run(fmt.Sprintf("%v vs %v (omitempty)", test.pointer, test.optional), func(t *testing.T) { wrapPointer := WrapOmitEmpty[*Sub]{Sub1: test.pointer} confPointer := confmap.NewFromStringMap(nil) require.NoError(t, confPointer.Marshal(wrapPointer)) wrapOptional := WrapOmitEmpty[Optional[Sub]]{Sub1: test.optional} confOptional := confmap.NewFromStringMap(nil) require.NoError(t, confOptional.Marshal(wrapOptional)) assert.Equal(t, confPointer.ToStringMap(), confOptional.ToStringMap()) }) } }