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
+47
View File
@@ -0,0 +1,47 @@
package env
import "github.com/mitchellh/mapstructure"
// Option customises Unmarshal/UnmarshalPrefix behaviour.
type Option func(*options)
type options struct {
trim bool
weaklyTyped bool
tagName string
extraHooks []mapstructure.DecodeHookFunc
}
func defaultOptions() options {
return options{
trim: true,
weaklyTyped: true,
tagName: "mapstructure",
}
}
// WithTrim toggles automatic TrimSpace on string values. Default: true.
// Use WithTrim(false) when whitespace is semantically meaningful.
func WithTrim(enable bool) Option {
return func(o *options) { o.trim = enable }
}
// WithWeaklyTyped toggles mapstructure's WeaklyTypedInput. Default: true.
// When true, "1" decodes into int, "true" into bool, etc. Disable for
// strict string-only decoding.
func WithWeaklyTyped(enable bool) Option {
return func(o *options) { o.weaklyTyped = enable }
}
// WithTagName selects the struct tag mapstructure uses for field names.
// Default: "mapstructure". Set "env" to use `env:"FIELD_NAME"` tags.
func WithTagName(name string) Option {
return func(o *options) { o.tagName = name }
}
// WithDecodeHook appends a user hook to the decoder chain. Hooks run after
// the built-in trim hook (unless trimming is disabled) and in the order
// they're added.
func WithDecodeHook(h mapstructure.DecodeHookFunc) Option {
return func(o *options) { o.extraHooks = append(o.extraHooks, h) }
}