1- # confkit
1+ # confkit — Type-Safe Configuration Toolkit for Go
22
3- > Typed, validated configuration loading for Go. The Pydantic equivalent for Go services.
3+ Typed, validated configuration loading for Go. The Pydantic equivalent for Go services.
44
55confkit loads configuration from multiple sources (YAML, JSON, TOML, env, flags, Kubernetes, Vault, Consul, etcd, AWS), merges them with explicit precedence, validates fields using struct tags, and returns a fully typed struct — or a human-readable error message identifying the exact field, source, and rule that failed.
66
7- Use confkit instead of Viper when you need: type-safe config without Get*() accessors, validation at startup, secret redaction, or clear error messages. Use instead of raw os.Getenv when you need struct-level config with defaults and validation.
7+ **Use confkit instead of Viper** when you need: type-safe config without Get*() accessors, validation at startup, secret redaction, or clear error messages.
8+
9+ **Use confkit instead of raw os.Getenv** when you need struct-level config with defaults and validation.
10+
11+ ## Key Differentiators
12+
13+ - ✅ **Typed Load[T]** — Returns your struct, fully type-checked at compile time (not map[string]interface{})
14+ - ✅ **Built-in validation** — 20+ rules in struct tags (`min`, `max`, `required`, `oneof`, `email`, `uuid`, ...), custom validators
15+ - ✅ **Secret redaction** — Mark fields `secret:"true"`, automatically redacted in errors and logs
16+ - ✅ **Human-readable errors** — Clear messages showing field, source, rule, and value (redacted for secrets)
17+ - ✅ **Multiple sources** — Explicit precedence for YAML, JSON, TOML, env, flags, K8s, Vault, AWS, Consul, etcd
18+ - ✅ **Optional cloud integrations** — Cloud sources are separate modules, not bundled (keeps core light)
19+ - ✅ **Production-ready** — v1.0.0, 80%+ test coverage, API stability guarantee
820
921## Install
1022
1123```bash
12- go get github.com/MimoJanra/confkit@v1.0.0
24+ go get github.com/MimoJanra/confkit@latest
1325```
1426
1527## Core API
@@ -18,35 +30,65 @@ go get github.com/MimoJanra/confkit@v1.0.0
1830// Primary entry points
1931func Load[T any](sources ...Source) (*T, error)
2032func LoadWithOptions[T any](options ...Option) (*T, error)
33+ func LoadContext[T any](ctx context.Context, sources ...Source) (*T, error)
34+ func LoadWithOptionsContext[T any](ctx context.Context, options ...Option) (*T, error)
2135func LoadWithWatcher[T any](filePath string, sources ...Source) (*T, *ConfigWatcher, error)
36+ func ValidateOnly[T any](ctx context.Context, options ...Option) (*T, error)
37+ func MustLoad[T any](sources ...Source) *T // panics with *ErrorReport
38+ func MustLoadContext[T any](ctx context.Context, sources ...Source) *T
2239
2340// Error formatting
24- func Explain(err error) string // returns human-readable string; safe to pass to log.Fatal
25-
26- // Config introspection
27- func ScanFields(v any) []FieldInfo
28- func DumpConfig(cfg any, fields []FieldInfo) ([]byte, error) // JSON; secrets → "***REDACTED***"
41+ func Explain(err error) string // human-readable; safe to pass to log.Fatal
42+
43+ // Config dump (secrets redacted by default)
44+ func Dump[T any](cfg T, opts ...DumpOption) ([]byte, error) // JSON
45+ func DumpString[T any](cfg T, opts ...DumpOption) string // JSON string
46+ func DumpYAML[T any](cfg T, opts ...DumpOption) ([]byte, error)
47+
48+ // Config file discovery
49+ func FindFile(name string, dirs ...string) (string, error)
50+ func FindSource(name string, dirs ...string) Source // returns error source wrapping ErrNotFound if absent
51+ func DefaultSearchDirs(appName string) []string // ./ ./config/ /etc/<app>/ ~/.<app>/
52+ var ErrNotFound error
53+
54+ // Overlay loading (Spring Boot-style)
55+ func FromOverlay(base, env string) Source // loads base, merges config.<env>.yaml on top if present
56+ func OverlayPath(basePath, env string) string
2957```
3058
3159## Option constructors (used with LoadWithOptions)
3260
3361```go
3462func WithSource(source Source) Option
3563func WithValidator(name string, fn func(reflect.Value) error) Option
64+ func WithModelValidator[T any](fn func(*T) error) Option
3665func WithMiddleware(fn MiddlewareFunc) Option
66+ func WithAuditLogger(fn AuditLogger) Option
67+ func WithLoadHook(fn LoadHookFunc) Option
68+ func WithContext(ctx context.Context) Option
3769func WithInterpolationMaxDepth(depth int) Option
3870```
3971
72+ ## Dump options
73+
74+ ```go
75+ func WithDumpFormat(f DumpFormat) DumpOption // FormatJSON (default) or FormatYAML
76+ func WithDumpRedactSecrets(redact bool) DumpOption // default: true
77+ ```
78+
4079## Built-in source constructors
4180
4281```go
4382confkit.FromYAML(path string) Source
83+ confkit.FromYAMLOptional(path string) Source
84+ confkit.FromYAMLFiles(paths ...string) Source
4485confkit.FromJSON(path string) Source
86+ confkit.FromJSONFiles(paths ...string) Source
4587confkit.FromTOML(path string) Source
88+ confkit.FromTOMLFiles(paths ...string) Source
4689confkit.FromEnv() Source
4790confkit.FromFlags() Source
48- confkit.FromKubernetesConfigMap(namespace, name string) Source
49- confkit.FromKubernetesConfigMapWithPath(namespace, name, mountPath string) Source
91+ confkit.FromFlagsWithArgs(args []string) Source
5092confkit.NewErrorSource(err error) Source // permanently-errored source; use in custom source constructors
5193```
5294
@@ -66,25 +108,45 @@ hidden:"true" exclude from CLI help
66108
67109## Validation rules (validate tag)
68110
111+ Rules are comma-separated: `validate:"required,min=1,max=65535"`
112+
69113```
70114required value must not be zero/empty
71- min=N int/float: value >= N; string: len >= N
72- max=N int/float: value <= N; string: len <= N
115+ min=N int/float: value >= N; string: len >= N chars
116+ max=N int/float: value <= N; string: len <= N chars
73117oneof=a b c string must equal one of the space-separated options
118+ email valid email address (regex-based)
119+ url valid URL (any scheme, must have host)
120+ http_url valid HTTP or HTTPS URL
121+ ip valid IP address (v4 or v6)
122+ ipv4 valid IPv4 address
123+ ipv6 valid IPv6 address
124+ uuid valid UUID (versions 1–5)
125+ hostname valid hostname (RFC 1123, max 253 chars)
126+ port valid port number (1–65535); works on int and string fields
127+ regex=PATTERN string matches the given regular expression
128+ len=N string: exactly N characters
129+ contains=S string contains substring S
130+ startswith=S string starts with prefix S
131+ endswith=S string ends with suffix S
132+ alpha string contains only Unicode letters
133+ alphanum string contains only Unicode letters and digits
134+ numeric string contains only Unicode digits
135+ lowercase string is all lowercase
136+ uppercase string is all uppercase
137+ notempty string is not blank (after TrimSpace)
74138<custom> name of a validator registered with WithValidator
75139```
76140
77- Rules are comma-separated: `validate:"required,min=1,max=65535"`
78-
79141## Source interface (implement to add custom backends)
80142
81143```go
82144type Source interface {
83145 Name() string
84146 Lookup(ctx context.Context, field *FieldInfo) (any, bool, error)
85- // ("" , false, nil) → not found in this source
86- // (value, true, nil) → found
87- // ("" , false, err) → error reading source
147+ // (nil , false, nil) → not found in this source
148+ // (value, true, nil) → found
149+ // (nil , false, err) → error reading source
88150}
89151```
90152
@@ -103,9 +165,19 @@ type FieldInfo struct {
103165
104166## Supported field types
105167
106- string, int/int8/int16/int32/int64, uint/uint8/uint16/uint32/uint64,
107- float32/float64, bool (true/false/1/0/yes/no), time.Duration ("5s","1m30s"),
108- []string (comma-separated), []int (comma-separated)
168+ ```
169+ string
170+ int / int8 / int16 / int32 / int64
171+ uint / uint8 / uint16 / uint32 / uint64
172+ float32 / float64
173+ bool — true / false / 1 / 0 / yes / no
174+ time.Duration — "5s", "1m30s", "2h"
175+ time.Time — RFC3339: "2006-01-02T15:04:05Z07:00"
176+ []string — comma-separated: "a,b,c"
177+ []int — comma-separated: "1,2,3"
178+ map[string]string — KEY=val,KEY2=val2 (quoted: KEY="val,with,commas")
179+ map[string]int — KEY=1,KEY2=2
180+ ```
109181
110182## Minimal working example
111183
@@ -169,7 +241,17 @@ cfg, err := confkit.LoadWithOptions[Config](
169241```go
170242cfg, watcher, err := confkit.LoadWithWatcher[Config]("config.yaml",
171243 confkit.FromYAML("config.yaml"), confkit.FromEnv())
244+
245+ // Basic listener
172246watcher.AddListener(func(old, new any, err error) { /* react to change */ })
247+
248+ // Delta listener — see exactly which fields changed
249+ watcher.AddDeltaListener(func(delta confkit.ConfigDelta, old, new map[string]any, err error) {
250+ for _, k := range delta.Changed { log.Printf("changed: %s", k) }
251+ for _, k := range delta.Added { log.Printf("added: %s", k) }
252+ for _, k := range delta.Removed { log.Printf("removed: %s", k) }
253+ })
254+
173255watcher.SetPollInterval(5 * time.Second)
174256watcher.Start()
175257defer watcher.Stop()
@@ -178,23 +260,27 @@ defer watcher.Stop()
178260## Enterprise sources (separate modules, import only what you need)
179261
180262```go
181- // go get confkit/vault
263+ // go get github.com/MimoJanra/confkit/k8s
264+ k8s.FromKubernetesConfigMap(namespace, name string) confkit.Source
265+ k8s.FromKubernetesConfigMapWithPath(namespace, name, mountPath string) confkit.Source
266+
267+ // go get github.com/MimoJanra/confkit/vault
182268vault.FromVault(addr string, auth vault.VaultAuth, pathPrefix string) confkit.Source
183269vault.FromVaultWithKVVersion(addr string, auth vault.VaultAuth, kvVersion int, pathPrefix string) confkit.Source
184270vault.VaultTokenAuth(token string) vault.VaultAuth
185271vault.VaultAppRoleAuth(roleID, secretID string) vault.VaultAuth
186272vault.VaultKubernetesAuth(role, jwt string) vault.VaultAuth
187273
188- // go get confkit/consul
274+ // go get github.com/MimoJanra/ confkit/consul
189275consul.FromConsul(addr string) confkit.Source
190276consul.FromConsulWithToken(addr, token string) confkit.Source
191277consul.FromConsulWithOptions(addr, token, datacenter string) confkit.Source
192278
193- // go get confkit/etcd
279+ // go get github.com/MimoJanra/ confkit/etcd
194280etcd.FromEtcd(endpoints []string) confkit.Source
195281etcd.FromEtcdWithPrefix(endpoints []string, prefix string) confkit.Source
196282
197- // go get confkit/aws
283+ // go get github.com/MimoJanra/ confkit/aws
198284aws.FromAWSSSMParameterStore(pathPrefix string) confkit.Source
199285aws.FromAWSSSMParameterStoreWithTTL(pathPrefix string, ttl time.Duration) confkit.Source
200286aws.FromAWSSecretsManager(secretName string) confkit.Source
@@ -207,7 +293,7 @@ aws.FromAWSSSMParameterStoreMultiRegion(pathPrefix string, regions []string) con
207293## Schema generation
208294
209295```go
210- import "confkit/schema"
296+ import "github.com/MimoJanra/ confkit/schema"
211297s, _ := schema.GenerateSchema[Config]() // *schema.Schema (JSON Schema draft-07)
212298schema.GenerateMarkdown[Config]() // Markdown reference table
213299schema.GenerateCLIHelp[Config]() // --help style text
@@ -221,6 +307,9 @@ Sources passed to Load are applied left to right; later sources override earlier
221307
222308```go
223309type ErrorReport struct{ Errors []FieldError } // implements error
310+ func (r *ErrorReport) FirstError() *FieldError // nil if no errors
311+ func (r *ErrorReport) Unwrap() []error
312+
224313type FieldError struct {
225314 Path string // dot-path: "Database.Password"
226315 Source string // "env" | "yaml" | "validation" | "default"
@@ -236,6 +325,7 @@ type FieldError struct {
236325
237326```
238327confkit — core (yaml.v3 + go-toml/v2 only)
328+ confkit/k8s — Kubernetes ConfigMap/Secret source
239329confkit/vault — HashiCorp Vault KV source
240330confkit/consul — HashiCorp Consul KV source
241331confkit/etcd — etcd v3 source
0 commit comments