11// Copyright The OpenTelemetry Authors
22// SPDX-License-Identifier: Apache-2.0
33
4- package xconfmap
4+ package internal
55
66import (
77 "bytes"
88 "errors"
99 "fmt"
1010 "path/filepath"
11+ "reflect"
1112 "testing"
1213
14+ "github.com/stretchr/testify/assert"
1315 "github.com/stretchr/testify/require"
14-
15- "go.opentelemetry.io/collector/confmap"
16- "go.opentelemetry.io/collector/confmap/confmaptest"
1716)
1817
1918type textMarshalerStruct struct {
@@ -53,24 +52,24 @@ type NonImplWrapperType[T any] struct {
5352}
5453
5554var (
56- _ confmap. Unmarshaler = (* wrapperType [any ])(nil )
57- _ ScalarMarshaler = wrapperType [any ]{}
58- _ ScalarUnmarshaler = (* wrapperType [any ])(nil )
55+ _ Unmarshaler = (* wrapperType [any ])(nil )
56+ _ ScalarMarshaler = wrapperType [any ]{}
57+ _ ScalarUnmarshaler = (* wrapperType [any ])(nil )
5958)
6059
6160type wrapperType [T any ] struct {
6261 inner T `mapstructure:"-"`
6362}
6463
65- func (wt * wrapperType [T ]) Unmarshal (conf * confmap. Conf ) error {
64+ func (wt * wrapperType [T ]) Unmarshal (conf * Conf ) error {
6665 if err := conf .Unmarshal (& wt .inner ); err != nil {
6766 return err
6867 }
6968
7069 return nil
7170}
7271
73- func (wt wrapperType [T ]) Marshal (conf * confmap. Conf ) error {
72+ func (wt wrapperType [T ]) Marshal (conf * Conf ) error {
7473 if err := conf .Marshal (wt .inner ); err != nil {
7574 return fmt .Errorf ("failed to marshal wrapperType value: %w" , err )
7675 }
@@ -92,7 +91,7 @@ func (wt *wrapperType[T]) UnmarshalScalar(val ScalarValue) error {
9291 return nil
9392}
9493
95- type testConfig struct {
94+ type testScalarConf struct {
9695 // Handled by confmap, treated as string
9796 Tma textMarshalerAlias `mapstructure:"text_marshaler_alias"`
9897 Ntma nonTextMarshalerAlias `mapstructure:"non_text_marshaler_alias"`
@@ -107,7 +106,7 @@ type testConfig struct {
107106 Recursive wrapperType [wrapperType [textMarshalerStruct ]] `mapstructure:"recursive"`
108107}
109108
110- func (cfg * testConfig ) Unmarshal (conf * confmap. Conf ) error {
109+ func (cfg * testScalarConf ) Unmarshal (conf * Conf ) error {
111110 if err := conf .Unmarshal (cfg ); err != nil {
112111 return err
113112 }
@@ -116,14 +115,13 @@ func (cfg *testConfig) Unmarshal(conf *confmap.Conf) error {
116115}
117116
118117func TestMarshalConfig (t * testing.T ) {
119- cm , err := confmaptest .LoadConf (filepath .Join ("testdata" , "config.yaml" ))
120- require .NoError (t , err )
121- wantCfg := & testConfig {}
118+ cm := NewFromStringMap (newConfFromFile (t , filepath .Join ("testdata" , "scalar.yaml" )))
119+ wantCfg := & testScalarConf {}
122120 require .NoError (t , cm .Unmarshal (wantCfg ))
123121 require .NoError (t , cm .Marshal (wantCfg ))
124122
125- conf := confmap . New ()
126- cfg := & testConfig {
123+ conf := New ()
124+ cfg := & testScalarConf {
127125 Tma : textMarshalerAlias ("test" ),
128126 Ntma : nonTextMarshalerAlias ("test" ),
129127 Nonimplint : NonImplWrapperType [int ]{inner : 1 },
@@ -149,14 +147,14 @@ func (f failingScalarMarshaler) MarshalScalar(_ ScalarValue) error {
149147}
150148
151149// TestMarshalScalarErrorPropagation verifies that an error returned by
152- // MarshalScalar surfaces as an error from confmap. Marshal.
150+ // MarshalScalar surfaces as an error from Marshal.
153151func TestMarshalScalarErrorPropagation (t * testing.T ) {
154152 type cfgWithFailing struct {
155153 Val failingScalarMarshaler `mapstructure:"val"`
156154 }
157155
158156 cfg := cfgWithFailing {Val : failingScalarMarshaler {}}
159- conf := confmap . New ()
157+ conf := New ()
160158 err := conf .Marshal (& cfg )
161159 require .Error (t , err )
162160 require .ErrorContains (t , err , "marshal always fails" )
@@ -178,7 +176,7 @@ func TestMarshalNonImplementingTypesUnaffected(t *testing.T) {
178176 NonImpl : NonImplWrapperType [int ]{inner : 7 },
179177 Plain : 99 ,
180178 }
181- conf := confmap . New ()
179+ conf := New ()
182180 require .NoError (t , conf .Marshal (cfg ))
183181
184182 m := conf .ToStringMap ()
@@ -188,3 +186,110 @@ func TestMarshalNonImplementingTypesUnaffected(t *testing.T) {
188186 _ , ok := m ["non_impl" ]
189187 require .True (t , ok , "non-implementing field should still appear in output" )
190188}
189+
190+ type nullableWrapperType [T any ] struct {
191+ inner T
192+ wasNil bool
193+ }
194+
195+ func (n * nullableWrapperType [T ]) UnmarshalScalar (val ScalarValue ) error {
196+ raw := val .GetRaw ()
197+ if raw == nil || (reflect .ValueOf (raw ).Kind () == reflect .Map && reflect .ValueOf (raw ).IsNil ()) {
198+ n .wasNil = true
199+ return nil
200+ }
201+ var v T
202+ if err := val .Unmarshal (& v ); err != nil {
203+ return fmt .Errorf ("nullableWrapperType: %w" , err )
204+ }
205+ n .inner = v
206+ return nil
207+ }
208+
209+ type failingScalarUnmarshaler struct {}
210+
211+ func (f * failingScalarUnmarshaler ) UnmarshalScalar (_ ScalarValue ) error {
212+ return errors .New ("always fails" )
213+ }
214+
215+ func TestUnmarshalConfig (t * testing.T ) {
216+ wantCfg := & testScalarConf {
217+ Tma : textMarshalerAlias ("test" ),
218+ Ntma : nonTextMarshalerAlias ("test" ),
219+ Implint : wrapperType [int ]{inner : 1 },
220+ Implstr : wrapperType [string ]{inner : "test" },
221+ Impltms : wrapperType [textMarshalerStruct ]{inner : textMarshalerStruct {id : 0 , data : []byte {81 }}},
222+ Recursive : wrapperType [wrapperType [textMarshalerStruct ]]{inner : wrapperType [textMarshalerStruct ]{inner : textMarshalerStruct {id : 0 , data : []byte {80 }}}},
223+ }
224+
225+ cm := NewFromStringMap (newConfFromFile (t , filepath .Join ("testdata" , "scalar.yaml" )))
226+ cfg := & testScalarConf {}
227+ require .NoError (t , cm .Unmarshal (cfg ))
228+
229+ require .Equal (t , wantCfg , cfg )
230+ }
231+
232+ // TestUnmarshalScalarNullInput verifies that the hook calls UnmarshalScalar(nil)
233+ // when the source value is a nil map, which is how mapstructure represents a
234+ // YAML null for a map-typed value.
235+ func TestUnmarshalScalarNullInput (t * testing.T ) {
236+ type cfgWithNullable struct {
237+ Val nullableWrapperType [int ] `mapstructure:"val"`
238+ }
239+
240+ // A nil map value triggers the `from.Kind() == reflect.Map && from.IsNil()` branch.
241+ cm := NewFromStringMap (map [string ]any {"val" : map [string ]any (nil )})
242+ var cfg cfgWithNullable
243+ require .NoError (t , cm .Unmarshal (& cfg ))
244+ assert .True (t , cfg .Val .wasNil , "expected UnmarshalScalar to be called with nil" )
245+ assert .Equal (t , 0 , cfg .Val .inner , "inner value should remain zero after nil" )
246+ }
247+
248+ // TestUnmarshalScalarDecodeError verifies that errors from internal.Decode are
249+ // propagated when the source value cannot be decoded into ScalarType().
250+ func TestUnmarshalScalarDecodeError (t * testing.T ) {
251+ type cfgWithInt struct {
252+ Val wrapperType [int ] `mapstructure:"val"`
253+ }
254+
255+ // A slice cannot be decoded into an int; this exercises the internal.Decode error path.
256+ cm := NewFromStringMap (map [string ]any {"val" : []string {"a" , "b" }})
257+ cfg := cfgWithInt {}
258+ err := cm .Unmarshal (& cfg )
259+ require .Error (t , err )
260+ }
261+
262+ // TestUnmarshalScalarErrorPropagation verifies that an error returned by
263+ // UnmarshalScalar surfaces as an error from Unmarshal.
264+ func TestUnmarshalScalarErrorPropagation (t * testing.T ) {
265+ type cfgWithFailing struct {
266+ Val failingScalarUnmarshaler `mapstructure:"val"`
267+ }
268+
269+ cm := NewFromStringMap (map [string ]any {"val" : 42 })
270+ var cfg cfgWithFailing
271+ err := cm .Unmarshal (& cfg )
272+ require .Error (t , err )
273+ require .ErrorContains (t , err , "always fails" )
274+ }
275+
276+ // TestNonImplementingTypesUnaffected verifies that fields whose types do not
277+ // implement ScalarUnmarshaler are decoded normally by mapstructure, even when
278+ // implementing fields are present in the same struct.
279+ func TestNonImplementingTypesUnaffected (t * testing.T ) {
280+ type mixedCfg struct {
281+ Impl wrapperType [int ] `mapstructure:"impl"`
282+ NonImpl NonImplWrapperType [int ] `mapstructure:"non_impl"`
283+ Plain int `mapstructure:"plain"`
284+ }
285+
286+ cm := NewFromStringMap (map [string ]any {
287+ "impl" : 10 ,
288+ "plain" : 99 ,
289+ })
290+ var cfg mixedCfg
291+ require .NoError (t , cm .Unmarshal (& cfg ))
292+ assert .Equal (t , 10 , cfg .Impl .inner , "implementing field should be decoded via UnmarshalScalar" )
293+ assert .Equal (t , 99 , cfg .Plain , "plain field should be decoded normally" )
294+ assert .Equal (t , 0 , cfg .NonImpl .inner , "non-implementing field should remain zero" )
295+ }
0 commit comments