Skip to content

Commit f58565d

Browse files
committed
Remove interfaces from xconfmap
1 parent cae76d7 commit f58565d

7 files changed

Lines changed: 152 additions & 181 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
Lines changed: 124 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
// Copyright The OpenTelemetry Authors
22
// SPDX-License-Identifier: Apache-2.0
33

4-
package xconfmap
4+
package internal
55

66
import (
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

1918
type textMarshalerStruct struct {
@@ -53,24 +52,24 @@ type NonImplWrapperType[T any] struct {
5352
}
5453

5554
var (
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

6160
type 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

118117
func 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.
153151
func 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+
}

confmap/xconfmap/scalarmarshaler.go

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

confmap/xconfmap/scalarunmarshaler.go

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

0 commit comments

Comments
 (0)