Implements conf.Load to populate tagged structs from a chain of Sources (env, .env, YAML/JSON/TOML, custom). Supports default values, slice separators, nested structs, pointer fields, and a Validator hook. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
110 lines
2.5 KiB
Go
110 lines
2.5 KiB
Go
// Package conf loads configuration values from one or more Sources into a
|
|
// tagged Go struct.
|
|
//
|
|
// Field tags:
|
|
//
|
|
// env:"KEY" key looked up in each Source (required to bind a field)
|
|
// default:"v" raw value used when no Source returns the key
|
|
// sep:"," separator for slice fields (default ",")
|
|
// env:"-" skip the field
|
|
//
|
|
// Sources are tried in order; the first one returning a value wins.
|
|
// If the destination type implements Validator, Validate is called after the
|
|
// fields are populated.
|
|
package conf
|
|
|
|
import (
|
|
"reflect"
|
|
|
|
"git.juancwu.dev/juancwu/errx"
|
|
)
|
|
|
|
// Validator is implemented by config types that want a post-load hook.
|
|
type Validator interface {
|
|
Validate() error
|
|
}
|
|
|
|
// Load populates dst from sources. dst must be a non-nil pointer to a struct.
|
|
func Load(dst any, sources ...Source) error {
|
|
const op = "conf.Load"
|
|
|
|
if dst == nil {
|
|
return errx.New(op, "dst is nil")
|
|
}
|
|
v := reflect.ValueOf(dst)
|
|
if v.Kind() != reflect.Pointer || v.IsNil() {
|
|
return errx.New(op, "dst must be a non-nil pointer to a struct")
|
|
}
|
|
v = v.Elem()
|
|
if v.Kind() != reflect.Struct {
|
|
return errx.New(op, "dst must point to a struct")
|
|
}
|
|
|
|
if err := walk(v, sources); err != nil {
|
|
return errx.Wrap(op, err)
|
|
}
|
|
|
|
if val, ok := dst.(Validator); ok {
|
|
if err := val.Validate(); err != nil {
|
|
return errx.Wrap(op, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func walk(v reflect.Value, sources []Source) error {
|
|
const op = "conf.walk"
|
|
|
|
t := v.Type()
|
|
for i := 0; i < v.NumField(); i++ {
|
|
sf := t.Field(i)
|
|
if !sf.IsExported() {
|
|
continue
|
|
}
|
|
fv := v.Field(i)
|
|
|
|
// Recurse into nested structs (and pointers to structs) that have no env tag.
|
|
key := sf.Tag.Get("env")
|
|
if key == "" {
|
|
switch {
|
|
case fv.Kind() == reflect.Struct:
|
|
if err := walk(fv, sources); err != nil {
|
|
return err
|
|
}
|
|
case fv.Kind() == reflect.Pointer && fv.Type().Elem().Kind() == reflect.Struct:
|
|
if fv.IsNil() {
|
|
fv.Set(reflect.New(fv.Type().Elem()))
|
|
}
|
|
if err := walk(fv.Elem(), sources); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
if key == "-" {
|
|
continue
|
|
}
|
|
|
|
raw, ok := lookup(sources, key)
|
|
if !ok {
|
|
raw, ok = sf.Tag.Lookup("default")
|
|
if !ok {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if err := assignString(fv, raw, sf.Tag.Get("sep")); err != nil {
|
|
return errx.Wrapf(op, err, "field %s (%s)", sf.Name, key)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func lookup(sources []Source, key string) (string, bool) {
|
|
for _, s := range sources {
|
|
if v, ok := s.Lookup(key); ok {
|
|
return v, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|