e4.0.0 Refactored, file watcher and improved builder, doc update

This commit is contained in:
2025-07-17 03:44:08 -04:00
parent 16dc829fd5
commit 2934ea9548
25 changed files with 3567 additions and 1828 deletions

376
doc/access.md Normal file
View File

@ -0,0 +1,376 @@
# Access Patterns
This guide covers all methods for getting and setting configuration values, type conversions, and working with structured data.
**Always Register First**: Register paths before setting values
**Use Type Assertions**: After struct registration, types are guaranteed
## Getting Values
### Basic Get
```go
// Get returns (value, exists)
value, exists := cfg.Get("server.port")
if !exists {
log.Fatal("server.port not configured")
}
// Type assertion (safe after registration)
port := value.(int64)
```
### Type-Safe Access
When using struct registration, types are guaranteed:
```go
type Config struct {
Server struct {
Port int64 `toml:"port"`
Host string `toml:"host"`
} `toml:"server"`
}
cfg.RegisterStruct("", &Config{})
// After registration, type assertions are safe
port, _ := cfg.Get("server.port")
portNum := port.(int64) // Won't panic - type is enforced
```
### Get from Specific Source
```go
// Get value from specific source
envPort, exists := cfg.GetSource("server.port", config.SourceEnv)
if exists {
log.Printf("Port from environment: %v", envPort)
}
// Check all sources
sources := cfg.GetSources("server.port")
for source, value := range sources {
log.Printf("%s: %v", source, value)
}
```
### Struct Scanning
```go
// Scan into struct
var serverConfig struct {
Host string `toml:"host"`
Port int64 `toml:"port"`
TLS struct {
Enabled bool `toml:"enabled"`
Cert string `toml:"cert"`
} `toml:"tls"`
}
if err := cfg.Scan("server", &serverConfig); err != nil {
log.Fatal(err)
}
// Use structured data
log.Printf("Server: %s:%d", serverConfig.Host, serverConfig.Port)
```
### Target Population
```go
// Populate entire config struct
var config AppConfig
if err := cfg.Target(&config); err != nil {
log.Fatal(err)
}
// Or with builder pattern
var config AppConfig
cfg, _ := config.NewBuilder().
WithTarget(&config).
Build()
// Access directly
fmt.Println(config.Server.Port)
```
### Type-Aware Mode
```go
var conf AppConfig
cfg, _ := config.NewBuilder().
WithTarget(&conf).
Build()
// Get updated struct anytime
latest, err := cfg.AsStruct()
if err != nil {
log.Fatal(err)
}
appConfig := latest.(*AppConfig)
```
## Setting Values
### Basic Set
```go
// Set updates the highest priority source (default: CLI)
if err := cfg.Set("server.port", int64(9090)); err != nil {
log.Fatal(err) // Error if path not registered
}
```
### Set in Specific Source
```go
// Set value in specific source
cfg.SetSource("server.port", config.SourceEnv, "8080")
cfg.SetSource("debug", config.SourceCLI, true)
// File source typically set via LoadFile, but can be manual
cfg.SetSource("feature.enabled", config.SourceFile, true)
```
### Batch Updates
```go
// Multiple updates
updates := map[string]interface{}{
"server.port": int64(9090),
"server.host": "0.0.0.0",
"database.maxconns": int64(50),
}
for path, value := range updates {
if err := cfg.Set(path, value); err != nil {
log.Printf("Failed to set %s: %v", path, err)
}
}
```
## Type Conversions
The package uses mapstructure for flexible type conversion:
```go
// These all work for a string field
cfg.Set("name", "value") // Direct string
cfg.Set("name", 123) // Number → "123"
cfg.Set("name", true) // Boolean → "true"
// For int64 fields
cfg.Set("port", int64(8080)) // Direct
cfg.Set("port", "8080") // String → int64
cfg.Set("port", 8080.0) // Float → int64
cfg.Set("port", int(8080)) // int → int64
```
### Duration Handling
```go
type Config struct {
Timeout time.Duration `toml:"timeout"`
}
// All these work
cfg.Set("timeout", 30*time.Second) // Direct duration
cfg.Set("timeout", "30s") // String parsing
cfg.Set("timeout", "5m30s") // Complex duration
```
### Network Types
```go
type Config struct {
IP net.IP `toml:"ip"`
CIDR net.IPNet `toml:"cidr"`
URL url.URL `toml:"url"`
}
// Automatic parsing
cfg.Set("ip", "192.168.1.1")
cfg.Set("cidr", "10.0.0.0/8")
cfg.Set("url", "https://example.com:8080/path")
```
### Slice Handling
```go
type Config struct {
Tags []string `toml:"tags"`
Ports []int `toml:"ports"`
}
// Direct slice
cfg.Set("tags", []string{"prod", "stable"})
// Comma-separated string (from env/CLI)
cfg.Set("tags", "prod,stable,v2")
// Number arrays
cfg.Set("ports", []int{8080, 8081, 8082})
```
## Checking Configuration
### Path Registration
```go
// Check if path is registered
if _, exists := cfg.Get("server.port"); !exists {
log.Fatal("server.port not registered")
}
// Get all registered paths
paths := cfg.GetRegisteredPaths("server.")
for path := range paths {
log.Printf("Registered: %s", path)
}
// With default values
defaults := cfg.GetRegisteredPathsWithDefaults("")
for path, defaultVal := range defaults {
log.Printf("%s = %v (default)", path, defaultVal)
}
```
### Validation
```go
// Check required fields
if err := cfg.Validate("api.key", "database.url"); err != nil {
log.Fatal("Missing required config:", err)
}
// Custom validation
requiredPorts := []string{"server.port", "metrics.port"}
for _, path := range requiredPorts {
if val, exists := cfg.Get(path); exists {
if port := val.(int64); port < 1024 {
log.Fatalf("%s must be >= 1024", path)
}
}
}
```
### Source Inspection
```go
// Debug specific value
path := "server.port"
log.Printf("=== %s ===", path)
log.Printf("Current: %v", cfg.Get(path))
sources := cfg.GetSources(path)
for source, value := range sources {
log.Printf(" %s: %v", source, value)
}
```
## Advanced Patterns
### Dynamic Configuration
```go
// Change configuration at runtime
func updatePort(cfg *config.Config, port int64) error {
if port < 1 || port > 65535 {
return fmt.Errorf("invalid port: %d", port)
}
return cfg.Set("server.port", port)
}
```
### Configuration Facade
```go
type ConfigFacade struct {
cfg *config.Config
}
func (f *ConfigFacade) ServerPort() int64 {
val, _ := f.cfg.Get("server.port")
return val.(int64)
}
func (f *ConfigFacade) SetServerPort(port int64) error {
return f.cfg.Set("server.port", port)
}
func (f *ConfigFacade) DatabaseURL() string {
val, _ := f.cfg.Get("database.url")
return val.(string)
}
```
### Default Fallbacks
```go
// Helper for optional configuration
func getOrDefault(cfg *config.Config, path string, defaultVal interface{}) interface{} {
if val, exists := cfg.Get(path); exists {
return val
}
return defaultVal
}
// Usage
timeout := getOrDefault(cfg, "timeout", 30*time.Second).(time.Duration)
```
## Thread Safety
All access methods are thread-safe:
```go
// Safe concurrent access
var wg sync.WaitGroup
// Multiple readers
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
port, _ := cfg.Get("server.port")
log.Printf("Port: %v", port)
}()
}
// Concurrent writes are safe too
wg.Add(1)
go func() {
defer wg.Done()
cfg.Set("counter", atomic.AddInt64(&counter, 1))
}()
wg.Wait()
```
## Debugging
### View All Configuration
```go
// Debug output
fmt.Println(cfg.Debug())
// Dump as TOML
cfg.Dump() // Writes to stdout
```
### Clone for Testing
```go
// Create isolated copy for testing
testCfg := cfg.Clone()
testCfg.Set("server.port", int64(0)) // Random port for tests
```
## See Also
- [Live Reconfiguration](reconfiguration.md) - Reacting to changes
- [Builder Pattern](builder.md) - Type-aware configuration
- [Environment Variables](env.md) - Environment value access

295
doc/builder.md Normal file
View File

@ -0,0 +1,295 @@
# Builder Pattern
The builder pattern provides fine-grained control over configuration initialization and loading behavior.
## Basic Builder Usage
```go
cfg, err := config.NewBuilder().
WithDefaults(defaultStruct).
WithEnvPrefix("MYAPP_").
WithFile("config.toml").
Build()
```
## Builder Methods
### WithDefaults
Register a struct containing default values:
```go
type Config struct {
Host string `toml:"host"`
Port int `toml:"port"`
}
defaults := &Config{
Host: "localhost",
Port: 8080,
}
cfg, _ := config.NewBuilder().
WithDefaults(defaults).
Build()
```
### WithTarget
Enable type-aware mode with automatic struct population:
```go
var appConfig Config
cfg, _ := config.NewBuilder().
WithTarget(&appConfig). // Registers struct and enables AsStruct()
WithFile("config.toml").
Build()
// Access populated struct
populated, _ := cfg.AsStruct()
config := populated.(*Config)
```
### WithTagName
Use different struct tags for field mapping:
```go
type Config struct {
Server struct {
Host string `json:"host"` // Using JSON tags
Port int `json:"port"`
} `json:"server"`
}
cfg, _ := config.NewBuilder().
WithDefaults(&Config{}).
WithTagName("json"). // Use json tags instead of toml
Build()
```
Supported tag names: `toml` (default), `json`, `yaml`
### WithPrefix
Add a prefix to all registered paths:
```go
cfg, _ := config.NewBuilder().
WithDefaults(serverConfig).
WithPrefix("server"). // All paths prefixed with "server."
Build()
// Access as "server.host" instead of just "host"
host, _ := cfg.Get("server.host")
```
### WithEnvPrefix
Set environment variable prefix:
```go
cfg, err := config.NewBuilder().
WithEnvPrefix("MYAPP_").
Build()
// Reads from MYAPP_SERVER_PORT for "server.port"
```
### WithSources
Configure source precedence order:
```go
// Environment variables take highest priority
cfg, _ := config.NewBuilder().
WithSources(
config.SourceEnv,
config.SourceFile,
config.SourceCLI,
config.SourceDefault,
).
Build()
```
### WithEnvTransform
Custom environment variable name mapping:
```go
cfg, _ := config.NewBuilder().
WithEnvTransform(func(path string) string {
// Custom mapping logic
switch path {
case "server.port":
return "PORT" // Use $PORT instead of $MYAPP_SERVER_PORT
case "database.url":
return "DATABASE_URL"
default:
// Default transformation
return "MYAPP_" + strings.ToUpper(
strings.ReplaceAll(path, ".", "_"),
)
}
}).
Build()
```
### WithEnvWhitelist
Limit which configuration paths check environment variables:
```go
cfg, _ := config.NewBuilder().
WithEnvWhitelist(
"server.port",
"database.url",
"api.key",
). // Only these paths read from env
Build()
```
### WithValidator
Add validation functions that run after loading:
```go
cfg, _ := config.NewBuilder().
WithDefaults(defaults).
WithValidator(func(c *config.Config) error {
// Validate port range
port, _ := c.Get("server.port")
if p := port.(int64); p < 1024 || p > 65535 {
return fmt.Errorf("port must be between 1024-65535")
}
return nil
}).
WithValidator(func(c *config.Config) error {
// Validate required fields
return c.Validate("api.key", "database.url")
}).
Build()
```
### WithFile
Set configuration file path:
```go
cfg, _ := config.NewBuilder().
WithFile("/etc/myapp/config.toml").
Build()
```
### WithArgs
Override command-line arguments (default is os.Args[1:]):
```go
cfg, _ := config.NewBuilder().
WithArgs([]string{"--debug", "--server.port=9090"}).
Build()
```
### WithFileDiscovery
Enable automatic configuration file discovery:
```go
cfg, _ := config.NewBuilder().
WithFileDiscovery(config.FileDiscoveryOptions{
Name: "myapp",
Extensions: []string{".toml", ".conf"},
EnvVar: "MYAPP_CONFIG",
CLIFlag: "--config",
UseXDG: true,
}).
Build()
```
This searches for configuration files in:
1. Path specified by `--config` flag
2. Path in `$MYAPP_CONFIG` environment variable
3. Current directory
4. XDG config directories (`~/.config/myapp/`, `/etc/myapp/`)
## Advanced Patterns
### Type-Safe Configuration Access
```go
type AppConfig struct {
Server ServerConfig `toml:"server"`
DB DBConfig `toml:"database"`
}
var conf AppConfig
cfg, _ := config.NewBuilder().
WithTarget(&conf).
WithFile("config.toml").
Build()
// Direct struct access after building
fmt.Printf("Port: %d\n", conf.Server.Port)
// Or get updated struct anytime
latest, _ := cfg.AsStruct()
appConf := latest.(*AppConfig)
```
### Multi-Stage Validation
```go
cfg, err := config.NewBuilder().
WithDefaults(defaults).
// Stage 1: Validate structure
WithValidator(validateStructure).
// Stage 2: Validate values
WithValidator(validateRanges).
// Stage 3: Validate relationships
WithValidator(validateRelationships).
Build()
func validateStructure(c *config.Config) error {
required := []string{"server.host", "server.port", "database.url"}
return c.Validate(required...)
}
func validateRanges(c *config.Config) error {
port, _ := c.Get("server.port")
if p := port.(int64); p < 1 || p > 65535 {
return fmt.Errorf("invalid port: %d", p)
}
return nil
}
func validateRelationships(c *config.Config) error {
// Validate that related values make sense together
// e.g., if SSL is enabled, ensure cert paths are set
return nil
}
```
### Error Handling
The builder accumulates errors and returns them on `Build()`:
```go
cfg, err := config.NewBuilder().
WithTarget(nil). // Error: nil target
WithTagName("invalid"). // Error: unsupported tag
Build()
if err != nil {
// err contains first error encountered
}
```
For panic on error use `MustBuild()`
## See Also
- [Environment Variables](env.md) - Environment configuration details
- [Live Reconfiguration](reconfiguration.md) - File watching with builder

193
doc/cli.md Normal file
View File

@ -0,0 +1,193 @@
# Command Line Arguments
The config package supports command-line argument parsing with flexible formats and automatic type conversion.
## Argument Formats
### Key-Value Pairs
```bash
# Space-separated
./myapp --server.port 8080 --database.url "postgres://localhost/db"
# Equals-separated
./myapp --server.port=8080 --database.url=postgres://localhost/db
# Mixed formats
./myapp --server.port 8080 --debug=true
```
### Boolean Flags
```bash
# Boolean flags don't require a value (assumed true)
./myapp --debug --verbose
# Explicit boolean values
./myapp --debug=true --verbose=false
```
### Nested Paths
Use dot notation for nested configuration:
```bash
./myapp --server.host=0.0.0.0 --server.port=9090 --server.tls.enabled=true
```
## Type Conversion
Command-line values are automatically converted to match registered types:
```go
type Config struct {
Port int64 `toml:"port"`
Timeout time.Duration `toml:"timeout"`
Ratio float64 `toml:"ratio"`
Enabled bool `toml:"enabled"`
Tags []string `toml:"tags"`
}
// All these are parsed correctly:
// --port=8080 → int64(8080)
// --timeout=30s → time.Duration(30 * time.Second)
// --ratio=0.95 → float64(0.95)
// --enabled=true → bool(true)
// --tags=prod,stable → []string{"prod", "stable"}
```
## Integration with flag Package
### Generate flag.FlagSet
```go
// Generate flags from registered configuration
fs := cfg.GenerateFlags()
// Parse command line
if err := fs.Parse(os.Args[1:]); err != nil {
log.Fatal(err)
}
// Apply parsed flags to configuration
if err := cfg.BindFlags(fs); err != nil {
log.Fatal(err)
}
```
### Custom Flag Registration
```go
fs := flag.NewFlagSet("myapp", flag.ContinueOnError)
// Add custom flags
verbose := fs.Bool("v", false, "verbose output")
configFile := fs.String("config", "config.toml", "config file path")
// Parse
fs.Parse(os.Args[1:])
// Use custom flags
if *verbose {
log.SetLevel(log.DebugLevel)
}
// Load config with custom file path
cfg, _ := config.NewBuilder().
WithFile(*configFile).
Build()
// Bind remaining flags
cfg.BindFlags(fs)
```
## Precedence and Overrides
Command-line arguments have the highest precedence by default:
```go
// Default precedence: CLI > Env > File > Default
cfg, _ := config.Quick(defaults, "APP_", "config.toml")
// Even if config.toml sets port=8080 and APP_PORT=9090,
// --port=7070 will win
```
Change precedence if needed:
```go
cfg, _ := config.NewBuilder().
WithSources(
config.SourceEnv, // Env highest
config.SourceCLI, // Then CLI
config.SourceFile, // Then file
config.SourceDefault, // Finally defaults
).
Build()
```
## Argument Parsing Details
### Validation
- Paths must use valid identifiers (letters, numbers, underscore, dash)
- No leading/trailing dots in paths
- Empty segments not allowed (no `..` in paths)
### Special Cases
```bash
# Double dash stops flag parsing
./myapp --port=8080 -- --not-a-flag
# Single dash flags are ignored (not GNU-style)
./myapp -p 8080 # Ignored, use --port
# Quoted values preserve spaces
./myapp --message="Hello World" --name='John Doe'
# Escape quotes in values
./myapp --json="{\"key\": \"value\"}"
```
### Value Parsing Rules
1. **Booleans**: `true`, `false` (case-sensitive)
2. **Numbers**: Standard decimal notation
3. **Strings**: Quoted or unquoted (quotes removed if present)
4. **Lists**: Comma-separated (when target type is slice)
## Override Arguments
```go
// Parse custom arguments instead of os.Args
customArgs := []string{"--debug", "--port=9090"}
cfg, _ := config.NewBuilder().
WithArgs(customArgs).
Build()
```
## Error Handling
CLI parsing errors are returned from `Build()` or `LoadCLI()`:
```go
cfg, err := config.NewBuilder().
WithDefaults(&Config{}).
Build()
if err != nil {
switch {
case errors.Is(err, config.ErrCLIParse):
log.Fatal("Invalid command line arguments:", err)
default:
log.Fatal("Configuration error:", err)
}
}
```
## See Also
- [Environment Variables](env.md) - Environment variable handling
- [Access Patterns](access.md) - Retrieving parsed values

195
doc/env.md Normal file
View File

@ -0,0 +1,195 @@
# Environment Variables
The config package provides flexible environment variable support with automatic name transformation, custom mappings, and whitelist capabilities.
## Basic Usage
Environment variables are automatically mapped from configuration paths:
```go
cfg, _ := config.Quick(defaults, "MYAPP_", "config.toml")
// These environment variables are automatically loaded:
// MYAPP_SERVER_PORT → server.port
// MYAPP_DATABASE_URL → database.url
// MYAPP_LOG_LEVEL → log.level
// MYAPP_FEATURES_ENABLED → features.enabled
```
## Name Transformation
### Default Transformation
By default, paths are transformed as follows:
1. Dots (`.`) become underscores (`_`)
2. Converted to uppercase
3. Prefix is prepended
```go
// Path transformations:
// server.port → MYAPP_SERVER_PORT
// database.url → MYAPP_DATABASE_URL
// tls.cert.path → MYAPP_TLS_CERT_PATH
// maxRetries → MYAPP_MAXRETRIES
```
### Custom Transformation
Define custom environment variable mappings:
```go
cfg, _ := config.NewBuilder().
WithEnvTransform(func(path string) string {
switch path {
case "server.port":
return "PORT" // Use $PORT directly
case "database.url":
return "DATABASE_URL"
case "api.key":
return "API_KEY"
default:
// Fallback to default transformation
return "MYAPP_" + strings.ToUpper(
strings.ReplaceAll(path, ".", "_"),
)
}
}).
Build()
```
### No Transformation
Return empty string to skip environment lookup:
```go
cfg, _ := config.NewBuilder().
WithEnvTransform(func(path string) string {
// Only allow specific env vars
allowed := map[string]string{
"port": "PORT",
"database": "DATABASE_URL",
}
return allowed[path] // Empty string if not in map
}).
Build()
```
## Explicit Environment Variable Mapping
Use the `env` struct tag for explicit mappings:
```go
type Config struct {
Port int `toml:"port" env:"PORT"`
Database string `toml:"database" env:"DATABASE_URL"`
APIKey string `toml:"api_key" env:"API_KEY"`
}
// These use the explicit env tag names, ignoring prefix
cfg, _ := config.NewBuilder().
WithDefaults(&Config{}).
WithEnvPrefix("MYAPP_"). // Not used for tagged fields
Build()
```
Or register with explicit environment variable:
```go
cfg.RegisterWithEnv("server.port", 8080, "PORT")
cfg.RegisterWithEnv("database.url", "localhost", "DATABASE_URL")
```
## Environment Variable Whitelist
Limit which paths can be set via environment:
```go
cfg, _ := config.NewBuilder().
WithEnvWhitelist(
"server.port",
"database.url",
"api.key",
"log.level",
). // Only these paths read from environment
Build()
```
## Type Conversion
Environment variables (strings) are automatically converted to the registered type:
```bash
# Booleans
export MYAPP_DEBUG=true
export MYAPP_VERBOSE=false
# Numbers
export MYAPP_PORT=8080
export MYAPP_TIMEOUT=30
export MYAPP_RATIO=0.95
# Durations
export MYAPP_TIMEOUT=30s
export MYAPP_INTERVAL=5m
# Lists (comma-separated)
export MYAPP_TAGS=prod,stable,v2
```
## Manual Environment Loading
Load environment variables at any time:
```go
cfg := config.New()
cfg.RegisterStruct("", &Config{})
// Load with prefix
if err := cfg.LoadEnv("MYAPP_"); err != nil {
log.Fatal(err)
}
// Or use existing options
if err := cfg.LoadWithOptions("", nil, config.LoadOptions{
EnvPrefix: "MYAPP_",
Sources: []config.Source{config.SourceEnv},
}); err != nil {
log.Fatal(err)
}
```
## Discovering Environment Variables
Find which environment variables are set:
```go
// Discover all env vars matching registered paths
discovered := cfg.DiscoverEnv("MYAPP_")
for path, envVar := range discovered {
log.Printf("%s is set via %s", path, envVar)
}
```
## Precedence Examples
Default precedence: CLI > Env > File > Default
Custom precedence (Env > File > CLI > Default):
```go
cfg, _ := config.NewBuilder().
WithSources(
config.SourceEnv,
config.SourceFile,
config.SourceCLI,
config.SourceDefault,
).
Build()
```
## See Also
- [Command Line](cli.md) - CLI argument handling
- [File Configuration](file.md) - Configuration file formats
- [Access Patterns](access.md) - Retrieving values

310
doc/file.md Normal file
View File

@ -0,0 +1,310 @@
# Configuration Files
The config package supports TOML configuration files with automatic loading, discovery, and atomic saving.
## TOML Format
TOML (Tom's Obvious, Minimal Language) is the supported configuration format:
```toml
# Basic values
host = "localhost"
port = 8080
debug = false
# Nested sections
[server]
host = "0.0.0.0"
port = 9090
timeout = "30s"
[database]
url = "postgres://localhost/mydb"
max_conns = 25
timeout = "5s"
# Arrays
[features]
enabled = ["auth", "api", "metrics"]
# Inline tables
tls = { enabled = true, cert = "/path/to/cert", key = "/path/to/key" }
```
## Loading Configuration Files
### Basic Loading
```go
cfg := config.New()
cfg.RegisterStruct("", &Config{})
if err := cfg.LoadFile("config.toml"); err != nil {
if errors.Is(err, config.ErrConfigNotFound) {
log.Println("Config file not found, using defaults")
} else {
log.Fatal("Failed to load config:", err)
}
}
```
### With Builder
```go
cfg, err := config.NewBuilder().
WithDefaults(&Config{}).
WithFile("/etc/myapp/config.toml").
Build()
```
### Multiple File Attempts
```go
// Try multiple locations
locations := []string{
"./config.toml",
"~/.config/myapp/config.toml",
"/etc/myapp/config.toml",
}
var cfg *config.Config
var err error
for _, path := range locations {
cfg, err = config.NewBuilder().
WithDefaults(&Config{}).
WithFile(path).
Build()
if err == nil || !errors.Is(err, config.ErrConfigNotFound) {
break
}
}
```
## Automatic File Discovery
Use file discovery to find configuration automatically:
```go
cfg, _ := config.NewBuilder().
WithDefaults(&Config{}).
WithFileDiscovery(config.FileDiscoveryOptions{
Name: "myapp",
Extensions: []string{".toml", ".conf"},
EnvVar: "MYAPP_CONFIG",
CLIFlag: "--config",
UseXDG: true,
UseCurrentDir: true,
Paths: []string{"/opt/myapp"},
}).
Build()
```
Search order:
1. CLI flag: `--config=/path/to/config.toml`
2. Environment variable: `$MYAPP_CONFIG`
3. Current directory: `./myapp.toml`, `./myapp.conf`
4. XDG config: `~/.config/myapp/myapp.toml`
5. System paths: `/etc/myapp/myapp.toml`
6. Custom paths: `/opt/myapp/myapp.toml`
## Saving Configuration
### Save Current State
```go
// Save all current values atomically
if err := cfg.Save("config.toml"); err != nil {
log.Fatal("Failed to save config:", err)
}
```
The save operation is atomic - it writes to a temporary file then renames it.
### Save Specific Source
```go
// Save only values from environment variables
if err := cfg.SaveSource("env-config.toml", config.SourceEnv); err != nil {
log.Fatal(err)
}
// Save only file-loaded values
if err := cfg.SaveSource("file-only.toml", config.SourceFile); err != nil {
log.Fatal(err)
}
```
### Generate Default Configuration
```go
// Create a default config file
defaults := &Config{}
// ... set default values ...
cfg, _ := config.NewBuilder().
WithDefaults(defaults).
Build()
// Save defaults as config template
if err := cfg.SaveSource("config.toml.example", config.SourceDefault); err != nil {
log.Fatal(err)
}
```
## File Structure Mapping
TOML structure maps directly to dot-notation paths:
```toml
# Maps to "debug"
debug = true
[server]
# Maps to "server.host"
host = "localhost"
# Maps to "server.port"
port = 8080
[server.tls]
# Maps to "server.tls.enabled"
enabled = true
# Maps to "server.tls.cert"
cert = "/path/to/cert"
[[users]]
# Array elements: "users.0.name", "users.0.role"
name = "admin"
role = "administrator"
[[users]]
# Array elements: "users.1.name", "users.1.role"
name = "user"
role = "standard"
```
## Type Handling
TOML types map to Go types:
```toml
# Strings
name = "myapp"
multiline = """
Line one
Line two
"""
# Numbers
port = 8080 # int64
timeout = 30 # int64
ratio = 0.95 # float64
max_size = 1_000_000 # int64 (underscores allowed)
# Booleans
enabled = true
debug = false
# Dates/Times (RFC 3339)
created_at = 2024-01-15T09:30:00Z
expires = 2024-12-31
# Arrays
ports = [8080, 8081, 8082]
tags = ["production", "stable"]
# Tables (objects)
[database]
host = "localhost"
port = 5432
# Array of tables
[[servers]]
name = "web1"
host = "10.0.0.1"
[[servers]]
name = "web2"
host = "10.0.0.2"
```
## Error Handling
File loading can produce several error types:
```go
err := cfg.LoadFile("config.toml")
if err != nil {
switch {
case errors.Is(err, config.ErrConfigNotFound):
// File doesn't exist - often not fatal
log.Println("No config file, using defaults")
case strings.Contains(err.Error(), "failed to parse TOML"):
// TOML syntax error
log.Fatal("Invalid TOML syntax:", err)
case strings.Contains(err.Error(), "failed to read"):
// Permission or I/O error
log.Fatal("Cannot read config file:", err)
default:
log.Fatal("Config error:", err)
}
}
```
## Security Considerations
### File Permissions
```go
// After saving, verify permissions
info, err := os.Stat("config.toml")
if err == nil {
mode := info.Mode()
if mode&0077 != 0 {
log.Warn("Config file is world/group readable")
// Fix permissions
os.Chmod("config.toml", 0600)
}
}
```
### Size Limits
Files and values have size limits:
- Maximum file size: ~10MB (10 * MaxValueSize)
- Maximum value size: 1MB
## Partial Loading
Load only specific sections:
```go
var serverCfg ServerConfig
if err := cfg.Scan("server", &serverCfg); err != nil {
log.Fatal(err)
}
var dbCfg DatabaseConfig
if err := cfg.Scan("database", &dbCfg); err != nil {
log.Fatal(err)
}
```
## Best Practices
1. **Use Example Files**: Generate `.example` files with defaults
2. **Check Permissions**: Ensure config files aren't world-readable
3. **Validate After Load**: Add validators to check loaded values
4. **Handle Missing Files**: Missing config files often aren't fatal
5. **Use Atomic Saves**: The built-in Save method is atomic
6. **Document Structure**: Comment your TOML files thoroughly
## See Also
- [Live Reconfiguration](reconfiguration.md) - Automatic file reloading
- [Builder Pattern](builder.md) - File discovery options
- [Access Patterns](access.md) - Working with loaded values

176
doc/quick-start.md Normal file
View File

@ -0,0 +1,176 @@
# Quick Start Guide
This guide gets you up and running with the config package in minutes.
## Basic Usage
The simplest way to use the config package is with the `Quick` function:
```go
package main
import (
"log"
"github.com/lixenwraith/config"
)
// Define your configuration structure
type Config struct {
Server struct {
Host string `toml:"host"`
Port int `toml:"port"`
} `toml:"server"`
Database struct {
URL string `toml:"url"`
MaxConns int `toml:"max_conns"`
} `toml:"database"`
Debug bool `toml:"debug"`
}
func main() {
// Create defaults
defaults := &Config{}
defaults.Server.Host = "localhost"
defaults.Server.Port = 8080
defaults.Database.URL = "postgres://localhost/mydb"
defaults.Database.MaxConns = 10
defaults.Debug = false
// Initialize configuration
cfg, err := config.Quick(
defaults, // Default values from struct
"MYAPP_", // Environment variable prefix
"config.toml", // Configuration file path
)
if err != nil {
log.Fatal(err)
}
// Access values
port, _ := cfg.Get("server.port")
dbURL, _ := cfg.Get("database.url")
log.Printf("Server running on port %d", port.(int64))
log.Printf("Database URL: %s", dbURL.(string))
}
```
## Configuration Sources
The package loads configuration from multiple sources in this default order (highest to lowest priority):
1. **Command-line arguments** - Override everything
2. **Environment variables** - Override file and defaults
3. **Configuration file** - Override defaults
4. **Default values** - Base configuration
### Command-Line Arguments
```bash
./myapp --server.port=9090 --debug
```
### Environment Variables
```bash
export MYAPP_SERVER_PORT=9090
export MYAPP_DATABASE_URL="postgres://prod/mydb"
export MYAPP_DEBUG=true
```
### Configuration File (config.toml)
```toml
[server]
host = "0.0.0.0"
port = 8080
[database]
url = "postgres://localhost/mydb"
max_conns = 25
debug = false
```
## Type Safety
The package uses struct tags to ensure type safety. When you register a struct, the types are enforced:
```go
// This struct defines the expected types
type Config struct {
Port int64 `toml:"port"` // Must be a number
Host string `toml:"host"` // Must be a string
Debug bool `toml:"debug"` // Must be a boolean
}
// Type assertions are safe after registration
port, _ := cfg.Get("port")
portNum := port.(int64) // Safe - type is guaranteed
```
## Error Handling
The package validates types during loading:
```go
cfg, err := config.Quick(defaults, "APP_", "config.toml")
if err != nil {
// Handle errors like:
// - Invalid TOML syntax
// - Type mismatches (e.g., string value for int field)
// - File permissions issues
log.Fatal(err)
}
```
## Common Patterns
### Required Fields
```go
// Register required configuration
cfg.RegisterRequired("api.key", "")
cfg.RegisterRequired("database.url", "")
// Validate all required fields are set
if err := cfg.Validate("api.key", "database.url"); err != nil {
log.Fatal("Missing required configuration:", err)
}
```
### Using Different Struct Tags
```go
// Use JSON tags instead of TOML
type Config struct {
Server struct {
Host string `json:"host"`
Port int `json:"port"`
} `json:"server"`
}
cfg, _ := config.NewBuilder().
WithTarget(&Config{}).
WithTagName("json").
WithFile("config.toml").
Build()
```
### Checking Value Sources
```go
// See which source provided a value
port, _ := cfg.Get("server.port")
sources := cfg.GetSources("server.port")
for source, value := range sources {
log.Printf("server.port from %s: %v", source, value)
}
```
## Next Steps
- [Builder Pattern](builder.md) - Advanced configuration options
- [Environment Variables](env.md) - Detailed environment variable handling
- [Access Patterns](access.md) - All ways to get and set values

355
doc/reconfiguration.md Normal file
View File

@ -0,0 +1,355 @@
# Live Reconfiguration
The config package supports automatic configuration reloading when files change, enabling zero-downtime reconfiguration.
## Basic File Watching
### Enable Auto-Update
```go
cfg, _ := config.NewBuilder().
WithDefaults(&Config{}).
WithFile("config.toml").
Build()
// Enable automatic reloading
cfg.AutoUpdate()
// Your application continues running
// Config reloads automatically when file changes
// Stop watching when done
defer cfg.StopAutoUpdate()
```
### Watch for Changes
```go
// Get notified of configuration changes
changes := cfg.Watch()
go func() {
for path := range changes {
log.Printf("Configuration changed: %s", path)
// React to specific changes
switch path {
case "server.port":
// Restart server with new port
restartServer()
case "log.level":
// Update log level
updateLogLevel()
}
}
}()
```
## Watch Options
### Custom Watch Configuration
```go
opts := config.WatchOptions{
PollInterval: 500 * time.Millisecond, // Check every 500ms
Debounce: 200 * time.Millisecond, // Wait 200ms after changes
MaxWatchers: 50, // Limit concurrent watchers
ReloadTimeout: 10 * time.Second, // Timeout for reload
VerifyPermissions: true, // Security check
}
cfg.AutoUpdateWithOptions(opts)
```
### Watch Without Auto-Update
```go
// Just watch, don't auto-reload
changes := cfg.WatchWithOptions(config.WatchOptions{
PollInterval: time.Second,
})
// Manually reload when desired
go func() {
for range changes {
if shouldReload() {
cfg.LoadFile("config.toml")
}
}
}()
```
## Change Detection
### Value Changes
The watcher detects and notifies about:
- New values added
- Existing values modified
- Values removed
- Type changes
```go
changes := cfg.Watch()
for path := range changes {
newVal, exists := cfg.Get(path)
if !exists {
log.Printf("Removed: %s", path)
continue
}
sources := cfg.GetSources(path)
fileVal, hasFile := sources[config.SourceFile]
log.Printf("Changed: %s = %v (from file: %v)",
path, newVal, hasFile)
}
```
### Special Notifications
```go
changes := cfg.Watch()
for notification := range changes {
switch notification {
case "file_deleted":
log.Warn("Config file was deleted")
case "permissions_changed":
log.Error("Config file permissions changed - potential security issue")
case "reload_timeout":
log.Error("Config reload timed out")
default:
if strings.HasPrefix(notification, "reload_error:") {
log.Error("Reload error:", notification)
} else {
// Normal path change
handleConfigChange(notification)
}
}
}
```
## Debouncing
Rapid file changes are automatically debounced:
```go
// Multiple rapid saves to config.toml
// Only triggers one reload after debounce period
opts := config.WatchOptions{
PollInterval: 100 * time.Millisecond,
Debounce: 500 * time.Millisecond, // Wait 500ms
}
cfg.AutoUpdateWithOptions(opts)
```
## Permission Monitoring
```go
opts := config.WatchOptions{
VerifyPermissions: true, // Enabled by default
}
cfg.AutoUpdateWithOptions(opts)
// Detects if file becomes world-writable
changes := cfg.Watch()
for change := range changes {
if change == "permissions_changed" {
// File permissions changed
// Possible security breach
alert("Config file permissions modified!")
}
}
```
## Pattern: Reconfiguration
```go
type Server struct {
cfg *config.Config
listener net.Listener
mu sync.RWMutex
}
func (s *Server) watchConfig() {
changes := s.cfg.Watch()
for path := range changes {
switch {
case strings.HasPrefix(path, "server."):
s.scheduleRestart()
case path == "log.level":
s.updateLogLevel()
case strings.HasPrefix(path, "feature."):
s.reloadFeatures()
}
}
}
func (s *Server) scheduleRestart() {
s.mu.Lock()
defer s.mu.Unlock()
// Graceful restart logic
log.Info("Scheduling server restart for config changes")
// ... drain connections, restart listener ...
}
```
## Pattern: Feature Flags
```go
type FeatureFlags struct {
cfg *config.Config
mu sync.RWMutex
}
func (ff *FeatureFlags) Watch() {
changes := ff.cfg.Watch()
for path := range changes {
if strings.HasPrefix(path, "features.") {
feature := strings.TrimPrefix(path, "features.")
enabled, _ := ff.cfg.Get(path)
log.Printf("Feature %s: %v", feature, enabled)
ff.notifyFeatureChange(feature, enabled.(bool))
}
}
}
func (ff *FeatureFlags) IsEnabled(feature string) bool {
ff.mu.RLock()
defer ff.mu.RUnlock()
val, exists := ff.cfg.Get("features." + feature)
return exists && val.(bool)
}
```
## Pattern: Multi-Stage Reload
```go
func watchConfigWithValidation(cfg *config.Config) {
changes := cfg.Watch()
for range changes {
// Stage 1: Snapshot current config
backup := cfg.Clone()
// Stage 2: Validate new configuration
if err := validateNewConfig(cfg); err != nil {
log.Error("Invalid configuration:", err)
continue
}
// Stage 3: Apply changes
if err := applyConfigChanges(cfg, backup); err != nil {
log.Error("Failed to apply changes:", err)
// Could restore from backup here
continue
}
log.Info("Configuration successfully reloaded")
}
}
```
## Monitoring
### Watch Status
```go
// Check if watching is active
if cfg.IsWatching() {
log.Printf("Auto-update is enabled")
log.Printf("Active watchers: %d", cfg.WatcherCount())
}
```
### Resource Management
```go
// Limit watchers to prevent resource exhaustion
opts := config.WatchOptions{
MaxWatchers: 10, // Max 10 concurrent watch channels
}
// Watchers beyond limit receive closed channels
cfg.AutoUpdateWithOptions(opts)
```
## Best Practices
1. **Always Stop Watching**: Use `defer cfg.StopAutoUpdate()` to clean up
2. **Handle All Notifications**: Check for special error notifications
3. **Validate After Reload**: Ensure new config is valid before applying
4. **Use Debouncing**: Prevent reload storms from rapid edits
5. **Monitor Permissions**: Enable permission verification for security
6. **Graceful Updates**: Plan how your app handles config changes
7. **Log Changes**: Audit configuration modifications
## Limitations
- File watching uses polling (not inotify/kqueue)
- No support for watching multiple files
- Changes only detected for registered paths
- Reloads entire file (no partial updates)
## Common Issues
### Changes Not Detected
```go
// Ensure path is registered before watching
cfg.Register("new.value", "default")
// Now changes to new.value will be detected
```
### Rapid Reloads
```go
// Increase debounce to prevent rapid reloads
opts := config.WatchOptions{
Debounce: 2 * time.Second, // Wait 2s after changes stop
}
```
### Memory Leaks
```go
// Always stop watching to prevent goroutine leaks
watcher := cfg.Watch()
// Use context for cancellation
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
for {
select {
case change := <-watcher:
handleChange(change)
case <-ctx.Done():
return
}
}
}()
```
## See Also
- [File Configuration](file.md) - File format and loading
- [Access Patterns](access.md) - Reacting to changed values
- [Builder Pattern](builder.md) - Setting up watching with builder