conf/parse.go
juancwu c4ebd80669 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>
2026-04-27 20:35:51 +00:00

92 lines
2.2 KiB
Go

package conf
import (
"reflect"
"strconv"
"strings"
"time"
"git.juancwu.dev/juancwu/errx"
)
var (
durationType = reflect.TypeOf(time.Duration(0))
timeType = reflect.TypeOf(time.Time{})
byteSliceTyp = reflect.TypeOf([]byte(nil))
)
// assignString parses raw into the value pointed to by v. v must be settable.
func assignString(v reflect.Value, raw, sep string) error {
const op = "conf.assignString"
if v.Kind() == reflect.Pointer {
if v.IsNil() {
v.Set(reflect.New(v.Type().Elem()))
}
return assignString(v.Elem(), raw, sep)
}
switch v.Type() {
case durationType:
d, err := time.ParseDuration(raw)
if err != nil {
return errx.Wrapf(op, err, "parse duration %q", raw)
}
v.SetInt(int64(d))
return nil
case timeType:
t, err := time.Parse(time.RFC3339, raw)
if err != nil {
return errx.Wrapf(op, err, "parse time %q", raw)
}
v.Set(reflect.ValueOf(t))
return nil
case byteSliceTyp:
v.SetBytes([]byte(raw))
return nil
}
switch v.Kind() {
case reflect.String:
v.SetString(raw)
case reflect.Bool:
b, err := strconv.ParseBool(raw)
if err != nil {
return errx.Wrapf(op, err, "parse bool %q", raw)
}
v.SetBool(b)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
n, err := strconv.ParseInt(raw, 10, v.Type().Bits())
if err != nil {
return errx.Wrapf(op, err, "parse int %q", raw)
}
v.SetInt(n)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
n, err := strconv.ParseUint(raw, 10, v.Type().Bits())
if err != nil {
return errx.Wrapf(op, err, "parse uint %q", raw)
}
v.SetUint(n)
case reflect.Float32, reflect.Float64:
f, err := strconv.ParseFloat(raw, v.Type().Bits())
if err != nil {
return errx.Wrapf(op, err, "parse float %q", raw)
}
v.SetFloat(f)
case reflect.Slice:
if sep == "" {
sep = ","
}
parts := strings.Split(raw, sep)
out := reflect.MakeSlice(v.Type(), len(parts), len(parts))
for i, p := range parts {
if err := assignString(out.Index(i), strings.TrimSpace(p), sep); err != nil {
return err
}
}
v.Set(out)
default:
return errx.Newf(op, "unsupported field type %s", v.Type())
}
return nil
}