Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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: []
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: []
85 changes: 56 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)
_ xconfmap.ScalarUnmarshaler = (*Optional[any])(nil)
)

// Unmarshal the configuration into the Optional value.
//
Expand Down Expand Up @@ -205,7 +190,7 @@ func (o *Optional[T]) Unmarshal(conf *confmap.Conf) error {
}
}

if err := conf.Unmarshal(&o.value, xconfmap.WithForceUnmarshaler()); err != nil {
if err := conf.Unmarshal(&o.value, xconfmap.WithForceUnmarshaler(), xconfmap.WithScalarUnmarshaler()); err != nil {
return err
}

Expand All @@ -221,7 +206,39 @@ 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 xconfmap.ScalarValue) error {
val := scalarValue.GetRaw()
if reflect.TypeOf(val).Kind() == reflect.Map {
if reflect.ValueOf(val).IsNil() {
if deref(reflect.TypeOf(o.value)).Kind() == reflect.Struct {
// Defer to Unmarshal behavior
return o.Unmarshal(confmap.NewFromStringMap(nil))
}
// 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)
_ xconfmap.ScalarMarshaler = (*Optional[any])(nil)
)

// Marshal the Optional value into the configuration.
// If the Optional is None or Default, it does not marshal anything.
Expand All @@ -230,22 +247,32 @@ var _ confmap.Marshaler = (*Optional[any])(nil)
// T must be derefenceable to a type with struct kind.
// Scalar values are not supported.
func (o Optional[T]) Marshal(conf *confmap.Conf) error {
if err := assertStructKind[T](); err != nil {
return err
}

if o.flavor == noneFlavor || o.flavor == defaultFlavor {
// Optional is None or Default, do not marshal anything.
return conf.Marshal(map[string]any(nil))
}

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

return nil
}

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

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 _ xconfmap.Validator = (*Optional[any])(nil)

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