The main entry point for loading configuration.
loader := rigging.NewLoader[Config]()Methods:
WithSource(src Source) *Loader[T]- Add a configuration sourceWithTransformer(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 validatorStrict(strict bool) *Loader[T]- Enable/disable strict modeLoad(ctx context.Context) (*T, error)- Load and validate configurationLoadWithProvenance(ctx context.Context) (*T, *Provenance, error)- Load config and return provenance without using global storageWatch(ctx context.Context) (<-chan Snapshot[T], <-chan error, error)- Watch for changes
Interface for configuration sources.
type Source interface {
Load(ctx context.Context) (map[string]any, error)
Watch(ctx context.Context) (<-chan ChangeEvent, error)
Name() string
}Watch may return ErrWatchNotSupported for sources that don't support runtime change events.
Built-in sources:
sourcefile.New(path string, opts sourcefile.Options)- YAML/JSON/TOML filessourceenv.New(opts sourceenv.Options)- Environment variables
sourcefile.Options fields:
Format string- Optional explicit format (yaml,json,toml). When empty, inferred from file extension.Required bool- When true, missing files return an error. When false, missing files return an empty map.Root string- Optional dot-separated subtree path to load and flatten as the source root.SnakeCaseKeys bool- Optional key-shape adaptation. When true, flattened keys are rewritten to underscore snake_case (pollInterval->poll_interval,http.clientTimeout->http_client_timeout).KeyPrefix string- Optional prefix added to each flattened key after optionalSnakeCaseKeysconversion (msa_+poll_interval->msa_poll_interval).
sourcefile.Root example:
src := sourcefile.New("config.yaml", sourcefile.Options{
Root: "root.section",
})
loader := rigging.NewLoader[Config]().WithSource(src)
cfg, err := loader.Load(ctx)
if err != nil {
if errors.Is(err, sourcefile.ErrRootNotFound) ||
errors.Is(err, sourcefile.ErrRootNotMap) ||
errors.Is(err, sourcefile.ErrInvalidRoot) {
// handle root configuration problems
}
return err
}
_ = cfgWhen SnakeCaseKeys is enabled, root.section.pollInterval becomes poll_interval; with KeyPrefix: "msa_", it becomes msa_poll_interval.
Name() is used in provenance output (for example, file:config.yaml, env:APP_PORT).
Optional interface for richer provenance (original source keys).
type SourceWithKeys interface {
Source
LoadWithKeys(ctx context.Context) (data map[string]any, originalKeys map[string]string, err error)
}originalKeys maps normalized keys back to original source keys (for example, full env var names).
Distinguish "not set" from "zero value".
type Optional[T any] struct {
Value T
Set bool
}Methods:
Get() (T, bool)- Returns value and whether it was setOrDefault(defaultVal T) T- Returns value or default
Interface for typed config transforms.
Transformers run after binding/defaults/type conversion and before tag-based validation. Use them for canonicalization (for example trim/lowercase/derive fields), not source key normalization.
type Transformer[T any] interface {
Transform(ctx context.Context, cfg *T) error
}Helper:
TransformerFunc[T](func(ctx context.Context, cfg *T) error)- Function adapter
When registering an inline transform, prefer WithTransformerFunc(...) for a shorter call site:
loader.WithTransformerFunc(func(ctx context.Context, cfg *Config) error {
cfg.Environment = strings.ToLower(strings.TrimSpace(cfg.Environment))
return nil
})Interface for custom validation.
type Validator[T any] interface {
Validate(ctx context.Context, cfg *T) error
}Helper:
ValidatorFunc[T](func(ctx context.Context, cfg *T) error)- Function adapter
Rigging uses a fixed load pipeline during Loader.Load and Loader.Watch reloads:
merge sources -> strict unknown-key check -> bind/type conversion/defaults -> typed transforms -> tag validation -> custom validators
Behavior notes:
- Sources are merged in registration order; later sources override earlier ones.
Strict(true)is the default, and unknown-key checks run before binding.- Binding applies defaults and type conversion before transformers run.
WithTransformer(...)mutates typed config values before tag validation.WithValidator(...)runs after tag validation for cross-field/business checks.- Source/key normalization (for example env key rewriting/aliasing) belongs in sources or source wrappers, not typed transformers.
Rigging distinguishes field presence from Go zero values during Loader.Load.
requiredchecks whether a key was provided by any source (or default), not whether the final value is non-zero.min,max, andoneofrun for fields that are explicitly provided, even when the value is a zero value (for example,0,"",false).- Optional fields that are absent skip value constraints.
Example behavior:
- Absent optional
port int \conf:"min:1"`` -> no validation error. - Provided
port=0->minvalidation error.
Track where configuration values came from.
func GetProvenance[T any](cfg *T) (*Provenance, bool)func ReleaseProvenance[T any](cfg *T)Returns provenance metadata with field-level source information.
ReleaseProvenance removes stored provenance for a config instance.
LoadWithProvenance returns provenance directly and does not populate global storage.
Lifecycle notes:
Loadstores provenance for the returned config pointer untilReleaseProvenanceis called.Watchkeeps global provenance for the most recently emitted snapshot; when a newer snapshot is emitted, the superseded snapshot's provenance is released.- If you need historical provenance across snapshots, persist/copy it in your own state.
type Provenance struct {
Fields []FieldProvenance
}
type FieldProvenance struct {
FieldPath string // e.g., "Database.Host"
KeyPath string // e.g., "database.host"
SourceName string // e.g., "file:config.yaml" or "env:APP_DATABASE__PASSWORD"
Secret bool // true if marked as secret
}Safely dump configuration with secret redaction.
func DumpEffective[T any](w io.Writer, cfg *T, opts ...DumpOption) errorOptions:
WithSources()- Include source attributionAsJSON()- Output as JSON instead of textWithIndent(indent string)- Set JSON indentation
When AsJSON() and WithSources() are combined, leaf fields are wrapped as:
{"value":"...","source":"..."}Examples:
// Text format
rigging.DumpEffective(os.Stdout, cfg)
// With source attribution
rigging.DumpEffective(os.Stdout, cfg, rigging.WithSources())
// JSON format
rigging.DumpEffective(os.Stdout, cfg, rigging.AsJSON())
// JSON with custom indent
rigging.DumpEffective(os.Stdout, cfg,
rigging.AsJSON(),
rigging.WithIndent(" "))Capture configuration state for debugging and auditing.
func CreateSnapshot[T any](cfg *T, opts ...SnapshotOption) (*ConfigSnapshot, error)Creates a point-in-time capture with flattened config, provenance, and automatic secret redaction.
snapshot, err := rigging.CreateSnapshot(cfg)
// snapshot.Config["database.host"] = "localhost"
// snapshot.Config["database.password"] = "***redacted***"Options:
WithExcludeFields(paths ...string)- Exclude specific field paths
snapshot, err := rigging.CreateSnapshot(cfg,
rigging.WithExcludeFields("debug", "internal.metrics"))func WriteSnapshot(snapshot *ConfigSnapshot, pathTemplate string) error
func ReadSnapshot(path string) (*ConfigSnapshot, error)Persist and restore snapshots with atomic writes and {{timestamp}} template support.
// Write with timestamp in filename
err := rigging.WriteSnapshot(snapshot, "snapshots/config-{{timestamp}}.json")
// Creates: snapshots/config-20240115-103000.json
// Read back
restored, err := rigging.ReadSnapshot("snapshots/config-20240115-103000.json")func ExpandPath(template string) string
func ExpandPathWithTime(template string, t time.Time) stringWriteSnapshot uses the snapshot's internal Timestamp when expanding {{timestamp}} to keep filename and snapshot metadata consistent.
type ConfigSnapshot struct {
Version string // Format version ("1.0")
Timestamp time.Time // Creation time
Config map[string]any // Flattened config (secrets redacted)
Provenance []FieldProvenance // Source tracking
}const MaxSnapshotSize = 100 * 1024 * 1024 // 100MB limit
const SnapshotVersion = "1.0"
var ErrSnapshotTooLarge // Snapshot exceeds size limit
var ErrNilConfig // Nil config passed
var ErrUnsupportedVersion // Unknown snapshot versionAggregates all validation failures.
type ValidationError struct {
FieldErrors []FieldError
}Represents a single field validation failure.
type FieldError struct {
FieldPath string // e.g., "Database.Port"
Code string // e.g., "required", "min", "max"
Message string // Human-readable error
}Standard error codes:
required- Field is required but not providedmin- Value below minimummax- Value exceeds maximumoneof- Value not in allowed setinvalid_type- Type conversion failedunknown_key- Configuration key doesn't map to any field (strict mode)
Configure binding and validation with the conf tag:
| Tag | Description | Example |
|---|---|---|
required |
Field must have a value | conf:"required" |
env:VAR |
Bind from an explicit env-style key path (normalized with __ -> . and lowercased) after any sourceenv.Options.Prefix stripping |
conf:"env:DATABASE__HOST" |
default:X |
Default value if not provided | conf:"default:8080" |
min:N |
Minimum value (numeric) or length (string) | conf:"min:1024" |
max:N |
Maximum value (numeric) or length (string) | conf:"max:65535" |
oneof:a,b,c |
Value must be one of the options (duplicates removed, empty values ignored) | conf:"oneof:prod,staging,dev" |
secret |
Mark field for redaction | conf:"secret" |
prefix:path |
Prefix for nested struct fields | conf:"prefix:database" |
name:path |
Override derived key path | conf:"name:custom.path" |
Combining tags:
type Config struct {
Port int `conf:"default:8080,min:1024,max:65535"`
Env string `conf:"required,oneof:prod,staging,dev"`
Password string `conf:"required,secret"`
}Tag precedence:
name:overrides all key derivation (ignoresprefix:and field name)env:is used whenname:is not set; it matches the env-style key after any source prefix stripping- otherwise keys are derived from field names (snake_case), with parent
prefix:for nested structs - env-style normalization converts
__to.and preserves single_
type Config struct {
Database struct {
Host string // Key: database.host (prefix applied)
Port int `conf:"name:db.port"` // Key: db.port (name overrides prefix)
} `conf:"prefix:database"`
}Represents a loaded configuration with metadata.
type Snapshot[T any] struct {
Config *T // The loaded configuration
Version int64 // Incremented on each reload
LoadedAt time.Time // When loaded
Source string // What triggered the load
}Watch behavior:
Watchemits an initial snapshot immediately (Version=1,Source="initial").- Reloads from source change events are debounced by 100ms.
Provenance note:
- For configs emitted by
Watch,GetProvenanceis intended for the latest snapshot. - Older snapshots may no longer have global provenance after subsequent reloads.
Notification of configuration change.
type ChangeEvent struct {
At time.Time // When the change occurred
Cause string // Description of the change
}ErrWatchNotSupported is returned by Source.Watch for sources that don't support watch mode.
Catch typos and deprecated keys:
loader.Strict(true) // Fail on unknown keys (default)
loader.Strict(false) // Ignore unknown keysAll validation errors include field paths and codes:
cfg, err := loader.Load(ctx)
if err != nil {
if valErr, ok := err.(*rigging.ValidationError); ok {
for _, fe := range valErr.FieldErrors {
log.Printf("%s: %s (code: %s)",
fe.FieldPath, fe.Message, fe.Code)
}
}
}