Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
10 changes: 10 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions docs/configuration-sources.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<a id="key-normalization-by-source"></a>
## Key Normalization by Source

| Source | Example input | Normalized key |
Expand Down Expand Up @@ -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.

<a id="key-mapping-precedence-required"></a>
## Key Mapping + Precedence + `required` Decision Table

| Situation | Result | Validation outcome |
Expand Down Expand Up @@ -146,6 +148,7 @@ type SourceWithKeys interface {

`originalKeys` lets Rigging report exact source keys in provenance (for example, full env var names).

<a id="watch-reload"></a>
## Watch and Reload

```go
Expand Down
18 changes: 12 additions & 6 deletions docs/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,17 @@ if ok {
}
```

### Redacted dump
<a id="redacted-dump"></a>
### Redacted Dump

```go
rigging.DumpEffective(os.Stdout, cfg, rigging.WithSources())
```

Secrets tagged with `conf:"secret"` are redacted.

### Snapshot for debugging and audits
<a id="snapshots-debugging-audits"></a>
### Snapshots for Debugging and Audits

```go
snapshot, err := rigging.CreateSnapshot(cfg)
Expand All @@ -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)
<a id="key-mapping-rules"></a>
## 6. Key Mapping Rules

Rigging matches using normalized lowercase key paths.

Expand Down Expand Up @@ -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)
<a id="typed-transforms"></a>
### 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.
Expand All @@ -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]`)
<a id="optional-fields"></a>
### Optional Fields (`Optional[T]`)

Use `rigging.Optional[T]` when you need to distinguish "not set" from a zero value (`false`, `0`, `""`).

Expand All @@ -163,7 +168,8 @@ if rateLimit, ok := cfg.Features.RateLimit.Get(); ok {
}
```

### Custom validators (after tag validation)
<a id="custom-validators"></a>
### Custom Validators (After Tag Validation)

```go
loader.WithValidator(rigging.ValidatorFunc[Config](func(ctx context.Context, cfg *Config) error {
Expand Down
4 changes: 2 additions & 2 deletions examples/transformer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
34 changes: 34 additions & 0 deletions loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}]()
Expand Down