/* Copyright © 2026 Craig Williams This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . */ 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{}) ) // 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 } 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)) } 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] = fv.Interface() } } 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 } 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] = 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() } 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 } func valueOf(v any, key string) (any, error) { section, f, err := lookup(structValue(v), key) if err != nil { return nil, err } return section.FieldByIndex(f.index).Interface(), 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) } 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) } 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)) } 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 }