Skip to content

Commit 433357b

Browse files
committed
Remove interfaces from xconfmap
1 parent cae76d7 commit 433357b

8 files changed

Lines changed: 325 additions & 228 deletions

File tree

config/configoptional/optional.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,8 @@ func (o *Optional[T]) GetOrInsertDefault() *T {
150150
}
151151

152152
var (
153-
_ confmap.Unmarshaler = (*Optional[any])(nil)
154-
_ xconfmap.ScalarUnmarshaler = (*Optional[any])(nil)
153+
_ confmap.Unmarshaler = (*Optional[any])(nil)
154+
_ confmap.ScalarUnmarshaler = (*Optional[any])(nil)
155155
)
156156

157157
// Unmarshal the configuration into the Optional value.
@@ -211,7 +211,7 @@ func (o *Optional[T]) Unmarshal(conf *confmap.Conf) error {
211211
// A `nil` value will set the Optional to None, disabling it as setting
212212
// `enabled: false` for a struct-type Optional or `null` for a pointer field
213213
// would.
214-
func (o *Optional[T]) UnmarshalScalar(scalarValue xconfmap.ScalarValue) error {
214+
func (o *Optional[T]) UnmarshalScalar(scalarValue confmap.ScalarValue) error {
215215
if scalarValue.GetRaw() == nil {
216216
if deref(reflect.TypeOf(o.value)).Kind() == reflect.Struct {
217217
// Defer to Unmarshal behavior
@@ -234,8 +234,8 @@ func (o *Optional[T]) UnmarshalScalar(scalarValue xconfmap.ScalarValue) error {
234234
}
235235

236236
var (
237-
_ confmap.Marshaler = (*Optional[any])(nil)
238-
_ xconfmap.ScalarMarshaler = (*Optional[any])(nil)
237+
_ confmap.Marshaler = (*Optional[any])(nil)
238+
_ confmap.ScalarMarshaler = (*Optional[any])(nil)
239239
)
240240

241241
// Marshal the Optional value into the configuration.
@@ -257,7 +257,7 @@ func (o Optional[T]) Marshal(conf *confmap.Conf) error {
257257
return nil
258258
}
259259

260-
func (o Optional[T]) MarshalScalar(scalarValue xconfmap.ScalarValue) error {
260+
func (o Optional[T]) MarshalScalar(scalarValue confmap.ScalarValue) error {
261261
if deref(reflect.TypeOf(o.value)).Kind() == reflect.Struct {
262262
// Defer to Marshal behavior
263263
return nil

confmap/confmap.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,25 @@ type Unmarshaler = internal.Unmarshaler
4949
// A configuration struct can implement this interface to override the default
5050
// marshaling.
5151
type Marshaler = internal.Marshaler
52+
53+
// ScalarValue provides access to a scalar configuration value and allows
54+
// calling back into the confmap decoding/encoding machinery.
55+
//
56+
// This interface is only provided to methods used for [ScalarUnmarshaler] and
57+
// [ScalarMarshaler] implementations and cannot be implemented by types outside
58+
// the confmap package.
59+
type ScalarValue = internal.ScalarValue
60+
61+
// ScalarUnmarshaler is an interface which may be implemented by wrapper types
62+
// to customize their behavior when the type under the wrapper is a scalar
63+
// value.
64+
//
65+
// This should be used for types like `Wrapper[T]` where T is a scalar type, and
66+
// the wrapper type needs to implement custom logic for unmarshaling from a
67+
// scalar value (e.g. `5` for `Wrapper[int]`) into the wrapper type (e.g.
68+
// `Wrapper[int]{inner: 5}`).
69+
type ScalarUnmarshaler = internal.ScalarUnmarshaler
70+
71+
// ScalarMarshaler is an interface which may be implemented by wrapper types
72+
// to customize their behavior when the type under the wrapper is a scalar value.
73+
type ScalarMarshaler = internal.ScalarMarshaler

confmap/internal/scalar_test.go

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package internal
5+
6+
import (
7+
"bytes"
8+
"errors"
9+
"fmt"
10+
"path/filepath"
11+
"reflect"
12+
"testing"
13+
14+
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
type textMarshalerStruct struct {
19+
id int
20+
data []byte
21+
}
22+
23+
func (tms textMarshalerStruct) MarshalText() ([]byte, error) {
24+
return tms.data, nil
25+
}
26+
27+
func (tms *textMarshalerStruct) UnmarshalText(data []byte) error {
28+
tms.data = data
29+
return nil
30+
}
31+
32+
type nonTextMarshalerStruct struct {
33+
id int
34+
data []byte
35+
}
36+
37+
type textMarshalerAlias string
38+
39+
func (tma textMarshalerAlias) MarshalText() ([]byte, error) {
40+
return bytes.NewBufferString(string(tma)).Bytes(), nil
41+
}
42+
43+
func (tma *textMarshalerAlias) UnmarshalText(data []byte) error {
44+
*tma = textMarshalerAlias(data)
45+
return nil
46+
}
47+
48+
type nonTextMarshalerAlias string
49+
50+
type NonImplWrapperType[T any] struct {
51+
inner T `mapstructure:"-"`
52+
}
53+
54+
var (
55+
_ Unmarshaler = (*wrapperType[any])(nil)
56+
_ ScalarMarshaler = wrapperType[any]{}
57+
_ ScalarUnmarshaler = (*wrapperType[any])(nil)
58+
)
59+
60+
type wrapperType[T any] struct {
61+
inner T `mapstructure:"-"`
62+
}
63+
64+
func (wt *wrapperType[T]) Unmarshal(conf *Conf) error {
65+
if err := conf.Unmarshal(&wt.inner); err != nil {
66+
return err
67+
}
68+
69+
return nil
70+
}
71+
72+
func (wt wrapperType[T]) Marshal(conf *Conf) error {
73+
if err := conf.Marshal(wt.inner); err != nil {
74+
return fmt.Errorf("failed to marshal wrapperType value: %w", err)
75+
}
76+
77+
return nil
78+
}
79+
80+
func (wt wrapperType[T]) MarshalScalar(sv ScalarValue) error {
81+
return sv.Marshal(wt.inner)
82+
}
83+
84+
func (wt *wrapperType[T]) UnmarshalScalar(val ScalarValue) error {
85+
var v T
86+
if err := val.Unmarshal(&v); err != nil {
87+
return fmt.Errorf("could not unmarshal scalar: %w", err)
88+
}
89+
90+
wt.inner = v
91+
return nil
92+
}
93+
94+
type testScalarConf struct {
95+
// Handled by confmap, treated as string
96+
Tma textMarshalerAlias `mapstructure:"text_marshaler_alias"`
97+
Ntma nonTextMarshalerAlias `mapstructure:"non_text_marshaler_alias"`
98+
Nonimplint NonImplWrapperType[int] `mapstructure:"non_impl_int"`
99+
Nonimplstr NonImplWrapperType[string] `mapstructure:"non_impl_str"`
100+
Nonimpltms NonImplWrapperType[textMarshalerStruct] `mapstructure:"non_impl_text_marshaler_struct"`
101+
Nonimplntms NonImplWrapperType[nonTextMarshalerStruct] `mapstructure:"non_impl_non_text_marshaler_struct"`
102+
Implint wrapperType[int] `mapstructure:"impl_int"`
103+
Implstr wrapperType[string] `mapstructure:"impl_str"`
104+
Impltms wrapperType[textMarshalerStruct] `mapstructure:"impl_text_marshaler_struct"`
105+
Implntms wrapperType[nonTextMarshalerStruct] `mapstructure:"impl_non_text_marshaler_struct"`
106+
Recursive wrapperType[wrapperType[textMarshalerStruct]] `mapstructure:"recursive"`
107+
}
108+
109+
func (cfg *testScalarConf) Unmarshal(conf *Conf) error {
110+
if err := conf.Unmarshal(cfg); err != nil {
111+
return err
112+
}
113+
114+
return nil
115+
}
116+
117+
func TestMarshalConfig(t *testing.T) {
118+
cm := NewFromStringMap(newConfFromFile(t, filepath.Join("testdata", "scalar.yaml")))
119+
wantCfg := &testScalarConf{}
120+
require.NoError(t, cm.Unmarshal(wantCfg))
121+
require.NoError(t, cm.Marshal(wantCfg))
122+
123+
conf := New()
124+
cfg := &testScalarConf{
125+
Tma: textMarshalerAlias("test"),
126+
Ntma: nonTextMarshalerAlias("test"),
127+
Nonimplint: NonImplWrapperType[int]{inner: 1},
128+
Nonimplstr: NonImplWrapperType[string]{inner: "test"},
129+
Nonimpltms: NonImplWrapperType[textMarshalerStruct]{inner: textMarshalerStruct{id: 0, data: []byte{47}}},
130+
Nonimplntms: NonImplWrapperType[nonTextMarshalerStruct]{inner: nonTextMarshalerStruct{id: 2, data: []byte{48}}},
131+
Implint: wrapperType[int]{inner: 1},
132+
Implstr: wrapperType[string]{inner: "test"},
133+
Impltms: wrapperType[textMarshalerStruct]{inner: textMarshalerStruct{id: 0, data: []byte{81}}},
134+
Implntms: wrapperType[nonTextMarshalerStruct]{inner: nonTextMarshalerStruct{id: 2, data: []byte{80}}},
135+
Recursive: wrapperType[wrapperType[textMarshalerStruct]]{inner: wrapperType[textMarshalerStruct]{inner: textMarshalerStruct{id: 2, data: []byte{80}}}},
136+
}
137+
138+
require.NoError(t, conf.Marshal(cfg))
139+
require.Equal(t, cm.ToStringMap(), conf.ToStringMap())
140+
}
141+
142+
// failingScalarMarshaler always returns an error from MarshalScalar.
143+
type failingScalarMarshaler struct{}
144+
145+
func (f failingScalarMarshaler) MarshalScalar(_ ScalarValue) error {
146+
return errors.New("marshal always fails")
147+
}
148+
149+
// TestMarshalScalarErrorPropagation verifies that an error returned by
150+
// MarshalScalar surfaces as an error from Marshal.
151+
func TestMarshalScalarErrorPropagation(t *testing.T) {
152+
type cfgWithFailing struct {
153+
Val failingScalarMarshaler `mapstructure:"val"`
154+
}
155+
156+
cfg := cfgWithFailing{Val: failingScalarMarshaler{}}
157+
conf := New()
158+
err := conf.Marshal(&cfg)
159+
require.Error(t, err)
160+
require.ErrorContains(t, err, "marshal always fails")
161+
}
162+
163+
// TestMarshalNonImplementingTypesUnaffected verifies that fields whose types do
164+
// not implement ScalarMarshaler are encoded normally by mapstructure (as empty
165+
// maps for unexported-field structs), while implementing fields produce their
166+
// scalar representation.
167+
func TestMarshalNonImplementingTypesUnaffected(t *testing.T) {
168+
type mixedCfg struct {
169+
Impl wrapperType[int] `mapstructure:"impl"`
170+
NonImpl NonImplWrapperType[int] `mapstructure:"non_impl"`
171+
Plain int `mapstructure:"plain"`
172+
}
173+
174+
cfg := &mixedCfg{
175+
Impl: wrapperType[int]{inner: 42},
176+
NonImpl: NonImplWrapperType[int]{inner: 7},
177+
Plain: 99,
178+
}
179+
conf := New()
180+
require.NoError(t, conf.Marshal(cfg))
181+
182+
m := conf.ToStringMap()
183+
require.Equal(t, 42, m["impl"], "implementing field should be encoded as scalar")
184+
require.Equal(t, 99, m["plain"], "plain field should be encoded as scalar")
185+
// NonImplWrapperType has no exported fields, so mapstructure encodes it as an empty map.
186+
_, ok := m["non_impl"]
187+
require.True(t, ok, "non-implementing field should still appear in output")
188+
}
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+
}

confmap/xconfmap/scalarmarshaler.go

Lines changed: 0 additions & 10 deletions
This file was deleted.

0 commit comments

Comments
 (0)