opentelemetry-collector/component/componenttest/configtest.go

136 lines
4.0 KiB
Go

// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package componenttest // import "go.opentelemetry.io/collector/component/componenttest"
import (
"fmt"
"reflect"
"regexp"
"strings"
"go.uber.org/multierr"
)
// The regular expression for valid config field tag.
var configFieldTagRegExp = regexp.MustCompile("^[a-z0-9][a-z0-9_]*$")
// CheckConfigStruct enforces that given configuration object is following the patterns
// used by the collector. This ensures consistency between different implementations
// of components and extensions. It is recommended for implementers of components
// to call this function on their tests passing the default configuration of the
// component factory.
func CheckConfigStruct(config any) error {
t := reflect.TypeOf(config)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return fmt.Errorf("config must be a struct or a pointer to one, the passed object is a %s", t.Kind())
}
return validateConfigDataType(t)
}
// validateConfigDataType performs a descending validation of the given type.
// If the type is a struct it goes to each of its fields to check for the proper
// tags.
func validateConfigDataType(t reflect.Type) error {
var errs error
switch t.Kind() {
case reflect.Ptr:
errs = multierr.Append(errs, validateConfigDataType(t.Elem()))
case reflect.Struct:
// Reflect on the pointed data and check each of its fields.
nf := t.NumField()
for i := 0; i < nf; i++ {
f := t.Field(i)
errs = multierr.Append(errs, checkStructFieldTags(f))
}
default:
// The config object can carry other types but they are not used when
// reading the configuration via koanf so ignore them. Basically ignore:
// reflect.Uintptr, reflect.Chan, reflect.Func, reflect.Interface, and
// reflect.UnsafePointer.
}
if errs != nil {
return fmt.Errorf("type %q from package %q has invalid config settings: %w", t.Name(), t.PkgPath(), errs)
}
return nil
}
// checkStructFieldTags inspects the tags of a struct field.
func checkStructFieldTags(f reflect.StructField) error {
tagValue, ok := f.Tag.Lookup("mapstructure")
if !ok {
// Ignore special types.
switch f.Type.Kind() {
case reflect.Interface, reflect.Chan, reflect.Func, reflect.Uintptr, reflect.UnsafePointer:
// Allow the config to carry the types above, but since they are not read
// when loading configuration, just ignore them.
return nil
}
// Public fields of other types should be tagged.
chars := []byte(f.Name)
if len(chars) > 0 && chars[0] >= 'A' && chars[0] <= 'Z' {
return fmt.Errorf("mapstructure tag not present on field %q", f.Name)
}
// Not public field, no need to have a tag.
return nil
}
if tagValue == "" {
return fmt.Errorf("mapstructure tag on field %q is empty", f.Name)
}
tagParts := strings.Split(tagValue, ",")
if tagParts[0] != "" {
if tagParts[0] == "-" {
// Nothing to do, as mapstructure decode skips this field.
return nil
}
}
for _, tag := range tagParts[1:] {
switch tag {
case "squash":
if (f.Type.Kind() != reflect.Struct) && (f.Type.Kind() != reflect.Ptr || f.Type.Elem().Kind() != reflect.Struct) {
return fmt.Errorf(
"attempt to squash non-struct type on field %q", f.Name)
}
case "remain":
if f.Type.Kind() != reflect.Map && f.Type.Kind() != reflect.Interface {
return fmt.Errorf(`attempt to use "remain" on non-map or interface type field %q`, f.Name)
}
}
}
switch f.Type.Kind() {
case reflect.Struct:
// It is another struct, continue down-level.
return validateConfigDataType(f.Type)
case reflect.Map, reflect.Slice, reflect.Array:
// The element of map, array, or slice can be itself a configuration object.
return validateConfigDataType(f.Type.Elem())
default:
fieldTag := tagParts[0]
if fieldTag != "" && !configFieldTagRegExp.MatchString(fieldTag) {
return fmt.Errorf(
"field %q has config tag %q which doesn't satisfy %q",
f.Name,
fieldTag,
configFieldTagRegExp.String())
}
}
return nil
}