feat: initial release — env decoder extracted from ai-fabric

Public API:
- Unmarshal(dst, opts...)
- UnmarshalPrefix(dst, prefix, opts...)
- AsMap() / AsMapPrefix(prefix)
- Options: WithTrim, WithWeaklyTyped, WithTagName, WithDecodeHook

Merges three divergent in-repo copies (ai-fabric/pkg/env,
markets-platform/TP-general-code/pkg/system, and the var/agents/issue-*
snapshots), adds error wrapping, prefix filter, and pluggable hooks.

15 pure-Go unit tests, no synthetic mocks, lint clean.

Extracted per inventar/docs/asr/ASR-0008-ai-fabric-audit.md.
This commit is contained in:
2026-04-24 13:41:13 +01:00
commit 288a5b600c
13 changed files with 714 additions and 0 deletions
+222
View File
@@ -0,0 +1,222 @@
package env
import (
"reflect"
"testing"
)
// asMapFromEnviron is tested with a synthetic environ slice — this is NOT a
// mock of anything external. It's a pure-input unit test that isolates the
// core mapping logic from process-global state.
func TestAsMapFromEnviron_FlatAndNested(t *testing.T) {
got := asMapFromEnviron([]string{
"FOO=bar",
"SERVICE_HTTP_PORT=8080",
"SERVICE_KEY=abc",
}, "")
want := map[string]any{
"foo": "bar",
"service": map[string]any{
"http": map[string]any{"port": "8080"},
"key": "abc",
},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %#v, want %#v", got, want)
}
}
func TestAsMapFromEnviron_PreservesValuesWithEquals(t *testing.T) {
got := asMapFromEnviron([]string{"TOKEN=a=b=c="}, "")
if got["token"] != "a=b=c=" {
t.Fatalf("expected full value preserved, got %#v", got["token"])
}
}
func TestAsMapFromEnviron_PrefixFilterAndStrip(t *testing.T) {
got := asMapFromEnviron([]string{
"APP_DB_HOST=localhost",
"APP_DB_PORT=5432",
"OTHER_THING=skip",
}, "APP_")
want := map[string]any{
"db": map[string]any{"host": "localhost", "port": "5432"},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %#v, want %#v", got, want)
}
}
func TestAsMapFromEnviron_EmptyAndMalformedAreSkipped(t *testing.T) {
got := asMapFromEnviron([]string{"NOEQUALS", ""}, "")
if len(got) != 0 {
t.Fatalf("expected no entries, got %#v", got)
}
}
func TestAsMapFromEnviron_FirstWriteWinsOnCollision(t *testing.T) {
// This matches the ai-fabric original: once a leaf is set for a path,
// subsequent entries that would overwrite it are ignored.
got := asMapFromEnviron([]string{
"A=1",
"A=2",
}, "")
if got["a"] != "1" {
t.Fatalf("expected first-write-wins (a=1), got %#v", got["a"])
}
}
func TestAsMapFromEnviron_PrefixOnlyVariableIsSkipped(t *testing.T) {
// "APP_=xxx" with prefix "APP_" would strip to empty key — we skip it.
got := asMapFromEnviron([]string{"APP_=oops"}, "APP_")
if len(got) != 0 {
t.Fatalf("expected empty map, got %#v", got)
}
}
// ---------------------------------------------------------------------------
// Decoder / option tests
// ---------------------------------------------------------------------------
func TestUnmarshal_BasicStruct(t *testing.T) {
t.Setenv("SERVICE_KEY", "abc")
t.Setenv("SERVICE_PORT", "8080")
var cfg struct {
Service struct {
Key string
Port int
}
}
if err := Unmarshal(&cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.Service.Key != "abc" {
t.Errorf("Key = %q, want abc", cfg.Service.Key)
}
if cfg.Service.Port != 8080 {
t.Errorf("Port = %d, want 8080 (weakly-typed string->int)", cfg.Service.Port)
}
}
func TestUnmarshal_TrimsStringsByDefault(t *testing.T) {
t.Setenv("NAME", " hello ")
var cfg struct{ Name string }
if err := Unmarshal(&cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.Name != "hello" {
t.Fatalf("expected trimmed value, got %q", cfg.Name)
}
}
func TestUnmarshal_WithTrimOff(t *testing.T) {
t.Setenv("NAME", " keep me ")
var cfg struct{ Name string }
if err := Unmarshal(&cfg, WithTrim(false)); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.Name != " keep me " {
t.Fatalf("expected untrimmed value, got %q", cfg.Name)
}
}
func TestUnmarshalPrefix_StripsAndFilters(t *testing.T) {
t.Setenv("APP_DB_HOST", "localhost")
t.Setenv("APP_DB_PORT", "5432")
t.Setenv("OTHER_DB_HOST", "should-be-ignored")
var cfg struct {
DB struct {
Host string
Port int
} `mapstructure:"db"`
}
if err := UnmarshalPrefix(&cfg, "APP_"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.DB.Host != "localhost" {
t.Errorf("Host = %q, want localhost", cfg.DB.Host)
}
if cfg.DB.Port != 5432 {
t.Errorf("Port = %d, want 5432", cfg.DB.Port)
}
}
func TestUnmarshal_WithTagName(t *testing.T) {
t.Setenv("DATABASE_URL", "postgres://x")
// The map is always nested by '_', so tags also reference the nested
// path, one component per struct level. Here we override the outer
// field name via a custom tag.
var cfg struct {
DB struct {
URL string
} `env:"database"`
}
if err := Unmarshal(&cfg, WithTagName("env")); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.DB.URL != "postgres://x" {
t.Fatalf("URL = %q, want postgres://x", cfg.DB.URL)
}
}
func TestUnmarshal_WeaklyTypedOff(t *testing.T) {
t.Setenv("PORT", "8080")
var cfg struct{ Port int }
err := Unmarshal(&cfg, WithWeaklyTyped(false))
// With strict typing, "8080" (string) cannot decode into an int.
if err == nil {
t.Fatal("expected strict typing to reject string->int coercion")
}
}
func TestUnmarshal_SpecialCharactersSurvive(t *testing.T) {
// Regression: keys with special chars in their values must pass through
// unchanged (modulo TrimSpace).
t.Setenv("SPECIAL", "@!#$%^&*()")
var cfg struct{ Special string }
if err := Unmarshal(&cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.Special != "@!#$%^&*()" {
t.Fatalf("Special = %q, want @!#$%%^&*()", cfg.Special)
}
}
func TestUnmarshal_RejectsNonPointer(t *testing.T) {
var cfg struct{ A string }
err := Unmarshal(cfg) // no pointer
if err == nil {
t.Fatal("expected error when passing a non-pointer")
}
}
func TestAsMap_ExposesCurrentEnv(t *testing.T) {
t.Setenv("GO_ENV_ASMAP_PROBE", "hit")
m := AsMap()
goenv, ok := m["go"].(map[string]any)
if !ok {
t.Fatalf("expected nested map under 'go', got %T", m["go"])
}
env, ok := goenv["env"].(map[string]any)
if !ok {
t.Fatalf("expected nested map under 'go.env', got %T", goenv["env"])
}
asmap, ok := env["asmap"].(map[string]any)
if !ok {
t.Fatalf("expected nested map under 'go.env.asmap', got %T", env["asmap"])
}
if asmap["probe"] != "hit" {
t.Fatalf("probe = %v, want hit", asmap["probe"])
}
}