v0.2.0 toml-only, harden concurrency and env export, dep updates
Test / test (push) Canceled after 0s

This commit is contained in:
lixen committed 2026-09-28 16:55:58 -04:00
1 parent 3c35601179
commit e08467bf38
42 files changed
+2533 -4274

No files matched your search

+29 -299
View File
@@ -1,304 +1,34 @@
# Configuration Files
# TOML files and saves
The config package supports TOML configuration files with automatic loading, discovery, and atomic saving.
`LoadFile(path)` parses TOML regardless of the usual TOML/conf/config extension;
JSON/YAML extensions are rejected. `SetFileFormat` and `WithFileFormat` accept
`toml` and the compatibility alias `auto`; neither enables format detection.
Only registered paths are applied. Registered map fields accept whole tables.
Replacing a registered parent table with a scalar is an error. A missing key
removes that source override, exposing the next source or registered default.
## TOML Format
Loads parse and validate before publication. Syntax errors, incompatible values,
overflow and read failures leave prior values, comments and the loaded path intact.
Readers must be regular files. Security options check ownership of the opened file
on Unix and enforce size limits both before and during reading. Ownership checks
fail explicitly on unsupported platforms. PreventPathTraversal rejects relative
paths escaping through `..`; it is not a symlink sandbox. Absolute paths remain
allowed. A zero MaxFileSize means no configured byte limit.
TOML (Tom's Obvious, Minimal Language) is the supported configuration format:
`Save(path)` writes effective values; `SaveSource(path, source)` writes one source.
Default selects registered defaults. Both use a temporary file in the destination
directory, sync it, and rename it over the destination. They preserve an existing
file's permission bits and use 0600 for a new file. They do not promise portable
power-loss durability of the directory entry or platform-independent replacement
of open files. Encoding failures occur before the destination is changed.
```toml
# Basic values
host = "localhost"
port = 8080
debug = false
Loaded comment text, including inline comments, is preserved as a preamble. Hashes
inside strings are not comments. Formatting and comment positions are standardized,
and unknown/unregistered keys are omitted. Saves do not merge concurrent edits made
outside Config after the last successful load. Save does not switch the watched path.
# 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
Durations and atomic time/network values are saved as strings and decoded back to
their registered types. time.Time uses RFC3339Nano; native TOML datetime syntax is
unsupported. Integers in files have signed 64-bit range. Full uint64 values can be
held in memory but cannot be saved above MaxInt64. Unsupported TOML syntax and
numeric key restrictions follow the pinned [toml package](https://github.com/lixenwraith/toml).