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
+24
-169
@@ -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.
|
||||
Reference in new issue
Block a user