Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
495f28d
Allow configoptional to wrap scalar values
evan-bradley Jul 29, 2025
76a0b4c
Fixes and address PR feedback
evan-bradley Apr 23, 2026
2a9ae95
Fix merge, other CI fixes
evan-bradley Apr 23, 2026
1b64c73
Revert unintentional change
evan-bradley Apr 23, 2026
b5ec6fd
Revise Marshaler, allow passing options
evan-bradley Apr 23, 2026
96b20e1
Fix lint
evan-bradley Apr 23, 2026
ae8d617
Undo commenting out tests
evan-bradley Apr 23, 2026
875bad9
Merge remote-tracking branch 'upstream/main' into configoptional-scal…
evan-bradley Apr 23, 2026
b5b6f0e
Merge remote-tracking branch 'upstream/main' into configoptional-scal…
evan-bradley May 8, 2026
3c2a5ee
Remove likely unnecessary test
evan-bradley May 8, 2026
7934cd2
Formatting
evan-bradley May 8, 2026
efbafbd
Apply suggestions from code review
evan-bradley May 11, 2026
1da5267
Refactor hook
evan-bradley May 11, 2026
cae76d7
Get rid of scalar (un)marshaling options
evan-bradley May 11, 2026
1faf1b6
Remove interfaces from xconfmap
evan-bradley May 11, 2026
f55ef80
Update confmap/internal/scalar.go
evan-bradley May 12, 2026
7d875a8
Update confmap/internal/scalar.go
evan-bradley May 12, 2026
29a1a2c
Update confmap/internal/decoder.go
evan-bradley May 12, 2026
c821575
PR review
evan-bradley May 12, 2026
0fc42d6
Add scalar handling notes to non-scalar methods
evan-bradley May 12, 2026
193c61a
Remove special casing in multiple locations
evan-bradley May 12, 2026
8468925
Clean up tests
evan-bradley May 12, 2026
5d07f1d
Move textmarshaler hook to before scalarmarshaler hook
evan-bradley May 12, 2026
2048001
Undo formatting change
evan-bradley May 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .chloggen/configoptional-scalars.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: enhancement

# The name of the component, or a single word describing the area of concern, (e.g. receiver/otlp)
component: pkg/configoptional

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Add methods allowing scalar unmarshaling

# One or more tracking issues or pull requests related to the change
issues: [15175]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext:

# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: [api]
25 changes: 25 additions & 0 deletions .chloggen/xconfmap-scalars.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: enhancement

# The name of the component, or a single word describing the area of concern, (e.g. receiver/otlp)
component: pkg/xconfmap

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Add `ScalarMarshaler` and `ScalarUnmarshaler` interfaces to allow custom marshaling and unmarshaling of wrapped scalar values.

# One or more tracking issues or pull requests related to the change
issues: [15175]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext:

# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: [api]
87 changes: 58 additions & 29 deletions config/configoptional/optional.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,6 @@ func deref(t reflect.Type) reflect.Type {
return t
}

// assertStructKind checks if T can be dereferenced into a type with struct kind.
//
// We assert this because our unmarshaling logic currently only supports structs.
// This can be removed if we ever support scalar values.
func assertStructKind[T any]() error {
var instance T
t := deref(reflect.TypeOf(instance))
if t.Kind() != reflect.Struct {
return fmt.Errorf("configoptional: %q does not have a struct kind", t)
}

return nil
}

// assertNoEnabledField checks that a struct type
// does not have a field with a mapstructure tag "enabled".
//
Expand Down Expand Up @@ -101,12 +87,9 @@ func Some[T any](value T) Optional[T] {

// Default creates an Optional with a default value for unmarshaling.
//
// It panics if
// - T is not a struct OR
// - T has a field with the mapstructure tag "enabled".
// It panics if T has a field with the mapstructure tag "enabled".
func Default[T any](value T) Optional[T] {
err := errors.Join(assertStructKind[T](), assertNoEnabledField[T]())
if err != nil {
if err := assertNoEnabledField[T](); err != nil {
panic(err)
}
return Optional[T]{value: value, flavor: defaultFlavor}
Expand Down Expand Up @@ -149,8 +132,7 @@ func (o *Optional[T]) Get() *T {
// - T is not a struct OR
// - T has a field with the mapstructure tag "enabled".
func (o *Optional[T]) GetOrInsertDefault() *T {
err := errors.Join(assertStructKind[T](), assertNoEnabledField[T]())
if err != nil {
if err := assertNoEnabledField[T](); err != nil {
panic(err)
}

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

var _ confmap.Unmarshaler = (*Optional[any])(nil)
var (
_ confmap.Unmarshaler = (*Optional[any])(nil)
_ confmap.ScalarUnmarshaler = (*Optional[any])(nil)
)

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

var _ confmap.Marshaler = (*Optional[any])(nil)
// UnmarshalScalar unmarshals a scalar value into the Optional.
//
// A `nil` value will set the Optional to None, disabling it as setting
// `enabled: false` for a struct-type Optional or `null` for a pointer field
// would.
func (o *Optional[T]) UnmarshalScalar(scalarValue confmap.ScalarValue) error {
if scalarValue.GetRaw() == nil {
if deref(reflect.TypeOf(o.value)).Kind() == reflect.Struct {
// Defer to Unmarshal behavior
return confmap.ErrValueNotApplicable
}
// For scalar types, a nil map represents `null` and clears to None.
var zero T
o.value = zero
o.flavor = noneFlavor

return nil
}

if err := scalarValue.Unmarshal(&o.value); err != nil {
return err
}
o.flavor = someFlavor

return nil
}

var (
_ confmap.Marshaler = (*Optional[any])(nil)
_ confmap.ScalarMarshaler = (*Optional[any])(nil)
)

// Marshal the Optional value into the configuration.
// If the Optional is None or Default, it does not marshal anything.
// If the Optional is Some, it marshals the value into the configuration.
//
// T must be derefenceable to a type with struct kind.
// Scalar values are not supported.
// Scalar values are not supported, and will be handled by [MarshalScalar] instead.
// We do not need to check this since the hook for [ScalarMarshaler] will be called
// before the hook for [Marshaler].
func (o Optional[T]) Marshal(conf *confmap.Conf) error {
if err := assertStructKind[T](); err != nil {
return err
}
Comment thread
jade-guiton-dd marked this conversation as resolved.

if o.flavor == noneFlavor || o.flavor == defaultFlavor {
// Optional is None or Default, do not marshal anything.
return conf.Marshal(map[string]any(nil))
Expand All @@ -246,6 +261,20 @@ func (o Optional[T]) Marshal(conf *confmap.Conf) error {
return nil
}

func (o Optional[T]) MarshalScalar(scalarValue confmap.ScalarValue) error {
if deref(reflect.TypeOf(o.value)).Kind() == reflect.Struct {
// Defer to Marshal behavior
return confmap.ErrValueNotApplicable
}

if o.flavor == noneFlavor || o.flavor == defaultFlavor {
// An Optional of type None or Default should marshal as nil.
return scalarValue.Marshal(nil)
}

return scalarValue.Marshal(o.value)
}

var _ confmap.Validator = (*Optional[any])(nil)

// Validate implements [confmap.Validator]. This is required because the
Expand Down
Loading
Loading