opentelemetry-collector/config/configoptional/optional.go

192 lines
5.7 KiB
Go

// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package configoptional // import "go.opentelemetry.io/collector/config/configoptional"
import (
"errors"
"fmt"
"reflect"
"strings"
"go.opentelemetry.io/collector/confmap"
)
type flavor int
const (
noneFlavor flavor = 0
defaultFlavor flavor = 1
someFlavor flavor = 2
)
// Optional represents a value that may or may not be present.
// It supports two flavors for all types: Some(value) and None.
// It supports a third flavor for struct types: Default(defaultVal).
//
// For struct types, it supports unmarshaling from a configuration source.
// The zero value of Optional is None.
type Optional[T any] struct {
// value is the value of the Optional.
value T
// flavor indicates the flavor of the Optional.
// The zero value of flavor is noneFlavor.
flavor flavor
}
// deref a reflect.Type to its underlying type.
func deref(t reflect.Type) reflect.Type {
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
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".
//
// We assert this because we discussed an alternative design where we have an explicit
// "enabled" field in the struct to indicate if the struct is enabled or not.
// See https://github.com/open-telemetry/opentelemetry-collector/pull/13060.
// This can be removed if we ever support such a design (or if we just want to allow
// the "enabled" field in the struct).
func assertNoEnabledField[T any]() error {
var i T
t := deref(reflect.TypeOf(i))
if t.Kind() != reflect.Struct {
// Not a struct, no need to check for "enabled" field.
return nil
}
// Check if the struct has a field with the name "enabled".
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
mapstructureTags := strings.SplitN(field.Tag.Get("mapstructure"), ",", 2)
if len(mapstructureTags) > 0 && mapstructureTags[0] == "enabled" {
return errors.New("configoptional: underlying type cannot have a field with mapstructure tag 'enabled'")
}
}
return nil
}
// Some creates an Optional with a value and no factory.
//
// It panics if T has a field with the mapstructure tag "enabled".
func Some[T any](value T) Optional[T] {
if err := assertNoEnabledField[T](); err != nil {
panic(err)
}
return Optional[T]{value: value, flavor: someFlavor}
}
// 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".
func Default[T any](value T) Optional[T] {
err := errors.Join(assertStructKind[T](), assertNoEnabledField[T]())
if err != nil {
panic(err)
}
return Optional[T]{value: value, flavor: defaultFlavor}
}
// None has no value. It has the same behavior as a nil pointer when unmarshaling.
//
// The zero value of Optional[T] is None[T]. Prefer using this constructor
// for validation.
//
// It panics if T has a field with the mapstructure tag "enabled".
func None[T any]() Optional[T] {
if err := assertNoEnabledField[T](); err != nil {
panic(err)
}
return Optional[T]{}
}
// HasValue checks if the Optional has a value.
func (o Optional[T]) HasValue() bool {
return o.flavor == someFlavor
}
// Get returns the value of the Optional.
// If the value is not present, it returns nil.
func (o *Optional[T]) Get() *T {
if !o.HasValue() {
return nil
}
return &o.value
}
var _ confmap.Unmarshaler = (*Optional[any])(nil)
// Unmarshal the configuration into the Optional value.
//
// The behavior of this method depends on the state of the Optional:
// - None[T]: does nothing if the configuration is nil, otherwise it unmarshals into the zero value of T.
// - Some[T](val): equivalent to unmarshaling into a field of type T with value val.
// - Default[T](val), equivalent to unmarshaling into a field of type T with base value val,
// using val without overrides from the configuration if the configuration is nil.
//
// T must be derefenceable to a type with struct kind and not have an 'enabled' field.
// Scalar values are not supported.
func (o *Optional[T]) Unmarshal(conf *confmap.Conf) error {
if err := assertNoEnabledField[T](); err != nil {
return err
}
if o.flavor == noneFlavor && conf.ToStringMap() == nil {
// If the Optional is None and the configuration is nil, we do nothing.
// This replicates the behavior of unmarshaling into a field with a nil pointer.
return nil
}
if err := conf.Unmarshal(&o.value); err != nil {
return err
}
o.flavor = someFlavor
return nil
}
var _ confmap.Marshaler = (*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.
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 {
return fmt.Errorf("configoptional: failed to marshal Optional value: %w", err)
}
return nil
}