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>
134 lines
2.8 KiB
Go
134 lines
2.8 KiB
Go
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)
|
|
}
|
|
}
|