Skip to content

Commit 6fe6256

Browse files
[configoptional] Allow wrapping scalar values (#15175)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> #### Description Allow `configoptional.Optional` to wrap scalar values using new interfaces designed specifically to handle similar wrappers around scalar values. Follow up to #13524. --------- Co-authored-by: Jade Guiton <jade.guiton@datadoghq.com>
1 parent 31e5152 commit 6fe6256

14 files changed

Lines changed: 1369 additions & 226 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. receiver/otlp)
7+
component: pkg/configoptional
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Add methods allowing scalar unmarshaling
11+
12+
# One or more tracking issues or pull requests related to the change
13+
issues: [15175]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# Optional: The change log or logs in which this entry should be included.
21+
# e.g. '[user]' or '[user, api]'
22+
# Include 'user' if the change is relevant to end users.
23+
# Include 'api' if there is a change to a library API.
24+
# Default: '[user]'
25+
change_logs: [api]

.chloggen/xconfmap-scalars.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. receiver/otlp)
7+
component: pkg/xconfmap
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Add `ScalarMarshaler` and `ScalarUnmarshaler` interfaces to allow custom marshaling and unmarshaling of wrapped scalar values.
11+
12+
# One or more tracking issues or pull requests related to the change
13+
issues: [15175]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# Optional: The change log or logs in which this entry should be included.
21+
# e.g. '[user]' or '[user, api]'
22+
# Include 'user' if the change is relevant to end users.
23+
# Include 'api' if there is a change to a library API.
24+
# Default: '[user]'
25+
change_logs: [api]

config/configoptional/optional.go

Lines changed: 58 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -48,20 +48,6 @@ func deref(t reflect.Type) reflect.Type {
4848
return t
4949
}
5050

51-
// assertStructKind checks if T can be dereferenced into a type with struct kind.
52-
//
53-
// We assert this because our unmarshaling logic currently only supports structs.
54-
// This can be removed if we ever support scalar values.
55-
func assertStructKind[T any]() error {
56-
var instance T
57-
t := deref(reflect.TypeOf(instance))
58-
if t.Kind() != reflect.Struct {
59-
return fmt.Errorf("configoptional: %q does not have a struct kind", t)
60-
}
61-
62-
return nil
63-
}
64-
6551
// assertNoEnabledField checks that a struct type
6652
// does not have a field with a mapstructure tag "enabled".
6753
//
@@ -101,12 +87,9 @@ func Some[T any](value T) Optional[T] {
10187

10288
// Default creates an Optional with a default value for unmarshaling.
10389
//
104-
// It panics if
105-
// - T is not a struct OR
106-
// - T has a field with the mapstructure tag "enabled".
90+
// It panics if T has a field with the mapstructure tag "enabled".
10791
func Default[T any](value T) Optional[T] {
108-
err := errors.Join(assertStructKind[T](), assertNoEnabledField[T]())
109-
if err != nil {
92+
if err := assertNoEnabledField[T](); err != nil {
11093
panic(err)
11194
}
11295
return Optional[T]{value: value, flavor: defaultFlavor}
@@ -149,8 +132,7 @@ func (o *Optional[T]) Get() *T {
149132
// - T is not a struct OR
150133
// - T has a field with the mapstructure tag "enabled".
151134
func (o *Optional[T]) GetOrInsertDefault() *T {
152-
err := errors.Join(assertStructKind[T](), assertNoEnabledField[T]())
153-
if err != nil {
135+
if err := assertNoEnabledField[T](); err != nil {
154136
panic(err)
155137
}
156138

@@ -167,7 +149,10 @@ func (o *Optional[T]) GetOrInsertDefault() *T {
167149
return o.Get()
168150
}
169151

170-
var _ confmap.Unmarshaler = (*Optional[any])(nil)
152+
var (
153+
_ confmap.Unmarshaler = (*Optional[any])(nil)
154+
_ confmap.ScalarUnmarshaler = (*Optional[any])(nil)
155+
)
171156

172157
// Unmarshal the configuration into the Optional value.
173158
//
@@ -183,7 +168,9 @@ var _ confmap.Unmarshaler = (*Optional[any])(nil)
183168
// - if enabled is false: the Optional becomes None regardless of other configuration values.
184169
//
185170
// T must be derefenceable to a type with struct kind and not have an 'enabled' field.
186-
// Scalar values are not supported.
171+
// Scalar values are not supported, and will be handled by [UnmarshalScalar] instead.
172+
// We do not need to check this since the hook for [ScalarUnmarshaler] will be called
173+
// before the hook for [Unmarshaler].
187174
func (o *Optional[T]) Unmarshal(conf *confmap.Conf) error {
188175
if err := assertNoEnabledField[T](); err != nil {
189176
return err
@@ -221,19 +208,47 @@ func (o *Optional[T]) Unmarshal(conf *confmap.Conf) error {
221208
return nil
222209
}
223210

224-
var _ confmap.Marshaler = (*Optional[any])(nil)
211+
// UnmarshalScalar unmarshals a scalar value into the Optional.
212+
//
213+
// A `nil` value will set the Optional to None, disabling it as setting
214+
// `enabled: false` for a struct-type Optional or `null` for a pointer field
215+
// would.
216+
func (o *Optional[T]) UnmarshalScalar(scalarValue confmap.ScalarValue) error {
217+
if scalarValue.GetRaw() == nil {
218+
if deref(reflect.TypeOf(o.value)).Kind() == reflect.Struct {
219+
// Defer to Unmarshal behavior
220+
return confmap.ErrValueNotApplicable
221+
}
222+
// For scalar types, a nil map represents `null` and clears to None.
223+
var zero T
224+
o.value = zero
225+
o.flavor = noneFlavor
226+
227+
return nil
228+
}
229+
230+
if err := scalarValue.Unmarshal(&o.value); err != nil {
231+
return err
232+
}
233+
o.flavor = someFlavor
234+
235+
return nil
236+
}
237+
238+
var (
239+
_ confmap.Marshaler = (*Optional[any])(nil)
240+
_ confmap.ScalarMarshaler = (*Optional[any])(nil)
241+
)
225242

226243
// Marshal the Optional value into the configuration.
227244
// If the Optional is None or Default, it does not marshal anything.
228245
// If the Optional is Some, it marshals the value into the configuration.
229246
//
230247
// T must be derefenceable to a type with struct kind.
231-
// Scalar values are not supported.
248+
// Scalar values are not supported, and will be handled by [MarshalScalar] instead.
249+
// We do not need to check this since the hook for [ScalarMarshaler] will be called
250+
// before the hook for [Marshaler].
232251
func (o Optional[T]) Marshal(conf *confmap.Conf) error {
233-
if err := assertStructKind[T](); err != nil {
234-
return err
235-
}
236-
237252
if o.flavor == noneFlavor || o.flavor == defaultFlavor {
238253
// Optional is None or Default, do not marshal anything.
239254
return conf.Marshal(map[string]any(nil))
@@ -246,6 +261,20 @@ func (o Optional[T]) Marshal(conf *confmap.Conf) error {
246261
return nil
247262
}
248263

264+
func (o Optional[T]) MarshalScalar(scalarValue confmap.ScalarValue) error {
265+
if deref(reflect.TypeOf(o.value)).Kind() == reflect.Struct {
266+
// Defer to Marshal behavior
267+
return confmap.ErrValueNotApplicable
268+
}
269+
270+
if o.flavor == noneFlavor || o.flavor == defaultFlavor {
271+
// An Optional of type None or Default should marshal as nil.
272+
return scalarValue.Marshal(nil)
273+
}
274+
275+
return scalarValue.Marshal(o.value)
276+
}
277+
249278
var _ confmap.Validator = (*Optional[any])(nil)
250279

251280
// Validate implements [confmap.Validator]. This is required because the

0 commit comments

Comments
 (0)