Documentation
¶
Overview ¶
Package rigging provides type-safe configuration management with validation and provenance tracking.
Quick Start:
type Config struct {
Port int `conf:"default:8080,min:1024"`
Host string `conf:"required"`
}
loader := rigging.NewLoader[Config]().
WithSource(sourcefile.New("config.yaml", sourcefile.Options{})).
WithSource(sourceenv.New(sourceenv.Options{Prefix: "APP_"}))
cfg, err := loader.Load(context.Background())
Tag directives: env:VAR, default:val, required, min:N, max:N, oneof:a,b,c, secret, prefix:path, name:path
See example_test.go and README.md for detailed usage.
Example ¶
Example demonstrates basic configuration loading from multiple sources.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/Azhovan/rigging"
"github.com/Azhovan/rigging/sourceenv"
)
func main() {
// Define configuration structure
type Config struct {
Environment string `conf:"default:dev,oneof:prod,staging,dev"`
Port int `conf:"default:8080,min:1024,max:65535"`
Database struct {
Host string `conf:"required"`
Port int `conf:"default:5432"`
User string `conf:"required"`
Password string `conf:"required,secret"`
} `conf:"prefix:database"`
}
// Set up environment variables for this example
os.Setenv("EXAMPLE_DATABASE__HOST", "localhost")
os.Setenv("EXAMPLE_DATABASE__USER", "testuser")
os.Setenv("EXAMPLE_DATABASE__PASSWORD", "testpass")
defer func() {
os.Unsetenv("EXAMPLE_DATABASE__HOST")
os.Unsetenv("EXAMPLE_DATABASE__USER")
os.Unsetenv("EXAMPLE_DATABASE__PASSWORD")
}()
// Create loader with environment source (using prefix to avoid conflicts)
loader := rigging.NewLoader[Config]().
WithSource(sourceenv.New(sourceenv.Options{Prefix: "EXAMPLE_"})).
Strict(true)
// Load configuration
cfg, err := loader.Load(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Printf("Environment: %s\n", cfg.Environment)
fmt.Printf("Port: %d\n", cfg.Port)
fmt.Printf("Database Host: %s\n", cfg.Database.Host)
fmt.Printf("Database User: %s\n", cfg.Database.User)
}
Output: Environment: dev Port: 8080 Database Host: localhost Database User: testuser
Example (EnvCaseSensitive) ¶
Example_envCaseSensitive demonstrates case-sensitive prefix matching.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/Azhovan/rigging"
"github.com/Azhovan/rigging/sourceenv"
)
func main() {
type Config struct {
Host string `conf:"required"`
Port int `conf:"required"`
}
// Set environment variables with different cases
os.Setenv("APP_HOST", "prod.example.com")
os.Setenv("APP_PORT", "8080")
os.Setenv("app_host", "dev.example.com") // lowercase prefix
os.Setenv("app_port", "9090") // lowercase prefix
defer func() {
os.Unsetenv("APP_HOST")
os.Unsetenv("APP_PORT")
os.Unsetenv("app_host")
os.Unsetenv("app_port")
}()
// Case-insensitive (default) - matches all variations
// Both APP_* and app_* are loaded, later ones override
loaderInsensitive := rigging.NewLoader[Config]().
WithSource(sourceenv.New(sourceenv.Options{
Prefix: "APP_",
CaseSensitive: false, // default
}))
cfg, err := loaderInsensitive.Load(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Printf("Case-insensitive: Host=%s, Port=%d\n", cfg.Host, cfg.Port)
// Case-sensitive - only exact match (APP_* only)
loaderSensitive := rigging.NewLoader[Config]().
WithSource(sourceenv.New(sourceenv.Options{
Prefix: "APP_",
CaseSensitive: true,
}))
cfg2, err := loaderSensitive.Load(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Printf("Case-sensitive: Host=%s, Port=%d\n", cfg2.Host, cfg2.Port)
}
Output: Case-insensitive: Host=dev.example.com, Port=9090 Case-sensitive: Host=prod.example.com, Port=8080
Example (PrefixVsName) ¶
Example_prefixVsName demonstrates the difference between prefix and name tags.
- prefix: Adds a prefix to ALL nested fields (e.g., `conf:"prefix:database"` → "database.host", "database.port") - name: Overrides the key for a single field (e.g., `conf:"name:db.user"` → looks for exact key "db.user") - name tag takes precedence over prefix (ignores parent struct prefix)
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/Azhovan/rigging"
"github.com/Azhovan/rigging/sourceenv"
)
func main() {
type DatabaseConfig struct {
Host string `conf:"required"` // With prefix "database": looks for "database.host"
Port int `conf:"default:5432"` // With prefix "database": looks for "database.port"
Username string `conf:"name:db.user,required"` // name tag: looks for exact key "db.user" (ignores prefix!)
}
type ServerConfig struct {
Host string `conf:"required"` // With prefix "server": looks for "server.host"
Port int `conf:"default:8080"` // With prefix "server": looks for "server.port"
}
type Config struct {
AppName string `conf:"name:app.name,default:myapp"` // Looks for "app.name" (not "appname")
Server ServerConfig `conf:"prefix:server"` // Adds "server." prefix to nested fields
Database DatabaseConfig `conf:"prefix:database"` // Adds "database." prefix to nested fields
}
// Set environment variables
os.Setenv("EXPVN_APP__NAME", "testapp") // Matches app.name (__ becomes .)
os.Setenv("EXPVN_SERVER__HOST", "localhost") // Matches Server.Host (with prefix)
os.Setenv("EXPVN_SERVER__PORT", "9000") // Matches Server.Port (with prefix)
os.Setenv("EXPVN_DATABASE__HOST", "db.local") // Matches Database.Host (with prefix)
os.Setenv("EXPVN_DATABASE__PORT", "3306") // Matches Database.Port (with prefix)
os.Setenv("EXPVN_DB__USER", "admin") // Matches Database.Username (name tag ignores prefix)
defer func() {
os.Unsetenv("EXPVN_APP__NAME")
os.Unsetenv("EXPVN_SERVER__HOST")
os.Unsetenv("EXPVN_SERVER__PORT")
os.Unsetenv("EXPVN_DATABASE__HOST")
os.Unsetenv("EXPVN_DATABASE__PORT")
os.Unsetenv("EXPVN_DB__USER")
}()
loader := rigging.NewLoader[Config]().
WithSource(sourceenv.New(sourceenv.Options{Prefix: "EXPVN_"}))
cfg, err := loader.Load(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Printf("AppName: %s\n", cfg.AppName)
fmt.Printf("Server.Host: %s\n", cfg.Server.Host)
fmt.Printf("Server.Port: %d\n", cfg.Server.Port)
fmt.Printf("Database.Host: %s\n", cfg.Database.Host)
fmt.Printf("Database.Port: %d\n", cfg.Database.Port)
fmt.Printf("Database.Username: %s\n", cfg.Database.Username)
}
Output: AppName: testapp Server.Host: localhost Server.Port: 9000 Database.Host: db.local Database.Port: 3306 Database.Username: admin
Example (SnapshotRoundTrip) ¶
Example_snapshotRoundTrip demonstrates the complete snapshot lifecycle: Create → Write → Read, showing round-trip consistency.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/Azhovan/rigging"
"github.com/Azhovan/rigging/sourceenv"
)
func main() {
type Config struct {
Environment string `conf:"default:production"`
Host string `conf:"required"`
Port int `conf:"default:443"`
APIKey string `conf:"required,secret"`
}
os.Setenv("EXRT_HOST", "api.example.com")
os.Setenv("EXRT_APIKEY", "secret-api-key")
defer func() {
os.Unsetenv("EXRT_HOST")
os.Unsetenv("EXRT_APIKEY")
}()
loader := rigging.NewLoader[Config]().
WithSource(sourceenv.New(sourceenv.Options{Prefix: "EXRT_"}))
cfg, err := loader.Load(context.Background())
if err != nil {
log.Fatal(err)
}
// Step 1: Create snapshot
original, err := rigging.CreateSnapshot(cfg)
if err != nil {
log.Fatal(err)
}
fmt.Println("Step 1: Created snapshot")
// Step 2: Write to disk
tmpDir, err := os.MkdirTemp("", "roundtrip-example")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(tmpDir)
path := tmpDir + "/config.json"
if writeErr := rigging.WriteSnapshot(original, path); writeErr != nil {
log.Fatal(writeErr)
}
fmt.Println("Step 2: Wrote snapshot to disk")
// Step 3: Read back from disk
restored, err := rigging.ReadSnapshot(path)
if err != nil {
log.Fatal(err)
}
fmt.Println("Step 3: Read snapshot from disk")
// Verify round-trip consistency
fmt.Printf("Version matches: %v\n", original.Version == restored.Version)
fmt.Printf("Timestamp matches: %v\n", original.Timestamp.Equal(restored.Timestamp))
fmt.Printf("Host matches: %v\n", original.Config["host"] == restored.Config["host"])
fmt.Printf("Secret still redacted: %v\n", restored.Config["apikey"] == "***redacted***")
}
Output: Step 1: Created snapshot Step 2: Wrote snapshot to disk Step 3: Read snapshot from disk Version matches: true Timestamp matches: true Host matches: true Secret still redacted: true
Example (UnderscoreNormalization) ¶
Example_underscoreNormalization demonstrates how underscores in environment variables are normalized to match camelCase field names.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/Azhovan/rigging"
"github.com/Azhovan/rigging/sourceenv"
)
func main() {
type Config struct {
MaxConnections int
APIKey string
}
// All these environment variable formats match the same fields:
// - Single underscores are stripped for flexible matching
// - Double underscores (__) create nested structures
// - Everything is case-insensitive
os.Setenv("EXNORM_MAX_CONNECTIONS", "100") // Underscores → matches MaxConnections
os.Setenv("EXNORM_API_KEY", "secret-key") // Underscores → matches APIKey
defer func() {
os.Unsetenv("EXNORM_MAX_CONNECTIONS")
os.Unsetenv("EXNORM_API_KEY")
}()
loader := rigging.NewLoader[Config]().
WithSource(sourceenv.New(sourceenv.Options{Prefix: "EXNORM_"}))
cfg, err := loader.Load(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Printf("MaxConnections: %d\n", cfg.MaxConnections)
fmt.Printf("APIKey: %s\n", cfg.APIKey)
}
Output: MaxConnections: 100 APIKey: secret-key
Index ¶
- Constants
- Variables
- func DumpEffective[T any](w io.Writer, cfg *T, opts ...DumpOption) error
- func ExpandPath(template string) string
- func ExpandPathWithTime(template string, t time.Time) string
- func WriteSnapshot(snapshot *ConfigSnapshot, pathTemplate string) error
- type ChangeEvent
- type ConfigSnapshot
- type DumpOption
- type FieldError
- type FieldProvenance
- type Loader
- func (l *Loader[T]) Load(ctx context.Context) (*T, error)
- func (l *Loader[T]) Strict(strict bool) *Loader[T]
- func (l *Loader[T]) Watch(ctx context.Context) (<-chan Snapshot[T], <-chan error, error)
- func (l *Loader[T]) WithSource(src Source) *Loader[T]
- func (l *Loader[T]) WithValidator(v Validator[T]) *Loader[T]
- type Optional
- type Provenance
- type Snapshot
- type SnapshotOption
- type Source
- type SourceWithKeys
- type ValidationError
- type Validator
- type ValidatorFunc
Examples ¶
- Package
- Package (EnvCaseSensitive)
- Package (PrefixVsName)
- Package (SnapshotRoundTrip)
- Package (UnderscoreNormalization)
- CreateSnapshot
- CreateSnapshot (WithExclusions)
- DumpEffective
- DumpEffective (AsJSON)
- DumpEffective (WithSources)
- GetProvenance
- Loader.Load
- Loader.Strict
- Loader.Watch
- Loader.WithValidator
- Optional
- Source
- ValidationError
- WriteSnapshot
- WriteSnapshot (ErrorHandling)
Constants ¶
const ( ErrCodeRequired = "required" // Field is required but not provided ErrCodeMin = "min" // Value is below minimum constraint ErrCodeMax = "max" // Value exceeds maximum constraint ErrCodeOneOf = "oneof" // Value is not in the allowed set ErrCodeInvalidType = "invalid_type" // Type conversion failed ErrCodeUnknownKey = "unknown_key" // Configuration key doesn't map to any field (strict mode) )
Error codes for validation failures.
const MaxSnapshotSize = 100 * 1024 * 1024
MaxSnapshotSize is the maximum allowed snapshot size (100MB).
const SnapshotVersion = "1.0"
SnapshotVersion is the current snapshot format version.
Variables ¶
var ( // ErrSnapshotTooLarge is returned when a snapshot exceeds MaxSnapshotSize. ErrSnapshotTooLarge = errors.New("rigging: snapshot exceeds 100MB size limit") // ErrNilConfig is returned when CreateSnapshot receives a nil config. ErrNilConfig = errors.New("rigging: config is nil") // ErrUnsupportedVersion is returned when reading a snapshot with unknown version. ErrUnsupportedVersion = errors.New("rigging: unsupported snapshot version") )
Snapshot errors.
var ErrWatchNotSupported = errors.New("rigging: watch not supported by this source")
ErrWatchNotSupported is returned when watching is not supported.
Functions ¶
func DumpEffective ¶
func DumpEffective[T any](w io.Writer, cfg *T, opts ...DumpOption) error
DumpEffective writes configuration with automatic secret redaction. Supports text or JSON format. Use WithSources(), AsJSON(), WithIndent() options.
Example ¶
ExampleDumpEffective demonstrates dumping configuration with secret redaction.
package main
import (
"context"
"log"
"os"
"time"
"github.com/Azhovan/rigging"
"github.com/Azhovan/rigging/sourceenv"
)
func main() {
type Config struct {
APIKey string `conf:"secret,required"`
Endpoint string `conf:"required"`
Timeout time.Duration `conf:"default:30s"`
}
os.Setenv("EXDMPEFF_APIKEY", "super-secret-key")
os.Setenv("EXDMPEFF_ENDPOINT", "https://api.example.com")
defer func() {
os.Unsetenv("EXDMPEFF_APIKEY")
os.Unsetenv("EXDMPEFF_ENDPOINT")
}()
loader := rigging.NewLoader[Config]().
WithSource(sourceenv.New(sourceenv.Options{Prefix: "EXDMPEFF_"}))
cfg, err := loader.Load(context.Background())
if err != nil {
log.Fatal(err)
}
// Dump configuration (secrets will be redacted)
rigging.DumpEffective(os.Stdout, cfg)
}
Output: apikey: ***redacted*** endpoint: "https://api.example.com" timeout: 30s
Example (AsJSON) ¶
ExampleDumpEffective_asJSON demonstrates JSON output format.
package main
import (
"context"
"log"
"os"
"github.com/Azhovan/rigging"
"github.com/Azhovan/rigging/sourceenv"
)
func main() {
type Config struct {
Environment string `conf:"default:dev"`
Port int `conf:"default:8080"`
}
os.Setenv("EXJSON_ENVIRONMENT", "production")
defer os.Unsetenv("EXJSON_ENVIRONMENT")
loader := rigging.NewLoader[Config]().
WithSource(sourceenv.New(sourceenv.Options{Prefix: "EXJSON_"}))
cfg, err := loader.Load(context.Background())
if err != nil {
log.Fatal(err)
}
// Dump as JSON with source attribution
rigging.DumpEffective(os.Stdout, cfg, rigging.AsJSON(), rigging.WithSources())
}
Output: { "environment": { "source": "env:EXJSON_ENVIRONMENT", "value": "production" }, "port": { "source": "default", "value": 8080 } }
Example (WithSources) ¶
ExampleDumpEffective_withSources demonstrates dumping with source attribution.
package main
import (
"context"
"log"
"os"
"github.com/Azhovan/rigging"
"github.com/Azhovan/rigging/sourceenv"
)
func main() {
type Config struct {
Port int `conf:"default:8080"`
Host string `conf:"default:localhost"`
}
os.Setenv("EXDUMP_PORT", "9090")
defer os.Unsetenv("EXDUMP_PORT")
loader := rigging.NewLoader[Config]().
WithSource(sourceenv.New(sourceenv.Options{Prefix: "EXDUMP_"}))
cfg, err := loader.Load(context.Background())
if err != nil {
log.Fatal(err)
}
// Dump with source information
rigging.DumpEffective(os.Stdout, cfg, rigging.WithSources())
}
Output: port: 9090 (source: env:EXDUMP_PORT) host: "localhost" (source: default)
func ExpandPath ¶ added in v0.4.6
ExpandPath expands template variables using current time. For consistency with snapshot metadata, prefer WriteSnapshot which uses the snapshot's internal timestamp for expansion.
func ExpandPathWithTime ¶ added in v0.4.6
ExpandPathWithTime expands template variables using the provided timestamp. Replaces all {{timestamp}} occurrences with the time formatted as 20060102-150405. Returns the path unchanged if no template variables are present.
func WriteSnapshot ¶ added in v0.4.6
func WriteSnapshot(snapshot *ConfigSnapshot, pathTemplate string) error
WriteSnapshot persists a snapshot to disk with atomic write semantics. Supports {{timestamp}} template variable in path - uses snapshot.Timestamp (not current time) to ensure filename matches internal metadata. Returns ErrSnapshotTooLarge if serialized size exceeds 100MB.
Example ¶
ExampleWriteSnapshot demonstrates persisting a snapshot to disk. The path supports {{timestamp}} template variable which is expanded using the snapshot's internal timestamp for consistency.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/Azhovan/rigging"
"github.com/Azhovan/rigging/sourceenv"
)
func main() {
type Config struct {
Host string `conf:"required"`
Port int `conf:"default:8080"`
}
os.Setenv("EXWRITE_HOST", "localhost")
defer os.Unsetenv("EXWRITE_HOST")
loader := rigging.NewLoader[Config]().
WithSource(sourceenv.New(sourceenv.Options{Prefix: "EXWRITE_"}))
cfg, err := loader.Load(context.Background())
if err != nil {
log.Fatal(err)
}
// Create a snapshot
snapshot, err := rigging.CreateSnapshot(cfg)
if err != nil {
log.Fatal(err)
}
// Create a temp directory for the example
tmpDir, err := os.MkdirTemp("", "snapshot-example")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(tmpDir)
// Write snapshot with timestamp template in path
// The {{timestamp}} is replaced with the snapshot's timestamp (e.g., 20240115-103000)
path := tmpDir + "/config-{{timestamp}}.json"
err = rigging.WriteSnapshot(snapshot, path)
if err != nil {
log.Fatal(err)
}
// Verify the file was created
expectedPath := rigging.ExpandPathWithTime(path, snapshot.Timestamp)
if _, statErr := os.Stat(expectedPath); statErr == nil {
fmt.Println("Snapshot written successfully")
}
// Read the file to show it's valid JSON
readSnapshot, err := rigging.ReadSnapshot(expectedPath)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Read back version: %s\n", readSnapshot.Version)
fmt.Printf("Read back host: %s\n", readSnapshot.Config["host"])
}
Output: Snapshot written successfully Read back version: 1.0 Read back host: localhost
Example (ErrorHandling) ¶
ExampleWriteSnapshot_errorHandling demonstrates error handling when writing snapshots.
package main
import (
"fmt"
"github.com/Azhovan/rigging"
)
func main() {
// Attempting to write a nil snapshot returns an error
err := rigging.WriteSnapshot(nil, "/tmp/snapshot.json")
if err != nil {
fmt.Printf("Error: %v\n", err)
}
}
Output: Error: rigging: config is nil
Types ¶
type ChangeEvent ¶
ChangeEvent notifies of configuration changes.
type ConfigSnapshot ¶ added in v0.4.6
type ConfigSnapshot struct {
// Version is the snapshot format version (currently "1.0")
Version string `json:"version"`
// Timestamp is when the snapshot was created
Timestamp time.Time `json:"timestamp"`
// Config contains flattened configuration values with secrets redacted.
// Keys are dot-notation paths (e.g., "database.host").
Config map[string]any `json:"config"`
// Provenance tracks the source of each configuration field.
Provenance []FieldProvenance `json:"provenance"`
}
ConfigSnapshot represents a point-in-time configuration capture.
func CreateSnapshot ¶ added in v0.4.6
func CreateSnapshot[T any](cfg *T, opts ...SnapshotOption) (*ConfigSnapshot, error)
CreateSnapshot captures the current configuration state. Returns a snapshot with flattened config, provenance, and metadata. Secrets are automatically redacted using existing provenance data. The snapshot's Timestamp is captured at creation time.
Example ¶
ExampleCreateSnapshot demonstrates creating a configuration snapshot. Snapshots capture the effective configuration state at a point in time, including provenance information and automatic secret redaction.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/Azhovan/rigging"
"github.com/Azhovan/rigging/sourceenv"
)
func main() {
type Config struct {
Host string `conf:"required"`
Port int `conf:"default:8080"`
APIKey string `conf:"required,secret"`
LogLevel string `conf:"default:info"`
}
os.Setenv("EXSNAP_HOST", "api.example.com")
os.Setenv("EXSNAP_APIKEY", "super-secret-key-12345")
defer func() {
os.Unsetenv("EXSNAP_HOST")
os.Unsetenv("EXSNAP_APIKEY")
}()
loader := rigging.NewLoader[Config]().
WithSource(sourceenv.New(sourceenv.Options{Prefix: "EXSNAP_"}))
cfg, err := loader.Load(context.Background())
if err != nil {
log.Fatal(err)
}
// Create a snapshot of the loaded configuration
snapshot, err := rigging.CreateSnapshot(cfg)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Snapshot version: %s\n", snapshot.Version)
fmt.Printf("Host: %s\n", snapshot.Config["host"])
fmt.Printf("Port: %v\n", snapshot.Config["port"])
fmt.Printf("APIKey: %s\n", snapshot.Config["apikey"]) // Secret is redacted
fmt.Printf("LogLevel: %s\n", snapshot.Config["loglevel"])
}
Output: Snapshot version: 1.0 Host: api.example.com Port: 8080 APIKey: ***redacted*** LogLevel: info
Example (WithExclusions) ¶
ExampleCreateSnapshot_withExclusions demonstrates excluding specific fields from a snapshot using WithExcludeFields.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/Azhovan/rigging"
"github.com/Azhovan/rigging/sourceenv"
)
func main() {
type Config struct {
Host string `conf:"required"`
Port int `conf:"default:8080"`
Debug bool `conf:"default:false"`
Internal string `conf:"default:internal-value"`
}
os.Setenv("EXSNAPEX_HOST", "localhost")
defer os.Unsetenv("EXSNAPEX_HOST")
loader := rigging.NewLoader[Config]().
WithSource(sourceenv.New(sourceenv.Options{Prefix: "EXSNAPEX_"}))
cfg, err := loader.Load(context.Background())
if err != nil {
log.Fatal(err)
}
// Create snapshot excluding specific fields
snapshot, err := rigging.CreateSnapshot(cfg,
rigging.WithExcludeFields("debug", "internal"),
)
if err != nil {
log.Fatal(err)
}
// Print all keys in the snapshot config
fmt.Printf("Host present: %v\n", snapshot.Config["host"] != nil)
fmt.Printf("Port present: %v\n", snapshot.Config["port"] != nil)
fmt.Printf("Debug present: %v\n", snapshot.Config["debug"] != nil)
fmt.Printf("Internal present: %v\n", snapshot.Config["internal"] != nil)
}
Output: Host present: true Port present: true Debug present: false Internal present: false
func ReadSnapshot ¶ added in v0.4.6
func ReadSnapshot(path string) (*ConfigSnapshot, error)
ReadSnapshot loads a snapshot from disk. Returns ErrUnsupportedVersion if snapshot version is not supported. Returns appropriate errors for missing file or invalid JSON.
type DumpOption ¶
type DumpOption func(*dumpConfig)
DumpOption configures dump behavior.
func AsJSON ¶
func AsJSON() DumpOption
AsJSON outputs configuration as JSON. Secrets are still redacted.
func WithIndent ¶
func WithIndent(indent string) DumpOption
WithIndent sets JSON indentation (default: " "). No effect for text output.
type FieldError ¶
type FieldError struct {
FieldPath string // Dot notation (e.g., "Database.Host")
Code string // Error code (e.g., "required", "min")
Message string // Human-readable description
}
FieldError represents a single field validation failure.
type FieldProvenance ¶
type FieldProvenance struct {
FieldPath string // Dot notation (e.g., "Database.Host")
KeyPath string // Normalized key (e.g., "database.host")
SourceName string // Source identifier (e.g., "env:APP_PORT")
Secret bool // Whether field is secret
}
FieldProvenance describes where a field's value came from.
type Loader ¶
type Loader[T any] struct { // contains filtered or unexported fields }
Loader loads and validates configuration from multiple sources. Sources are processed in order (later override earlier). Supports tag-based and custom validation. Thread-safe for reads, not for concurrent configuration changes.
func (*Loader[T]) Load ¶
Load loads, merges, binds, and validates configuration from all sources. Returns populated config or ValidationError with all field errors.
Example ¶
ExampleLoader_Load demonstrates loading configuration with validation.
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/Azhovan/rigging"
"github.com/Azhovan/rigging/sourceenv"
)
func main() {
type Config struct {
APIKey string `conf:"required,secret"`
Timeout time.Duration `conf:"default:30s"`
MaxRetries int `conf:"default:3,min:1,max:10"`
}
os.Setenv("EXLOAD_APIKEY", "test-key-12345")
defer os.Unsetenv("EXLOAD_APIKEY")
loader := rigging.NewLoader[Config]().
WithSource(sourceenv.New(sourceenv.Options{Prefix: "EXLOAD_"}))
cfg, err := loader.Load(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Printf("Timeout: %v\n", cfg.Timeout)
fmt.Printf("MaxRetries: %d\n", cfg.MaxRetries)
fmt.Printf("APIKey: %s\n", cfg.APIKey)
}
Output: Timeout: 30s MaxRetries: 3 APIKey: test-key-12345
func (*Loader[T]) Strict ¶
Strict controls whether unknown keys cause errors. Default: true.
Example ¶
ExampleLoader_Strict demonstrates strict mode behavior.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/Azhovan/rigging"
"github.com/Azhovan/rigging/sourceenv"
)
func main() {
type Config struct {
Host string `conf:"required"`
Port int `conf:"default:8080"`
}
// Set an unknown configuration key
os.Setenv("EXSTRICT_HOST", "localhost")
os.Setenv("EXSTRICT_UNKNOWNKEY", "some-value")
defer func() {
os.Unsetenv("EXSTRICT_HOST")
os.Unsetenv("EXSTRICT_UNKNOWNKEY")
}()
// Strict mode enabled (default)
loader := rigging.NewLoader[Config]().
WithSource(sourceenv.New(sourceenv.Options{Prefix: "EXSTRICT_"})).
Strict(true)
_, err := loader.Load(context.Background())
if err != nil {
fmt.Printf("Strict mode error detected\n")
}
// Strict mode disabled
loader = rigging.NewLoader[Config]().
WithSource(sourceenv.New(sourceenv.Options{Prefix: "EXSTRICT_"})).
Strict(false)
cfg, err := loader.Load(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Printf("Host: %s (strict mode disabled)\n", cfg.Host)
}
Output: Strict mode error detected Host: localhost (strict mode disabled)
func (*Loader[T]) Watch ¶
Watch monitors sources for changes and auto-reloads configuration. Returns: snapshots channel, errors channel, initial load error. Changes are debounced (100ms). Built-in sources don't support watching yet.
Example ¶
ExampleLoader_Watch demonstrates configuration watching. Built-in sources (sourceenv, sourcefile) don't support watching yet. Custom sources can implement Watch() to enable hot-reload.
package main
import (
"context"
"fmt"
"github.com/Azhovan/rigging"
)
// staticSource is a custom source that provides static configuration.
// This demonstrates how to implement the Source interface.
type staticSource struct {
data map[string]any
}
// Load implements the Source interface for staticSource.
func (s *staticSource) Load(ctx context.Context) (map[string]any, error) {
return s.data, nil
}
// Watch implements the Source interface for staticSource.
func (s *staticSource) Watch(ctx context.Context) (<-chan rigging.ChangeEvent, error) {
return nil, rigging.ErrWatchNotSupported
}
// Name implements the Source interface for staticSource.
func (s *staticSource) Name() string {
return "static"
}
func main() {
type Config struct {
Host string `conf:"required"`
Port int `conf:"default:8080"`
}
source := &staticSource{
data: map[string]any{
"host": "localhost",
"port": 8080,
},
}
loader := rigging.NewLoader[Config]().
WithSource(source)
// Watch starts monitoring and returns channels
snapshots, errors, err := loader.Watch(context.Background())
if err != nil {
fmt.Printf("Watch failed: %v\n", err)
return
}
// Receive initial snapshot
snapshot := <-snapshots
fmt.Printf("Initial config loaded (version %d)\n", snapshot.Version)
// In a real application, you would monitor snapshots and errors
// in a goroutine for configuration updates
_ = errors
}
Output: Initial config loaded (version 1)
func (*Loader[T]) WithSource ¶
WithSource adds a source. Sources are processed in order (later override earlier).
func (*Loader[T]) WithValidator ¶
WithValidator adds a custom validator (executed after tag-based validation).
Example ¶
ExampleLoader_WithValidator demonstrates custom validation.
package main
import (
"context"
"fmt"
"log"
"github.com/Azhovan/rigging"
"github.com/Azhovan/rigging/sourceenv"
)
func main() {
type Config struct {
Environment string `conf:"default:dev"`
DebugMode bool `conf:"default:false"`
}
loader := rigging.NewLoader[Config]().
WithSource(sourceenv.New(sourceenv.Options{Prefix: "EXVAL_"})).
WithValidator(rigging.ValidatorFunc[Config](func(ctx context.Context, cfg *Config) error {
// Cross-field validation: debug mode not allowed in production
if cfg.Environment == "prod" && cfg.DebugMode {
return &rigging.ValidationError{
FieldErrors: []rigging.FieldError{{
FieldPath: "DebugMode",
Code: "invalid_prod_debug",
Message: "debug mode cannot be enabled in production",
}},
}
}
return nil
}))
cfg, err := loader.Load(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Printf("Environment: %s\n", cfg.Environment)
fmt.Printf("DebugMode: %t\n", cfg.DebugMode)
}
Output: Environment: dev DebugMode: false
type Optional ¶
Optional distinguishes "not set" from "zero value".
Example ¶
ExampleOptional demonstrates using Optional fields.
package main
import (
"fmt"
"time"
"github.com/Azhovan/rigging"
)
func main() {
type Config struct {
Timeout rigging.Optional[time.Duration]
MaxRetries rigging.Optional[int]
}
cfg := &Config{}
// Set Timeout but not MaxRetries
cfg.Timeout = rigging.Optional[time.Duration]{
Value: 30 * time.Second,
Set: true,
}
// Check if Timeout was set
if timeout, ok := cfg.Timeout.Get(); ok {
fmt.Printf("Timeout is set to: %v\n", timeout)
}
// Check if MaxRetries was set
if _, ok := cfg.MaxRetries.Get(); !ok {
fmt.Println("MaxRetries was not set")
}
// Use OrDefault for fallback values
maxRetries := cfg.MaxRetries.OrDefault(3)
fmt.Printf("MaxRetries (with default): %d\n", maxRetries)
}
Output: Timeout is set to: 30s MaxRetries was not set MaxRetries (with default): 3
type Provenance ¶
type Provenance struct {
Fields []FieldProvenance
}
Provenance contains source information for configuration fields.
func GetProvenance ¶
func GetProvenance[T any](cfg *T) (*Provenance, bool)
GetProvenance returns provenance metadata for a loaded configuration. Thread-safe.
Example ¶
ExampleGetProvenance demonstrates querying configuration provenance.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/Azhovan/rigging"
"github.com/Azhovan/rigging/sourceenv"
)
func main() {
type Config struct {
Host string `conf:"required"`
Port int `conf:"default:8080"`
}
os.Setenv("EXPROV_HOST", "example.com")
defer os.Unsetenv("EXPROV_HOST")
loader := rigging.NewLoader[Config]().
WithSource(sourceenv.New(sourceenv.Options{Prefix: "EXPROV_"}))
cfg, err := loader.Load(context.Background())
if err != nil {
log.Fatal(err)
}
// Query provenance
prov, ok := rigging.GetProvenance(cfg)
if ok {
for _, field := range prov.Fields {
fmt.Printf("%s from %s\n", field.FieldPath, field.SourceName)
}
}
}
Output: Host from env:EXPROV_HOST Port from default
type Snapshot ¶
type Snapshot[T any] struct { Config *T Version int64 // Increments on reload (starts at 1) LoadedAt time.Time Source string // What triggered the load }
Snapshot represents a configuration version emitted by Watch().
type SnapshotOption ¶ added in v0.4.6
type SnapshotOption func(*snapshotConfig)
SnapshotOption configures snapshot creation behavior.
func WithExcludeFields ¶ added in v0.4.6
func WithExcludeFields(paths ...string) SnapshotOption
WithExcludeFields excludes specified field paths from the snapshot. Paths use dot notation (e.g., "database.password", "cache.redis.url").
type Source ¶
type Source interface {
// Load returns configuration as a flat map. Missing optional sources should return empty map.
Load(ctx context.Context) (map[string]any, error)
// Watch emits ChangeEvent when configuration changes. Returns ErrWatchNotSupported if not supported.
Watch(ctx context.Context) (<-chan ChangeEvent, error)
// Name returns a human-readable identifier for this source (e.g., "env:API_", "file:config.yaml").
Name() string
}
Source provides configuration data from backends (env vars, files, remote stores). Keys must be normalized to lowercase dot-separated paths (e.g., "database.host").
Example ¶
ExampleSource demonstrates implementing a custom source.
package main
import (
"context"
"fmt"
"log"
"github.com/Azhovan/rigging"
)
// staticSource is a custom source that provides static configuration.
// This demonstrates how to implement the Source interface.
type staticSource struct {
data map[string]any
}
// Load implements the Source interface for staticSource.
func (s *staticSource) Load(ctx context.Context) (map[string]any, error) {
return s.data, nil
}
// Watch implements the Source interface for staticSource.
func (s *staticSource) Watch(ctx context.Context) (<-chan rigging.ChangeEvent, error) {
return nil, rigging.ErrWatchNotSupported
}
// Name implements the Source interface for staticSource.
func (s *staticSource) Name() string {
return "static"
}
func main() {
// Create a custom source with static data
source := &staticSource{
data: map[string]any{
"host": "localhost",
"port": 8080,
},
}
type Config struct {
Host string `conf:"required"`
Port int `conf:"required"`
}
loader := rigging.NewLoader[Config]().
WithSource(source)
cfg, err := loader.Load(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Printf("Host: %s, Port: %d\n", cfg.Host, cfg.Port)
}
Output: Host: localhost, Port: 8080
type SourceWithKeys ¶ added in v0.4.2
type SourceWithKeys interface {
Source
// LoadWithKeys returns configuration with original keys mapped to normalized keys.
// The returned map has normalized keys, and originalKeys maps normalized -> original.
LoadWithKeys(ctx context.Context) (data map[string]any, originalKeys map[string]string, err error)
}
SourceWithKeys is an optional interface that sources can implement to provide original key information for better provenance tracking.
type ValidationError ¶
type ValidationError struct {
FieldErrors []FieldError
}
ValidationError aggregates field-level validation failures.
Example ¶
ExampleValidationError demonstrates handling validation errors.
package main
import (
"context"
"fmt"
"os"
"github.com/Azhovan/rigging"
"github.com/Azhovan/rigging/sourceenv"
)
func main() {
type Config struct {
Port int `conf:"required,min:1024,max:65535"`
Env string `conf:"required,oneof:prod,staging,dev"`
}
// Set invalid values
os.Setenv("EXVERR_PORT", "80") // Below minimum
os.Setenv("EXVERR_ENV", "production") // Not in oneof list
defer func() {
os.Unsetenv("EXVERR_PORT")
os.Unsetenv("EXVERR_ENV")
}()
loader := rigging.NewLoader[Config]().
WithSource(sourceenv.New(sourceenv.Options{Prefix: "EXVERR_"}))
_, err := loader.Load(context.Background())
if err != nil {
if valErr, ok := err.(*rigging.ValidationError); ok {
fmt.Printf("Validation failed with %d errors\n", len(valErr.FieldErrors))
}
}
}
Output: Validation failed with 2 errors
func (*ValidationError) Error ¶
func (e *ValidationError) Error() string
Error formats validation errors as a multi-line message.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
basic
command
|
|
|
internal
|
|
|
Package sourceenv loads configuration from environment variables.
|
Package sourceenv loads configuration from environment variables. |
|
Package sourcefile loads configuration from YAML, JSON, or TOML files.
|
Package sourcefile loads configuration from YAML, JSON, or TOML files. |