e4.0.0 Refactored, file watcher and improved builder, doc update
This commit is contained in:
376
doc/access.md
Normal file
376
doc/access.md
Normal 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
295
doc/builder.md
Normal 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
193
doc/cli.md
Normal 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
195
doc/env.md
Normal 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
310
doc/file.md
Normal 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
176
doc/quick-start.md
Normal 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
355
doc/reconfiguration.md
Normal 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
|
||||
Reference in New Issue
Block a user