diff --git a/README.md b/README.md
index f459e27..5a741e4 100644
--- a/README.md
+++ b/README.md
@@ -111,10 +111,10 @@ For example, a transformer can normalize `" PROD "` to `"prod"` before a `oneof:
```go
loader := rigging.NewLoader[Config]().
WithSource(sourceenv.New(sourceenv.Options{Prefix: "APP_"})).
- WithTransformer(rigging.TransformerFunc[Config](func(ctx context.Context, cfg *Config) error {
+ WithTransformerFunc(func(ctx context.Context, cfg *Config) error {
cfg.Environment = strings.ToLower(strings.TrimSpace(cfg.Environment))
return nil
- }))
+ })
```
## Comparison with Other Libraries
@@ -131,14 +131,50 @@ loader := rigging.NewLoader[Config]().
\* Rigging provides the `Watch()` API for custom configuration sources. Built-in file and environment sources don't support watching yet.
+## Watch/Reload Capability Status
+
+| Capability | Status | Notes |
+|---|---|---|
+| `Loader.Watch()` API | Available | Reload pipeline and snapshots are supported |
+| Custom source watch (`Source.Watch`) | Available | Implement change events in your source |
+| Built-in `sourcefile` watch | Not supported yet | Returns `ErrWatchNotSupported` |
+| Built-in `sourceenv` watch | Not supported yet | Returns `ErrWatchNotSupported` |
+
+See [Configuration Sources: Watch and Reload](docs/configuration-sources.md#watch-reload) and the [FAQ](#faq) for current limitations and usage guidance.
+
## Installation
```bash
go get github.com/Azhovan/rigging@latest
```
+## Common Paths
+
+- **New service setup**: [Quick Start](docs/quick-start.md) -> [Configuration Sources](docs/configuration-sources.md) -> [`examples/basic`](examples/basic)
+- **Validation-first startup policy**: [Quick Start: Fail-Fast Validation](docs/quick-start.md#7-fail-fast-validation) -> [API Reference: Validation Semantics](docs/api-reference.md#validation-semantics)
+- **Debugging config incidents**: [Quick Start: Provenance](docs/quick-start.md#provenance) -> [Quick Start: Snapshots](docs/quick-start.md#snapshots-debugging-audits) -> [Configuration Patterns](docs/patterns.md#7-use-provenance-during-incident-response)
+- **Migrating from Viper / envconfig**: [Comparison](#comparison-with-other-libraries) -> [Quick Start: Key Mapping Rules](docs/quick-start.md#key-mapping-rules) -> [Configuration Sources](docs/configuration-sources.md#key-normalization-by-source)
+- **Custom source + reload loop**: [Configuration Sources: Custom Sources](docs/configuration-sources.md#custom-sources) -> [Configuration Sources: Watch and Reload](docs/configuration-sources.md#watch-reload) -> [API Reference: Watch and Reload](docs/api-reference.md#watch-and-reload)
+
## Documentation
+### Choose a Starting Point
+
+| If you need to... | Start here | API / deep dive |
+|---|---|---|
+| Get from zero to a typed config loader quickly | [Quick Start](docs/quick-start.md) | [API Reference](docs/api-reference.md) |
+| Layer file + env config with explicit precedence | [Quick Start: Provide Inputs](docs/quick-start.md#3-provide-inputs) | [Configuration Sources](docs/configuration-sources.md) |
+| See where each value came from (provenance) | [Quick Start: Provenance](docs/quick-start.md#provenance) | [API Reference: `GetProvenance`](docs/api-reference.md#getprovenance) |
+| Print effective config safely (with secret redaction) | [Quick Start: Redacted Dump](docs/quick-start.md#redacted-dump) | [API Reference: `DumpEffective`](docs/api-reference.md#dumpeffective) |
+| Capture/share a redacted snapshot for debugging or audits | [Quick Start: Snapshots](docs/quick-start.md#snapshots-debugging-audits) | [API Reference: `CreateSnapshot`](docs/api-reference.md#createsnapshot) |
+| Understand env/file key mapping and naming rules | [Quick Start: Key Mapping Rules](docs/quick-start.md#key-mapping-rules) | [Configuration Sources: Key Normalization](docs/configuration-sources.md#key-normalization-by-source) |
+| Understand precedence + `required` outcomes for edge cases | [Configuration Sources: Decision Table](docs/configuration-sources.md#key-mapping-precedence-required) | [API Reference: Validation Semantics](docs/api-reference.md#validation-semantics) |
+| Normalize typed values before tag validation | [Quick Start: Typed Transforms](docs/quick-start.md#typed-transforms) | [API Reference: Load Pipeline](docs/api-reference.md#load-pipeline) |
+| Distinguish “not set” from zero values | [Quick Start: Optional Fields](docs/quick-start.md#optional-fields) | [API Reference: `Optional[T]`](docs/api-reference.md#optionalt) |
+| Add cross-field/business-rule checks | [Quick Start: Custom Validators](docs/quick-start.md#custom-validators) | [API Reference: `Validator[T]`](docs/api-reference.md#validatort) |
+| Reject unknown keys during startup | [API Reference: Strict Mode](docs/api-reference.md#strict-mode) | [Configuration Sources](docs/configuration-sources.md) |
+| Implement watch/reload with a custom source | [Configuration Sources: Watch and Reload](docs/configuration-sources.md#watch-reload) | [API Reference: Watch and Reload](docs/api-reference.md#watch-and-reload) |
+
- **[Quick Start Guide](docs/quick-start.md)** - Get started with installation, basic usage, validation, and observability
- **[Configuration Sources](docs/configuration-sources.md)** - Learn about environment variables, file sources, custom sources, and watch/reload
- **[API Reference](docs/api-reference.md)** - Complete API documentation for all types, methods, and struct tags
diff --git a/docs/api-reference.md b/docs/api-reference.md
index 4c71fef..4a3076f 100644
--- a/docs/api-reference.md
+++ b/docs/api-reference.md
@@ -14,6 +14,7 @@ loader := rigging.NewLoader[Config]()
- `WithSource(src Source) *Loader[T]` - Add a configuration source
- `WithTransformer(t Transformer[T]) *Loader[T]` - Add a typed transform (after bind/defaults/conversion, before validation)
+- `WithTransformerFunc(fn func(context.Context, *T) error) *Loader[T]` - Add a typed transform using a function (ergonomic helper for inline transforms)
- `WithValidator(v Validator[T]) *Loader[T]` - Add a custom validator
- `Strict(strict bool) *Loader[T]` - Enable/disable strict mode
- `Load(ctx context.Context) (*T, error)` - Load and validate configuration
@@ -84,6 +85,15 @@ type Transformer[T any] interface {
**Helper:**
- `TransformerFunc[T](func(ctx context.Context, cfg *T) error)` - Function adapter
+When registering an inline transform, prefer `WithTransformerFunc(...)` for a shorter call site:
+
+```go
+loader.WithTransformerFunc(func(ctx context.Context, cfg *Config) error {
+ cfg.Environment = strings.ToLower(strings.TrimSpace(cfg.Environment))
+ return nil
+})
+```
+
### Validator[T]
Interface for custom validation.
diff --git a/docs/configuration-sources.md b/docs/configuration-sources.md
index fad77d5..0d4235c 100644
--- a/docs/configuration-sources.md
+++ b/docs/configuration-sources.md
@@ -84,6 +84,7 @@ Behavior notes:
- In strict mode, valid nested keys under dynamic map entries are accepted (for example, `clickhouse_map.primary.host`), but unknown nested fields are rejected (for example, `clickhouse_map.primary.unknown`).
- With `Strict(false)`, unknown nested keys do not fail the load.
+
## Key Normalization by Source
| Source | Example input | Normalized key |
@@ -112,6 +113,7 @@ Typed transforms are a separate stage from source key normalization:
See [API Reference](api-reference.md) (`Load Pipeline`, `Transformer[T]`) and [Configuration Patterns](patterns.md) for when to use typed transforms.
+
## Key Mapping + Precedence + `required` Decision Table
| Situation | Result | Validation outcome |
@@ -146,6 +148,7 @@ type SourceWithKeys interface {
`originalKeys` lets Rigging report exact source keys in provenance (for example, full env var names).
+
## Watch and Reload
```go
diff --git a/docs/quick-start.md b/docs/quick-start.md
index 65074eb..7d157e3 100644
--- a/docs/quick-start.md
+++ b/docs/quick-start.md
@@ -70,7 +70,8 @@ if ok {
}
```
-### Redacted dump
+
+### Redacted Dump
```go
rigging.DumpEffective(os.Stdout, cfg, rigging.WithSources())
@@ -78,7 +79,8 @@ rigging.DumpEffective(os.Stdout, cfg, rigging.WithSources())
Secrets tagged with `conf:"secret"` are redacted.
-### Snapshot for debugging and audits
+
+### Snapshots for Debugging and Audits
```go
snapshot, err := rigging.CreateSnapshot(cfg)
@@ -94,7 +96,8 @@ if err := rigging.WriteSnapshot(snapshot, "snapshots/config-{{timestamp}}.json")
Snapshots include flattened config values, provenance, and secret redaction.
Use `WithExcludeFields(...)` to omit noisy fields when sharing or storing snapshots.
-## 6. Key Mapping Rules (Important)
+
+## 6. Key Mapping Rules
Rigging matches using normalized lowercase key paths.
@@ -127,7 +130,8 @@ If you need to bind a field to a specific environment-style key path, use `env:`
Tag validation and custom validators run during `Load`.
-### Typed transforms (before tag validation)
+
+### Typed Transforms (Before Tag Validation)
Use `WithTransformer(...)` when you need to normalize or derive typed values before tag validation runs.
This is useful for canonicalization such as trimming whitespace or normalizing enum casing.
@@ -142,7 +146,8 @@ loader.WithTransformer(rigging.TransformerFunc[Config](func(ctx context.Context,
Use typed transforms for post-bind value normalization.
For source key aliasing/normalization (for example renaming keys before strict mode), use a custom source wrapper instead.
-### Optional fields (`Optional[T]`)
+
+### Optional Fields (`Optional[T]`)
Use `rigging.Optional[T]` when you need to distinguish "not set" from a zero value (`false`, `0`, `""`).
@@ -163,7 +168,8 @@ if rateLimit, ok := cfg.Features.RateLimit.Get(); ok {
}
```
-### Custom validators (after tag validation)
+
+### Custom Validators (After Tag Validation)
```go
loader.WithValidator(rigging.ValidatorFunc[Config](func(ctx context.Context, cfg *Config) error {
diff --git a/examples/transformer/main.go b/examples/transformer/main.go
index 2db2a21..4bd4187 100644
--- a/examples/transformer/main.go
+++ b/examples/transformer/main.go
@@ -25,12 +25,12 @@ func main() {
loader := rigging.NewLoader[AppConfig]().
WithSource(sourceenv.New(sourceenv.Options{Prefix: "EXTRANS_"})).
- WithTransformer(rigging.TransformerFunc[AppConfig](func(ctx context.Context, cfg *AppConfig) error {
+ WithTransformerFunc(func(ctx context.Context, cfg *AppConfig) error {
// Typed transform: canonicalize values after binding/conversion, before validation.
cfg.Environment = strings.ToLower(strings.TrimSpace(cfg.Environment))
cfg.Region = strings.ToLower(strings.TrimSpace(cfg.Region))
return nil
- }))
+ })
cfg, err := loader.Load(ctx)
if err != nil {
diff --git a/loader.go b/loader.go
index e61f541..1ade5f2 100644
--- a/loader.go
+++ b/loader.go
@@ -54,6 +54,12 @@ func (l *Loader[T]) WithTransformer(t Transformer[T]) *Loader[T] {
return l
}
+// WithTransformerFunc adds a typed transform function (executed after binding and before tag-based validation).
+// This is a convenience wrapper around WithTransformer(TransformerFunc[T](fn)).
+func (l *Loader[T]) WithTransformerFunc(fn func(context.Context, *T) error) *Loader[T] {
+ return l.WithTransformer(TransformerFunc[T](fn))
+}
+
// WithValidator adds a custom validator (executed after transformers and tag-based validation).
func (l *Loader[T]) WithValidator(v Validator[T]) *Loader[T] {
l.validators = append(l.validators, v)
diff --git a/loader_test.go b/loader_test.go
index 06048c6..c98fff3 100644
--- a/loader_test.go
+++ b/loader_test.go
@@ -123,6 +123,40 @@ func TestWithTransformer(t *testing.T) {
}
}
+// TestWithTransformerFunc verifies that WithTransformerFunc wraps and adds a transformer and returns the loader for chaining.
+func TestWithTransformerFunc(t *testing.T) {
+ type Config struct {
+ Value string
+ }
+
+ loader := NewLoader[Config]()
+ called := false
+
+ result := loader.WithTransformerFunc(func(ctx context.Context, cfg *Config) error {
+ called = true
+ cfg.Value = "normalized"
+ return nil
+ })
+ if result != loader {
+ t.Error("WithTransformerFunc should return the same loader instance for chaining")
+ }
+
+ if len(loader.transformers) != 1 {
+ t.Fatalf("expected 1 transformer, got %d", len(loader.transformers))
+ }
+
+ cfg := &Config{}
+ if err := loader.transformers[0].Transform(context.Background(), cfg); err != nil {
+ t.Fatalf("unexpected error calling wrapped transformer: %v", err)
+ }
+ if !called {
+ t.Fatal("expected wrapped transformer function to be called")
+ }
+ if cfg.Value != "normalized" {
+ t.Fatalf("expected transformer to mutate cfg.Value to %q, got %q", "normalized", cfg.Value)
+ }
+}
+
// TestStrict verifies that Strict method sets the strict flag and returns the loader for chaining.
func TestStrict(t *testing.T) {
loader := NewLoader[struct{}]()