Skip to content

Commit 06d583d

Browse files
committed
Update docs for v0.6-v0.8: multi-file sources, built-in validators, model validators, observability
1 parent eeb8b30 commit 06d583d

5 files changed

Lines changed: 348 additions & 27 deletions

File tree

CHANGELOG.md

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -192,24 +192,84 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
192192

193193
## Roadmap
194194

195-
### v0.6 (Planned)
195+
## [0.8.0] - 2026-05-05
196+
197+
### Added — Observability
198+
199+
- **Audit logging** (`WithAuditLogger`)
200+
- Callback receives `[]AuditEntry` after every successful load
201+
- Each entry has `Field`, `Source`, and `Value` (secrets redacted)
202+
- **Load hooks** (`WithLoadHook`)
203+
- Low-level hook called after every load with `success bool`, `duration`, `errCount`
204+
- Used by Prometheus and OTel submodules
205+
- **Prometheus submodule** (`confkit/prometheus`)
206+
- `NewMetrics(reg)` registers `confkit_loads_total`, `confkit_load_duration_seconds`, `confkit_errors_total`
207+
- `Metrics.Hook()` returns a `confkit.Option` — drop-in, no wrapping needed
208+
- **OpenTelemetry submodule** (`confkit/otel`)
209+
- `otel.Load[T](ctx, tracer, sources...)` — wraps Load with a span
210+
- `otel.LoadWithOptions[T](ctx, tracer, options...)` — same for options form
211+
- Span attributes: `confkit.sources`, `confkit.success`
196212

197-
- Configuration composition / includes
198-
- Multi-file YAML support (merge multiple files)
199-
- Config templating (Helm-style)
200-
- Performance optimizations
213+
---
201214

202-
### v0.7 (Planned)
215+
## [0.7.0] - 2026-05-05
216+
217+
### Added — Advanced Validation
218+
219+
- **18 built-in format validators** (no external dependencies):
220+
- `email` — valid email address
221+
- `url` — valid URL (any scheme)
222+
- `http_url` — valid HTTP or HTTPS URL
223+
- `ip` — valid IPv4 or IPv6 address
224+
- `ipv4` — valid IPv4 address
225+
- `ipv6` — valid IPv6 address
226+
- `uuid` — valid UUID (v1–v5)
227+
- `hostname` — valid hostname (RFC 1123)
228+
- `port` — valid port number (1–65535), works on int and string fields
229+
- `regex=pattern` — value must match the given regular expression
230+
- `len=N` — string must be exactly N characters (Unicode-aware)
231+
- `contains=str` — string must contain substring
232+
- `startswith=str` — string must start with prefix
233+
- `endswith=str` — string must end with suffix
234+
- `alpha` — letters only
235+
- `alphanum` — letters and digits only
236+
- `numeric` — digits only
237+
- `lowercase` — all lowercase
238+
- `uppercase` — all uppercase
239+
- `notempty` — must not be blank (non-whitespace)
240+
241+
- **Model validators** (`WithModelValidator[T](fn func(*T) error)`)
242+
- Cross-field validation: runs after all field validators pass
243+
- Receives a pointer to the fully populated config struct
244+
- Multiple model validators can be registered; all run independently
203245

204-
- Custom validation rules library (email, URL, IP, etc.)
205-
- Conditional validation (Field B validation depends on Field A)
206-
- Cross-field validation
246+
---
207247

208-
### v0.8 (Planned)
248+
## [0.6.0] - 2026-05-05
249+
250+
### Added — Config Composition
251+
252+
- **Multi-file YAML** (`FromYAMLFiles(paths ...string)`)
253+
- Merges multiple YAML files; later files override earlier ones
254+
- Nested maps are merged recursively (deep merge)
255+
- **Multi-file JSON** (`FromJSONFiles(paths ...string)`)
256+
- Same semantics as `FromYAMLFiles` for JSON
257+
- **Multi-file TOML** (`FromTOMLFiles(paths ...string)`)
258+
- Same semantics as `FromYAMLFiles` for TOML
259+
260+
**Usage pattern:**
261+
```go
262+
cfg, err := confkit.Load[Config](
263+
confkit.FromYAMLFiles(
264+
"config/base.yaml", // defaults
265+
"config/production.yaml", // env-specific overrides
266+
"config/local.yaml", // developer local overrides
267+
),
268+
confkit.FromEnv(),
269+
)
270+
```
209271

210-
- Prometheus metrics for config loads and reloads
211-
- Structured tracing (OpenTelemetry)
212-
- Config audit logging
272+
---
213273

214274
### v1.0 (Planned)
215275

docs/api/index.md

Lines changed: 105 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -57,21 +57,31 @@ Advanced loading with functional options for custom validators, middleware, and
5757

5858
**Options:**
5959
- `WithSource(source Source)` — Add a source
60-
- `WithValidator(name string, fn CustomValidatorFunc)` — Register custom validator
61-
- `WithMiddleware(fn MiddlewareFunc)` — Add transformation middleware
62-
- `WithInterpolationMax(max int)` — Set interpolation recursion limit
60+
- `WithValidator(name string, fn CustomValidatorFunc)` — Register a custom per-field validator
61+
- `WithModelValidator[T](fn func(*T) error)` — Register a cross-field validator
62+
- `WithMiddleware(fn MiddlewareFunc)` — Add a value transformation middleware
63+
- `WithInterpolationMaxDepth(n int)` — Set interpolation recursion limit
64+
- `WithAuditLogger(fn AuditLogger)` — Receive a log of every field's resolved value and source
65+
- `WithLoadHook(fn LoadHookFunc)` — Receive success/duration/errCount after every load
6366

6467
**Example:**
6568
```go
6669
cfg, err := confkit.LoadWithOptions[Config](
6770
confkit.WithSource(confkit.FromYAML("config.yaml")),
6871
confkit.WithSource(confkit.FromEnv()),
69-
confkit.WithValidator("custom", func(val string) error {
70-
if val == "invalid" {
71-
return fmt.Errorf("cannot be 'invalid'")
72+
// Cross-field validation
73+
confkit.WithModelValidator(func(cfg *Config) error {
74+
if cfg.TLSEnabled && cfg.CertPath == "" {
75+
return fmt.Errorf("cert_path required when tls_enabled is true")
7276
}
7377
return nil
7478
}),
79+
// Audit trail
80+
confkit.WithAuditLogger(func(entries []confkit.AuditEntry) {
81+
for _, e := range entries {
82+
log.Printf("field=%s source=%s value=%s", e.Field, e.Source, e.Value)
83+
}
84+
}),
7585
)
7686
```
7787

@@ -217,6 +227,26 @@ cfg, _ := confkit.Load[Config](confkit.FromTOML("config.toml"))
217227

218228
---
219229

230+
### FromYAMLFiles / FromJSONFiles / FromTOMLFiles
231+
232+
```go
233+
func FromYAMLFiles(paths ...string) Source
234+
func FromJSONFiles(paths ...string) Source
235+
func FromTOMLFiles(paths ...string) Source
236+
```
237+
238+
Merges multiple files of the same format into a single source. Later files override earlier ones; nested maps are deep-merged.
239+
240+
**Example:**
241+
```go
242+
cfg, _ := confkit.Load[Config](
243+
confkit.FromYAMLFiles("base.yaml", "production.yaml", "local.yaml"),
244+
confkit.FromEnv(),
245+
)
246+
```
247+
248+
---
249+
220250
### FromFlags
221251

222252
```go
@@ -233,6 +263,42 @@ cfg, _ := confkit.Load[Config](confkit.FromFlags())
233263

234264
---
235265

266+
## Observability Submodules
267+
268+
### confkit/prometheus
269+
270+
```go
271+
import "github.com/MimoJanra/confkit/prometheus"
272+
273+
m := prometheus.NewMetrics(prometheus.DefaultRegisterer)
274+
275+
cfg, err := confkit.LoadWithOptions[Config](
276+
confkit.WithSource(confkit.FromEnv()),
277+
m.Hook(), // records loads_total, load_duration_seconds, errors_total
278+
)
279+
```
280+
281+
**Metrics registered:**
282+
- `confkit_loads_total{status="success|error"}` — counter
283+
- `confkit_load_duration_seconds` — histogram
284+
- `confkit_errors_total{kind="validation"}` — counter
285+
286+
---
287+
288+
### confkit/otel
289+
290+
```go
291+
import "github.com/MimoJanra/confkit/otel"
292+
293+
cfg, err := otel.Load[Config](ctx, tracer,
294+
confkit.FromEnv(),
295+
confkit.FromYAML("config.yaml"),
296+
)
297+
// Creates span "confkit.Load" with attributes: confkit.sources, confkit.success
298+
```
299+
300+
---
301+
236302
## Cloud & Enterprise Sources
237303

238304
### FromK8sConfigMap
@@ -310,17 +376,43 @@ Configuration is driven by struct tags. Common tags:
310376

311377
### Validation Rules
312378

313-
- `required` — Field must be present
314-
- `min=N` — Minimum value (int, float, string length)
315-
- `max=N` — Maximum value (int, float, string length)
316-
- `oneof=val1,val2,...` — Must be one of the listed values
379+
**Numeric / range:**
380+
- `required` — field must be present and non-zero
381+
- `min=N` — minimum value (int/float) or minimum length (string)
382+
- `max=N` — maximum value (int/float) or maximum length (string)
383+
- `oneof=val1,val2,...` — must be one of the listed values
384+
385+
**Format (string fields):**
386+
- `email` — valid email address
387+
- `url` — valid URL (any scheme)
388+
- `http_url` — valid HTTP or HTTPS URL
389+
- `ip` — valid IPv4 or IPv6 address
390+
- `ipv4` — valid IPv4 address
391+
- `ipv6` — valid IPv6 address
392+
- `uuid` — valid UUID (v1–v5)
393+
- `hostname` — valid hostname per RFC 1123
394+
- `port` — valid port number 1–65535 (works on `int` and `string`)
395+
- `regex=pattern` — must match the regular expression
396+
- `len=N` — must be exactly N characters (Unicode-aware)
397+
- `contains=str` — must contain the substring
398+
- `startswith=str` — must start with the prefix
399+
- `endswith=str` — must end with the suffix
400+
- `alpha` — letters only
401+
- `alphanum` — letters and digits only
402+
- `numeric` — digits only
403+
- `lowercase` — must be all lowercase
404+
- `uppercase` — must be all uppercase
405+
- `notempty` — must not be blank (non-whitespace)
317406

318407
**Example:**
319408
```go
320409
type Config struct {
321-
Port int `env:"PORT" validate:"required,min=1,max=65535"`
322-
Status string `validate:"oneof=active,inactive,pending"`
323-
DatabaseURL string `validate:"required" secret:"true"`
410+
Port int `env:"PORT" validate:"required,port"`
411+
AdminEmail string `env:"EMAIL" validate:"required,email"`
412+
APIKey string `env:"API_KEY" validate:"required,len=32" secret:"true"`
413+
Environment string `env:"ENV" validate:"oneof=dev,staging,prod"`
414+
ServiceURL string `env:"SVC_URL" validate:"http_url"`
415+
ServiceID string `env:"SVC_ID" validate:"uuid"`
324416
}
325417
```
326418

docs/docs/getting-started.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Install confkit and load your first configuration in 5 minutes.
1010
## Installation
1111

1212
```bash
13-
go get github.com/MimoJanra/confkit@v0.5.0
13+
go get github.com/MimoJanra/confkit@v0.5.1
1414
```
1515

1616
**Requirements:** Go 1.24 or later

docs/docs/sources.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,43 @@ cfg, err := confkit.Load[Config](etcd)
298298

299299
---
300300

301+
## Multi-File Sources
302+
303+
When you need to merge several files of the same format (e.g., a base config + environment overlay + local overrides), use the `*Files` variants. Later files override earlier ones; nested maps are merged recursively (deep merge).
304+
305+
### FromYAMLFiles
306+
307+
```go
308+
cfg, err := confkit.Load[Config](
309+
confkit.FromYAMLFiles(
310+
"config/base.yaml", // shipped defaults
311+
"config/production.yaml", // environment overlay
312+
"config/local.yaml", // developer overrides (git-ignored)
313+
),
314+
confkit.FromEnv(),
315+
)
316+
```
317+
318+
### FromJSONFiles
319+
320+
```go
321+
cfg, err := confkit.Load[Config](
322+
confkit.FromJSONFiles("base.json", "override.json"),
323+
)
324+
```
325+
326+
### FromTOMLFiles
327+
328+
```go
329+
cfg, err := confkit.Load[Config](
330+
confkit.FromTOMLFiles("base.toml", "local.toml"),
331+
)
332+
```
333+
334+
**Deep merge behaviour:** if both files define a nested map key, the maps are merged. Scalar values in the later file win.
335+
336+
---
337+
301338
## Source Precedence
302339

303340
When using multiple sources, they are evaluated left-to-right. Later sources override earlier ones:

0 commit comments

Comments
 (0)