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{}]()