Skip to content

Commit 495f28d

Browse files
committed
Allow configoptional to wrap scalar values
1 parent b1b3b63 commit 495f28d

16 files changed

Lines changed: 1226 additions & 265 deletions
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/config/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: []
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: []

.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/confmap/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: []
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: []

config/configoptional/optional.go

Lines changed: 47 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+
_ xconfmap.ScalarUnmarshaler = (*Optional[any])(nil)
155+
)
171156

172157
// Unmarshal the configuration into the Optional value.
173158
//
@@ -205,7 +190,7 @@ func (o *Optional[T]) Unmarshal(conf *confmap.Conf) error {
205190
}
206191
}
207192

208-
if err := conf.Unmarshal(&o.value, xconfmap.WithForceUnmarshaler()); err != nil {
193+
if err := conf.Unmarshal(&o.value, xconfmap.WithForceUnmarshaler(), xconfmap.WithScalarMarshaler()); err != nil {
209194
return err
210195
}
211196

@@ -221,7 +206,36 @@ func (o *Optional[T]) Unmarshal(conf *confmap.Conf) error {
221206
return nil
222207
}
223208

224-
var _ confmap.Marshaler = (*Optional[any])(nil)
209+
// UnmarshalScalar unmarshals a scalar value into the Optional.
210+
//
211+
// A `nil` value will set the Optional to None, disabling it as setting
212+
// `enabled: false` for a struct-type Optional or `null` for a pointer field
213+
// would.
214+
func (o *Optional[T]) UnmarshalScalar(val any) error {
215+
if val == nil {
216+
var zero T
217+
o.value = zero
218+
o.flavor = noneFlavor
219+
return nil
220+
}
221+
222+
v, ok := val.(T)
223+
if !ok {
224+
return fmt.Errorf("val is %T, not %T", val, v)
225+
}
226+
o.value = v
227+
o.flavor = someFlavor
228+
return nil
229+
}
230+
231+
func (o *Optional[T]) ScalarType() any {
232+
return o.value
233+
}
234+
235+
var (
236+
_ confmap.Marshaler = (*Optional[any])(nil)
237+
_ xconfmap.ScalarMarshaler = (*Optional[any])(nil)
238+
)
225239

226240
// Marshal the Optional value into the configuration.
227241
// If the Optional is None or Default, it does not marshal anything.
@@ -230,22 +244,26 @@ var _ confmap.Marshaler = (*Optional[any])(nil)
230244
// T must be derefenceable to a type with struct kind.
231245
// Scalar values are not supported.
232246
func (o Optional[T]) Marshal(conf *confmap.Conf) error {
233-
if err := assertStructKind[T](); err != nil {
234-
return err
235-
}
236-
237247
if o.flavor == noneFlavor || o.flavor == defaultFlavor {
238248
// Optional is None or Default, do not marshal anything.
239249
return conf.Marshal(map[string]any(nil))
240250
}
241251

242-
if err := conf.Marshal(o.value); err != nil {
252+
if err := conf.Marshal(o.value, xconfmap.WithScalarMarshaler()); err != nil {
243253
return fmt.Errorf("configoptional: failed to marshal Optional value: %w", err)
244254
}
245255

246256
return nil
247257
}
248258

259+
func (o Optional[T]) GetScalarValue() (any, error) {
260+
if o.flavor == noneFlavor || o.flavor == defaultFlavor {
261+
return nil, nil
262+
}
263+
264+
return o.value, nil
265+
}
266+
249267
var _ xconfmap.Validator = (*Optional[any])(nil)
250268

251269
// Validate implements [xconfmap.Validator]. This is required because the

0 commit comments

Comments
 (0)