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
134
conf_test.go
Normal file
134
conf_test.go
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type allTypes struct {
|
||||
S string `env:"S" default:"hi"`
|
||||
B bool `env:"B" default:"true"`
|
||||
I int `env:"I" default:"7"`
|
||||
I64 int64 `env:"I64" default:"100"`
|
||||
U uint32 `env:"U" default:"3"`
|
||||
F float64 `env:"F" default:"1.5"`
|
||||
D time.Duration `env:"D" default:"5s"`
|
||||
BS []byte `env:"BS" default:"abc"`
|
||||
List []string `env:"LIST" default:"a,b,c"`
|
||||
Skip string `env:"-"`
|
||||
None string
|
||||
}
|
||||
|
||||
func TestLoadDefaults(t *testing.T) {
|
||||
var c allTypes
|
||||
if err := Load(&c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.S != "hi" || !c.B || c.I != 7 || c.I64 != 100 || c.U != 3 || c.F != 1.5 ||
|
||||
c.D != 5*time.Second || string(c.BS) != "abc" ||
|
||||
strings.Join(c.List, ",") != "a,b,c" {
|
||||
t.Fatalf("defaults not applied: %+v", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrecedence(t *testing.T) {
|
||||
high := MapSource(map[string]string{"S": "high"})
|
||||
low := MapSource(map[string]string{"S": "low", "I": "42"})
|
||||
|
||||
var c allTypes
|
||||
if err := Load(&c, high, low); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.S != "high" {
|
||||
t.Fatalf("S=%q want high", c.S)
|
||||
}
|
||||
if c.I != 42 {
|
||||
t.Fatalf("I=%d want 42", c.I)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPointerField(t *testing.T) {
|
||||
type p struct {
|
||||
X *int `env:"X"`
|
||||
Y *int `env:"Y"`
|
||||
}
|
||||
var c p
|
||||
if err := Load(&c, MapSource(map[string]string{"X": "9"})); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.X == nil || *c.X != 9 {
|
||||
t.Fatalf("X=%v", c.X)
|
||||
}
|
||||
if c.Y != nil {
|
||||
t.Fatalf("Y should be nil, got %v", *c.Y)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNestedStruct(t *testing.T) {
|
||||
type Inner struct {
|
||||
A string `env:"A" default:"x"`
|
||||
}
|
||||
type Outer struct {
|
||||
Inner Inner
|
||||
B string `env:"B" default:"y"`
|
||||
}
|
||||
var o Outer
|
||||
if err := Load(&o); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if o.Inner.A != "x" || o.B != "y" {
|
||||
t.Fatalf("%+v", o)
|
||||
}
|
||||
}
|
||||
|
||||
type withValidate struct {
|
||||
URL string `env:"URL"`
|
||||
}
|
||||
|
||||
func (w *withValidate) Validate() error {
|
||||
if w.URL == "" {
|
||||
return errors.New("URL required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestValidatorFires(t *testing.T) {
|
||||
var w withValidate
|
||||
err := Load(&w)
|
||||
if err == nil || !strings.Contains(err.Error(), "URL required") {
|
||||
t.Fatalf("want URL required error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseError(t *testing.T) {
|
||||
type bad struct {
|
||||
N int `env:"N"`
|
||||
}
|
||||
var b bad
|
||||
err := Load(&b, MapSource(map[string]string{"N": "notanumber"}))
|
||||
if err == nil || !strings.Contains(err.Error(), "N") {
|
||||
t.Fatalf("want field N error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectsNonPointer(t *testing.T) {
|
||||
var c allTypes
|
||||
if err := Load(c); err == nil {
|
||||
t.Fatal("expected error for non-pointer")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSepOverride(t *testing.T) {
|
||||
type s struct {
|
||||
L []string `env:"L" sep:"|"`
|
||||
}
|
||||
var x s
|
||||
if err := Load(&x, MapSource(map[string]string{"L": "a|b|c"})); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Join(x.L, ",") != "a,b,c" {
|
||||
t.Fatalf("got %v", x.L)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue