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

+24 -169
View File
@@ -1,174 +1,29 @@
# Config Package Architecture
# Architecture
## Overview
Registered items hold a default, source overrides and an effective value selected
by precedence. A Config RWMutex protects these maps and runtime options. Values
are copied on admission and never mutated internally afterward; replacement removes
or swaps references. Public reads return copies. Nested snapshots can therefore be
assembled under a read lock and decoded after releasing it.
The `lixenwraith/config` package provides thread-safe configuration management with support for multiple sources, type-safe struct population, and live reconfiguration.
The decoder handles the limited configuration object model: TOML tags, standard
containers and atomic duration/time/network values. It delegates numeric conversion
to lixenwraith/toml and uses explicit decimal parsing for environment/CLI strings.
No mapstructure or external file parser is used in the runtime dependency graph.
## Logical Architecture
Typed cache metadata uses a second mutex, always acquired after the Config read
lock. Version changes invalidate its immutable decoded snapshot. Every AsStruct call
returns a detached copy, so publication cannot race with existing readers. Build
writes the supplied target once. Clone copies configuration state and options but
does not copy active watchers or subscriptions.
```
┌──────────────────────────────────────────────────────────┐
│ User API Layer │
├────────────────┬────────────────┬────────────────────────┤
│ Builder │ Type-Safe │ Dynamic Access │
│ Pattern │ Struct API │ Key-Value API │
├────────────────┴────────────────┴────────────────────────┤
│ Core Config Engine │
│ • Path Registration • Source Merging • Thread Safety │
├──────────────────────────────────────────────────────────┤
│ Source Loaders │
│ File │ Environment │ CLI Arguments │ Defaults │
├──────────────────────────────────────────────────────────┤
│ Supporting Systems │
│ Validation │ Type Decode │ File Watch │ Error Handling │
└──────────────────────────────────────────────────────────┘
```
File reads and parsing precede source publication. All candidate values are checked
against the current schema, then the complete source is replaced under one lock.
The watcher additionally verifies cancellation, timeout and generation at commit.
One worker owns fingerprint/debounce state. Subscriber send/close operations share a
mutex; stop does not wait for that worker while holding the Config mutex.
## Component Interactions
```
Builder Flow:
NewBuilder()
↓
Configure (WithTarget, WithFile, WithEnvPrefix)
↓
Build() → Register Paths → Load Sources → Merge Values
↓
Config Instance
↓
AsStruct() / Get() / Watch()
Value Resolution:
Path Request
↓
Check Registration
↓
Search Sources (CLI → Env → File → Default)
↓
Type Conversion
↓
Return Value
Live Reload:
File Change Detected
↓
Debounce Timer
↓
Reload File
↓
Merge with Sources
↓
Notify Watchers
```
## File Organization
### Core (`config.go`)
- **Config struct**: Thread-safe state management with atomic versioning
- **Initialization**: `New()`, `NewWithOptions()`
- **State Management**: `Get()`, `Set()`, `GetSource()`, `SetSource()`
- **Precedence Control**: `SetPrecedence()`, `GetPrecedence()`, `computeValue()`
- **Cache Management**: `AsStruct()`, `populateStruct()`, `invalidateCache()`
- **Source Operations**: `Reset()`, `ResetSource()`, `GetSources()`
### Registration (`register.go`)
- **Path Registration**: `Register()`, `RegisterWithEnv()`, `RegisterRequired()`, `Unregister()`
- **Struct Registration**: `RegisterStruct()`, `RegisterStructWithTags()`, `registerFields()`
- **Discovery**: `GetRegisteredPaths()`, `GetRegisteredPathsWithDefaults()`
- **Decoding Bridge**: `Scan()`, `ScanSource()` - delegates to decode.go
### Builder Pattern (`builder.go`)
- **Fluent API**: `NewBuilder()`, `Build()`, `MustBuild()`
- **Configuration**: `WithTarget()`, `WithDefaults()`, `WithFile()`, `WithEnvPrefix()`
- **Discovery Integration**: `WithFileDiscovery()`
- **Validation**: `WithValidator()`, `WithTypedValidator()`
- **Security**: `WithSecurityOptions()`
### Source Loading (`loader.go`)
- **Multi-Source Loading**: `loadWithOptions()`
- **Individual Sources**: `LoadFile()`, `LoadEnv()`, `LoadCLI()`
- **Persistence**: `Save()`, `SaveSource()`, `atomicWriteFile()`
- **Environment Mapping**: `DiscoverEnv()`, `ExportEnv()`, `defaultEnvTransform()`
- **Parsing**: `parseArgs()`, `parseValue()`, `detectFileFormat()`
- **Security Checks**: Path traversal, file ownership, size limits
### Type System (`decode.go`)
- **Unified Decoding**: `unmarshal()` - single authoritative decoder
- **Hook Composition**: `getDecodeHook()`, `customDecodeHook()`
- **Type Converters**: `jsonNumberHookFunc()`, `stringToNetIPHookFunc()`, `stringToURLHookFunc()`
- **Navigation**: `navigateToPath()`, `normalizeMap()`
### File Watching (`watch.go`)
- **Auto-Reload**: `AutoUpdate()`, `AutoUpdateWithOptions()`, `StopAutoUpdate()`
- **Subscriptions**: `Watch()`, `WatchWithOptions()`, `WatchFile()`
- **Watcher State**: `watcher` struct with debouncing, polling, version tracking
- **Change Detection**: `checkAndReload()`, `performReload()`, `notifyWatchers()`
- **Resource Management**: Max watcher limits, graceful shutdown
### Convenience API (`utility.go`)
- **Quick Setup**: `Quick()`, `QuickCustom()`, `MustQuick()`, `QuickTyped()`
- **Flag Integration**: `GenerateFlags()`, `BindFlags()`
- **Type-Safe Access**: `GetTyped()`, `GetTypedWithDefault()`, `ScanTyped()`
- **Utilities**: `Validate()`, `Debug()`, `Dump()`, `Clone()`, `ScanMap()`
### File Discovery (`discovery.go`)
- **Options**: `FileDiscoveryOptions` struct with search strategies
- **XDG Compliance**: `getXDGConfigPaths()` for standard config locations
- **Defaults**: `DefaultDiscoveryOptions()` with sensible patterns
### Validation Library (`validator.go`)
- **Network**: `Port()`, `IPAddress()`, `IPv4Address()`, `IPv6Address()`
- **Numeric**: `Positive()`, `NonNegative()`, `Range()`
- **String**: `NonEmpty()`, `Pattern()`, `OneOf()`, `URLPath()`
### Error System (`error.go`)
- **Categories**: Sentinel errors for `errors.Is()` checking
- **Wrapping**: `wrapError()` maintains dual error chains
### Internal Utilities (`helper.go`)
- **Map Operations**: `flattenMap()`, `setNestedValue()`
- **Validation**: `isValidKeySegment()` for path validation
### Constants (`constant.go`)
- **Shared Constants**: Defines shared constants for timing, formats, limits, file watcher and discovery.
## Data Flow Patterns
### Configuration Loading
1. **Registration Phase**: Paths registered with types/defaults
2. **Source Collection**: Each source populates its layer
3. **Merge Phase**: Precedence determines final values
4. **Population Phase**: Struct populated via reflection
### Thread Safety Model
- **Read Operations**: Multiple concurrent readers via RLock
- **Write Operations**: Exclusive access via Lock
- **Atomic Updates**: Prepare → Lock → Swap → Unlock pattern
- **Version Tracking**: Atomic counter for cache invalidation
### Error Propagation
- **Wrapped Errors**: Category + specific error detail
- **Early Return**: Builder accumulates first error
- **Panic Mode**: MustBuild for fail-fast scenarios
## Extension Points
### Custom Types
Implement decode hooks in `customDecodeHook()`:
```go
func(f reflect.Type, t reflect.Type, data any) (any, error)
```
### Source Transformation
Environment variable mapping via `WithEnvTransform()`:
```go
func(path string) string // Return env var name
```
### Validation Layers
- Pre-decode: `WithValidator(func(*Config) error)`
- Post-decode: `WithTypedValidator(func(*YourType) error)`
### File Discovery
Search strategy via `WithFileDiscovery()`:
- CLI flag check → Env var → XDG paths → Current dir
Save normalizes atomic application values to TOML strings, marshals before touching
the target file, then writes/syncs/renames a temporary file. Comment text is captured
by lexer tokens and emitted as a preamble. The codec owns syntax, while Config owns
source precedence, snapshot isolation and live reload publication.