v0.2.0 toml-only, harden concurrency and env export, dep updates
Test / test (push) Canceled after 0s
Test / test (push) Canceled after 0s
This commit is contained in:
1 parent
3c35601179
commit
e08467bf38
42 files changed
+2533
-4274
No files matched your search
+32
-373
@@ -1,373 +1,32 @@
|
||||
# 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 *before* the target struct is populated. These validators operate on the raw `*config.Config` object and are suitable for checking required paths or formats before type conversion.
|
||||
|
||||
```go
|
||||
// Validator runs on raw, pre-decoded values.
|
||||
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()
|
||||
```
|
||||
|
||||
For type-safe validation, see `WithTypedValidator`.
|
||||
|
||||
### WithTypedValidator
|
||||
|
||||
Add a type-safe validation function that runs *after* the configuration has been fully loaded and decoded into the target struct (set by `WithTarget`). This is the recommended approach for most validation logic.
|
||||
|
||||
The validation function must accept a single argument: a pointer to the same struct type that was passed to `WithTarget`.
|
||||
```go
|
||||
type AppConfig struct {
|
||||
Server struct {
|
||||
Port int64 `toml:"port"`
|
||||
} `toml:"server"`
|
||||
}
|
||||
|
||||
var target AppConfig
|
||||
|
||||
cfg, err := config.NewBuilder().
|
||||
WithTarget(&target).
|
||||
WithFile("config.toml").
|
||||
WithTypedValidator(func(conf *AppConfig) error {
|
||||
if conf.Server.Port < 1024 || conf.Server.Port > 65535 {
|
||||
return fmt.Errorf("port %d is outside the valid range", conf.Server.Port)
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
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/`)
|
||||
|
||||
## Method Interaction and Precedence
|
||||
|
||||
While most builder methods can be chained in any order, it's important to understand how `WithDefaults` and `WithTarget` interact to define the default configuration values.
|
||||
|
||||
### `WithDefaults` Has Precedence
|
||||
|
||||
**Rule:** If `WithDefaults()` is used anywhere in the chain, it will **always** be the definitive source for default values.
|
||||
|
||||
This is the recommended approach for clarity and explicitness. It cleanly separates the struct that defines the defaults from the struct that will be populated.
|
||||
|
||||
**Example (Recommended Pattern):**
|
||||
|
||||
```go
|
||||
// initialData contains the fallback values.
|
||||
initialData := &AppConfig{
|
||||
Server: ServerConfig{Port: 8080},
|
||||
}
|
||||
|
||||
// target is an empty shell for population.
|
||||
var target AppConfig
|
||||
|
||||
// WithDefaults explicitly sets the defaults.
|
||||
// WithTarget sets up the config for type-safe decoding.
|
||||
cfg, err := config.NewBuilder().
|
||||
WithTarget(&target).
|
||||
WithDefaults(initialData).
|
||||
WithFile("config.toml").
|
||||
Build()
|
||||
```
|
||||
|
||||
In this scenario, the `target` struct is *only* used for type information and `AsStruct()` functionality; its initial (zero) values are not used as defaults as per below.
|
||||
|
||||
### Using `WithTarget` for Defaults
|
||||
|
||||
**Rule:** If `WithDefaults()` is **not** used, the struct passed to `WithTarget()` will serve as the source of default values.
|
||||
|
||||
This provides a convenient shorthand for simpler cases where the initial state of your application's config struct *is* the desired default state. The unit tests for the package rely on this behavior.
|
||||
|
||||
**Example (Convenience Pattern):**
|
||||
|
||||
```go
|
||||
// The initial state of this struct will be used as the defaults.
|
||||
target := &AppConfig{
|
||||
Server: ServerConfig{Port: 8080},
|
||||
}
|
||||
|
||||
// Since WithDefaults() is absent, the builder uses `target`
|
||||
// for both defaults and for type-safe decoding.
|
||||
cfg, err := config.NewBuilder().
|
||||
WithTarget(&target).
|
||||
WithFile("config.toml").
|
||||
Build()
|
||||
```
|
||||
|
||||
## Usage 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()`
|
||||
# Builder
|
||||
|
||||
`NewBuilder` collects defaults, an optional typed target, file, environment and CLI
|
||||
settings. Build registers defaults, loads each configured source, checks required
|
||||
paths, runs Config validators and then typed validators. Missing files are nonfatal
|
||||
only when other loading and validation succeeds.
|
||||
|
||||
| Option | Purpose |
|
||||
| --- | --- |
|
||||
| `WithDefaults(value)` | Explicit struct defaults |
|
||||
| `WithTarget(&value)` | Target type and fallback defaults; filled once by Build |
|
||||
| `WithPrefix("section")` | Prefix registered paths and the typed snapshot section |
|
||||
| `WithFile(path)` | TOML file; missing path can be watched for later creation |
|
||||
| `WithFileDiscovery(options)` | Search CLI, environment and configured directories |
|
||||
| `WithEnvPrefix`, `WithEnvTransform`, `WithEnvWhitelist` | Environment mapping |
|
||||
| `WithArgs(args)` | Explicit CLI input; nil disables it |
|
||||
| `WithSources(...)` | Source precedence; first wins |
|
||||
| `WithSecurityOptions(options)` | File traversal, owner and size checks |
|
||||
| `WithValidator(fn)` | Build-time validation using Config accessors |
|
||||
| `WithTypedValidator(func(*T) error)` | Build-time validation of the target snapshot |
|
||||
|
||||
Only TOML tags and file format are supported. WithTagName accepts `toml`;
|
||||
WithFileFormat accepts `toml` or `auto`. JSON/YAML modes are removed.
|
||||
|
||||
Use a builder from one goroutine during initialization. Runtime updates go through
|
||||
Config methods. `AsStruct` returns independent snapshots and never mutates the
|
||||
original target after Build. Validators run during Build, not on every later Set
|
||||
or reload; later changes are always checked against registered value types.
|
||||
|
||||
Explicit defaults must use the same struct type as the target. `WithDefaults`
|
||||
takes precedence over the initial target values. A typed nil validator is ignored;
|
||||
signatures must match the target pointer and return error.
|
||||
Reference in new issue
Block a user