Add reflective struct-tag config loader
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>
This commit is contained in:
parent
3c806e6803
commit
c4ebd80669
15 changed files with 941 additions and 0 deletions
110
conf.go
Normal file
110
conf.go
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
// 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue