Files
job-search/internal/config/section.go
T

351 lines
10 KiB
Go

package config
import (
"errors"
"fmt"
"reflect"
"strconv"
"strings"
"time"
)
// Configuration sections are plain structs described with struct tags:
//
// cfg:"key" the field's configuration key. Fields without it are
// not part of the configuration.
// default:"value" the field's default, parsed the same way as a string
// passed to SetValue (e.g. "10m", "true", "42").
//
// A tagged field whose type is a struct (other than time.Time) is a nested
// section, addressed as "section.key". An embedded struct has its tagged
// fields promoted into the enclosing section, just as Go promotes fields.
//
// time.Duration fields report reflect.String from TypeOf and accept either a
// time.Duration or a duration string such as "90s" in SetValue.
//
// A section can check values before they are stored by implementing
// validator on its pointer type.
const (
keyTag = "cfg"
defaultTag = "default"
)
var (
errUnknownKey = errors.New("unknown configuration key")
durationType = reflect.TypeOf(time.Duration(0))
timeType = reflect.TypeOf(time.Time{})
)
// Section is the method set of every configuration section, including
// JobSearchConfig itself, implemented on the section's pointer type. Keys are
// dotted paths relative to the section, e.g. "sleeper.interval" in
// JobSearchConfigSources. Durations are given out as strings such as "10m0s",
// matching TypeOf's reflect.String for them. TestSections checks every
// section, found by walking JobSearchConfig, implements it.
type Section interface {
// Map returns the section as a map of keys to values, with nested
// sections as nested maps.
Map() map[string]any
// Flatten returns the section as a flat map of dotted keys, each
// prefixed with the section's own key (its JSCK* constant), so a
// section's keys match how its parent flattens it. JobSearchConfig and
// the embedded JobSearchConfigSource have no prefix.
Flatten() map[string]any
// TypeOf returns the kind of the value at key, which is how it is read
// from Viper, or an error for an unknown key.
TypeOf(key string) (reflect.Kind, error)
// ValueOf returns the value at key, or an error for an unknown key.
ValueOf(key string) (any, error)
// SetValue stores value at key. value may be of the field's type or a
// string to parse (e.g. "10m", "true", "42"). The section's validation,
// if any, runs first; on error the field is unchanged.
SetValue(key string, value any) error
}
// validator is implemented by sections that need to reject some values.
// key is the field's own key within the section; value has already been
// converted to the field's type.
type validator interface {
validate(key string, value any) error
}
// field is a section struct field that is part of the configuration (see
// fieldsOf).
type field struct {
key string
index []int // path for reflect.Value.FieldByIndex
typ reflect.Type
section bool
def string
hasDefault bool
}
// fieldsOf lists the configuration fields of a section type in declaration
// order, with the fields of embedded structs promoted into it.
func fieldsOf(t reflect.Type) []field {
var fields []field
for i := 0; i < t.NumField(); i++ {
sf := t.Field(i)
if sf.Anonymous && sf.Type.Kind() == reflect.Struct {
for _, f := range fieldsOf(sf.Type) {
f.index = append([]int{i}, f.index...)
fields = append(fields, f)
}
continue
}
key, ok := sf.Tag.Lookup(keyTag)
if !ok || key == "" || key == "-" || !sf.IsExported() {
continue
}
def, hasDefault := sf.Tag.Lookup(defaultTag)
fields = append(fields, field{
key: key,
index: []int{i},
typ: sf.Type,
section: sf.Type.Kind() == reflect.Struct && sf.Type != timeType,
def: def,
hasDefault: hasDefault,
})
}
return fields
}
// structValue dereferences v down to the section struct it holds.
func structValue(v any) reflect.Value {
rv := reflect.ValueOf(v)
for rv.Kind() == reflect.Pointer {
rv = rv.Elem()
}
if rv.Kind() != reflect.Struct {
panic(fmt.Sprintf("config: %T is not a configuration section", v))
}
return rv
}
// lookup resolves a dotted key to the leaf field and the section holding it.
func lookup(section reflect.Value, key string) (reflect.Value, field, error) {
head, rest, nested := strings.Cut(key, ".")
for _, f := range fieldsOf(section.Type()) {
if f.key != head {
continue
}
switch {
case f.section && nested:
return lookup(section.FieldByIndex(f.index), rest)
case !f.section && !nested:
return section, f, nil
}
// Key matched but the shape didn't: a section with no sub-key, or a
// value with one.
return reflect.Value{}, field{}, errUnknownKey
}
return reflect.Value{}, field{}, errUnknownKey
}
func joinKey(prefix, key string) string {
if prefix == "" {
return key
}
return prefix + "." + key
}
// keysOf returns the keys directly in a section.
func keysOf(v any) []string {
fields := fieldsOf(structValue(v).Type())
keys := make([]string, 0, len(fields))
for _, f := range fields {
keys = append(keys, f.key)
}
return keys
}
// mapOf converts a section to a map, with nested sections as nested maps.
func mapOf(v any) map[string]any {
return mapOfValue(structValue(v))
}
// mapOfValue is mapOf for a section struct value.
func mapOfValue(section reflect.Value) map[string]any {
m := map[string]any{}
for _, f := range fieldsOf(section.Type()) {
fv := section.FieldByIndex(f.index)
if f.section {
m[f.key] = mapOfValue(fv)
} else {
m[f.key] = exportValue(fv)
}
}
return m
}
// flattenOf converts a section to a flat map of dotted keys, each prefixed
// with prefix (if non-empty).
func flattenOf(prefix string, v any) map[string]any {
m := map[string]any{}
flattenInto(m, prefix, structValue(v))
return m
}
// flattenInto adds section's keys to m, dotted and prefixed with prefix (if
// non-empty), descending into nested sections.
func flattenInto(m map[string]any, prefix string, section reflect.Value) {
for _, f := range fieldsOf(section.Type()) {
fv := section.FieldByIndex(f.index)
key := joinKey(prefix, f.key)
if f.section {
flattenInto(m, key, fv)
} else {
m[key] = exportValue(fv)
}
}
}
// exportValue returns a field's value as Map, Flatten and ValueOf give it
// out: durations become strings such as "1m30s", so values agree with kindOf
// and can be passed back to SetValue; everything else is unchanged.
func exportValue(fv reflect.Value) any {
if fv.Type() == durationType {
return time.Duration(fv.Int()).String()
}
return fv.Interface()
}
// kindOf is the kind reported by TypeOf, i.e. how the value is read from
// Viper. Durations are read as strings and parsed by SetValue.
func kindOf(t reflect.Type) reflect.Kind {
if t == durationType {
return reflect.String
}
return t.Kind()
}
// typeOf returns the kind of the dotted key in section v (see kindOf), or
// errUnknownKey.
func typeOf(v any, key string) (reflect.Kind, error) {
_, f, err := lookup(structValue(v), key)
if err != nil {
return reflect.Invalid, err
}
return kindOf(f.typ), nil
}
// valueOf returns the value of the dotted key in section v, or errUnknownKey.
func valueOf(v any, key string) (any, error) {
section, f, err := lookup(structValue(v), key)
if err != nil {
return nil, err
}
return exportValue(section.FieldByIndex(f.index)), nil
}
// setValue sets a dotted key on the section that ptr points to.
func setValue(ptr any, key string, value any) error {
section, f, err := lookup(structValue(ptr), key)
if err != nil {
return err
}
return setField(section, f, value)
}
// setField converts value to the field's type and, if the section has a
// validator, checks it, before setting the field. On error the field is left
// unchanged.
func setField(section reflect.Value, f field, value any) error {
newValue, err := convert(value, f.typ)
if err != nil {
return err
}
if v, ok := section.Addr().Interface().(validator); ok {
if err := v.validate(f.key, newValue.Interface()); err != nil {
return err
}
}
section.FieldByIndex(f.index).Set(newValue)
return nil
}
// convert turns value into a value of type t. Values already of the right
// type are used as-is; strings are parsed.
func convert(value any, t reflect.Type) (reflect.Value, error) {
if value != nil {
rv := reflect.ValueOf(value)
if rv.Type().AssignableTo(t) {
return rv, nil
}
if s, ok := value.(string); ok {
return parseString(s, t)
}
}
return reflect.Value{}, fmt.Errorf("unsupported type %T", value)
}
// parseString parses s as a value of type t. It supports strings, bools,
// signed and unsigned integers, floats and time.Duration.
func parseString(s string, t reflect.Type) (reflect.Value, error) {
v := reflect.New(t).Elem()
if t == durationType {
d, err := time.ParseDuration(s)
if err != nil {
return reflect.Value{}, fmt.Errorf("unable to convert to duration: %w", err)
}
v.SetInt(int64(d))
return v, nil
}
var err error
switch t.Kind() {
case reflect.String:
v.SetString(s)
case reflect.Bool:
var b bool
b, err = strconv.ParseBool(s)
v.SetBool(b)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
var n int64
n, err = strconv.ParseInt(s, 10, t.Bits())
v.SetInt(n)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
var n uint64
n, err = strconv.ParseUint(s, 10, t.Bits())
v.SetUint(n)
case reflect.Float32, reflect.Float64:
var n float64
n, err = strconv.ParseFloat(s, t.Bits())
v.SetFloat(n)
default:
return reflect.Value{}, fmt.Errorf("unsupported type string for %s", t)
}
if err != nil {
return reflect.Value{}, fmt.Errorf("unable to convert %q to %s: %w", s, t, err)
}
return v, nil
}
// applyDefaults sets every field that has a default tag, recursing into
// nested sections. ptr must point to a section struct.
func applyDefaults(ptr any) error {
return applyDefaultsTo("", structValue(ptr))
}
// applyDefaultsTo sets every field in section that has a default tag,
// recursing into nested sections. prefix is the section's dotted key, used in
// error messages.
func applyDefaultsTo(prefix string, section reflect.Value) error {
for _, f := range fieldsOf(section.Type()) {
key := joinKey(prefix, f.key)
if f.section {
if err := applyDefaultsTo(key, section.FieldByIndex(f.index)); err != nil {
return err
}
continue
}
if !f.hasDefault {
continue
}
if err := setField(section, f, f.def); err != nil {
return fmt.Errorf("invalid default for %s: %w", key, err)
}
}
return nil
}