v0.1.1 helpers update to public, docs and comments update
This commit is contained in:
@ -61,7 +61,7 @@ func main() {
|
||||
- [File Configuration](doc/file.md) - File formats and loading
|
||||
- [Validation](doc/validator.md) - Validation functions and integration
|
||||
- [Live Reconfiguration](doc/reconfiguration.md) - File watching and auto-update on change
|
||||
- [LLM Integration Guide](doc/config-llm-guide.md) - Guide for LLM usage without full codebase
|
||||
- [Quick Guide](doc/quick-guide_lixenwraith_config.md) - Quick reference guide
|
||||
|
||||
## License
|
||||
|
||||
|
||||
29
builder.go
29
builder.go
@ -10,8 +10,8 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Builder provides a fluent API for constructing a Config instance. It allows for
|
||||
// chaining configuration options before final build of the config object.
|
||||
// Builder provides a fluent API for constructing a Config instance
|
||||
// Allows chaining configuration options before final build of the config object
|
||||
type Builder struct {
|
||||
cfg *Config
|
||||
opts LoadOptions
|
||||
@ -27,8 +27,8 @@ type Builder struct {
|
||||
typedValidators []any
|
||||
}
|
||||
|
||||
// ValidatorFunc defines the signature for a function that can validate a Config instance.
|
||||
// It receives the fully loaded *Config object and should return an error if validation fails.
|
||||
// ValidatorFunc defines the signature for a function that can validate a Config instance
|
||||
// It receives the fully loaded *Config object and returns error if validation fails
|
||||
type ValidatorFunc func(c *Config) error
|
||||
|
||||
// NewBuilder creates a new configuration builder
|
||||
@ -63,23 +63,22 @@ func (b *Builder) Build() (*Config, error) {
|
||||
}
|
||||
|
||||
// 1. Register defaults
|
||||
// If WithDefaults() was called, it takes precedence.
|
||||
// If not, but WithTarget() was called, use the target struct for defaults.
|
||||
// If WithDefaults() was called, it takes precedence
|
||||
// If not, but WithTarget() was called, use the target struct for defaults
|
||||
if b.defaults != nil {
|
||||
// WithDefaults() was called explicitly.
|
||||
if err := b.cfg.RegisterStructWithTags(b.prefix, b.defaults, tagName); err != nil {
|
||||
return nil, wrapError(ErrTypeMismatch, fmt.Errorf("failed to register defaults: %w", err))
|
||||
}
|
||||
} else if b.cfg.structCache != nil && b.cfg.structCache.target != nil {
|
||||
// No explicit defaults, so use the target struct as the source of defaults.
|
||||
// This is the behavior the tests rely on.
|
||||
// No explicit defaults, so use the target struct as the source of defaults
|
||||
if err := b.cfg.RegisterStructWithTags(b.prefix, b.cfg.structCache.target, tagName); err != nil {
|
||||
return nil, wrapError(ErrTypeMismatch, fmt.Errorf("failed to register target struct as defaults: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
// Explicitly set the file path on the config object so the watcher can find it,
|
||||
// even if the initial load fails with a non-fatal error (file not found).
|
||||
// even if the initial load fails with a non-fatal error (file not found)
|
||||
b.cfg.configFilePath = b.file
|
||||
|
||||
// 2. Load configuration
|
||||
@ -98,23 +97,23 @@ func (b *Builder) Build() (*Config, error) {
|
||||
|
||||
// 4. Populate target and run typed validators
|
||||
if b.cfg.structCache != nil && b.cfg.structCache.target != nil && len(b.typedValidators) > 0 {
|
||||
// Populate the target struct first. This unifies all types (e.g., string "8888" -> int64 8888).
|
||||
// Populate the target struct first, unifying all types (e.g., string "8888" -> int64 8888)
|
||||
populatedTarget, err := b.cfg.AsStruct()
|
||||
if err != nil {
|
||||
return nil, wrapError(ErrValidation, fmt.Errorf("failed to populate target struct for validation: %w", err))
|
||||
}
|
||||
|
||||
// Run the typed validators against the populated, type-safe struct.
|
||||
// Run the typed validators against the populated, type-safe struct
|
||||
for _, validator := range b.typedValidators {
|
||||
validatorFunc := reflect.ValueOf(validator)
|
||||
validatorType := validatorFunc.Type()
|
||||
|
||||
// Check if the validator's input type matches the target's type.
|
||||
// Check if the validator's input type matches the target's type
|
||||
if validatorType.In(0) != reflect.TypeOf(populatedTarget) {
|
||||
return nil, wrapError(ErrTypeMismatch, fmt.Errorf("typed validator signature %v does not match target type %T", validatorType, populatedTarget))
|
||||
}
|
||||
|
||||
// Call the validator.
|
||||
// Call the validator
|
||||
results := validatorFunc.Call([]reflect.Value{reflect.ValueOf(populatedTarget)})
|
||||
if !results[0].IsNil() {
|
||||
err := results[0].Interface().(error)
|
||||
@ -319,13 +318,13 @@ func (b *Builder) WithValidator(fn ValidatorFunc) *Builder {
|
||||
|
||||
// WithTypedValidator adds a type-safe validation function that runs at the end of the build process,
|
||||
// after the target struct has been populated. The provided function must accept a single argument
|
||||
// that is a pointer to the same type as the one provided to WithTarget, and must return an error.
|
||||
// that is a pointer to the same type as the one provided to WithTarget, and must return an error
|
||||
func (b *Builder) WithTypedValidator(fn any) *Builder {
|
||||
if fn == nil {
|
||||
return b
|
||||
}
|
||||
|
||||
// Basic reflection check to ensure it's a function that takes one argument and returns an error.
|
||||
// Basic reflection check to ensure it's a function that takes one argument and returns an error
|
||||
t := reflect.TypeOf(fn)
|
||||
if t.Kind() != reflect.Func || t.NumIn() != 1 || t.NumOut() != 1 || t.Out(0) != reflect.TypeOf((*error)(nil)).Elem() {
|
||||
b.err = wrapError(ErrTypeMismatch, fmt.Errorf("WithTypedValidator requires a function with signature func(*T) error"))
|
||||
|
||||
@ -224,7 +224,6 @@ func TestFileDiscovery(t *testing.T) {
|
||||
assert.Equal(t, "value", val)
|
||||
})
|
||||
|
||||
// Rest of test cases remain the same...
|
||||
t.Run("DiscoveryWithEnvVar", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "env.toml")
|
||||
@ -361,4 +360,4 @@ func TestBuilderWithTypedValidator(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "typed validator signature")
|
||||
})
|
||||
}
|
||||
}
|
||||
14
config.go
14
config.go
@ -55,7 +55,7 @@ type Config struct {
|
||||
configFilePath string // Track loaded file path
|
||||
}
|
||||
|
||||
// New creates and initializes a new Config instance.
|
||||
// New creates and initializes a new Config instance
|
||||
func New() *Config {
|
||||
return &Config{
|
||||
items: make(map[string]configItem),
|
||||
@ -160,8 +160,8 @@ func (c *Config) GetPrecedence() []Source {
|
||||
return result
|
||||
}
|
||||
|
||||
// SetFileFormat sets the expected format for configuration files.
|
||||
// Use "auto" to detect based on file extension.
|
||||
// SetFileFormat sets the expected format for configuration files
|
||||
// Use "auto" to detect based on file extension
|
||||
func (c *Config) SetFileFormat(format string) error {
|
||||
switch format {
|
||||
case FormatTOML, FormatJSON, FormatYAML, FormatAuto:
|
||||
@ -211,10 +211,10 @@ func (c *Config) GetSource(path string, source Source) (any, bool) {
|
||||
return val, exists
|
||||
}
|
||||
|
||||
// Set updates a configuration value for the given path.
|
||||
// It sets the value in the highest priority source from the configured Sources.
|
||||
// By default, this is SourceCLI. Returns an error if the path is not registered.
|
||||
// To set a value in a specific source, use SetSource instead.
|
||||
// Set updates a configuration value for the given path
|
||||
// It sets the value in the highest priority source from the configured Sources
|
||||
// By default, this is SourceCLI. Returns an error if the path is not registered
|
||||
// To set a value in a specific source, use SetSource instead
|
||||
func (c *Config) Set(path string, value any) error {
|
||||
return c.SetSource(c.options.Sources[0], path, value)
|
||||
}
|
||||
|
||||
@ -180,7 +180,7 @@ func (c *Config) Dump() error {
|
||||
|
||||
nestedData := make(map[string]any)
|
||||
for path, item := range c.items {
|
||||
setNestedValue(nestedData, path, item.currentValue)
|
||||
SetNestedValue(nestedData, path, item.currentValue)
|
||||
}
|
||||
|
||||
encoder := toml.NewEncoder(os.Stdout)
|
||||
@ -241,7 +241,7 @@ func QuickTyped[T any](target *T, envPrefix, configFile string) (*Config, error)
|
||||
Build()
|
||||
}
|
||||
|
||||
// GetTyped retrieves a configuration value and decodes it into the specified type T.
|
||||
// GetTyped retrieves a configuration value and decodes it into the specified type T
|
||||
// It leverages the same decoding hooks as the Scan and AsStruct methods,
|
||||
// providing type conversion from strings, numbers, etc.
|
||||
func GetTyped[T any](c *Config, path string) (T, error) {
|
||||
@ -252,13 +252,13 @@ func GetTyped[T any](c *Config, path string) (T, error) {
|
||||
return zero, wrapError(ErrPathNotFound, fmt.Errorf("path %q not found", path))
|
||||
}
|
||||
|
||||
// Prepare the input map and target struct for the decoder.
|
||||
// Prepare the input map and target struct for the decoder
|
||||
inputMap := map[string]any{"value": rawValue}
|
||||
var target struct {
|
||||
Value T `mapstructure:"value"`
|
||||
}
|
||||
|
||||
// Create a new decoder configured with the same hooks as the main config.
|
||||
// Create a new decoder configured with the same hooks as the main config
|
||||
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
Result: &target,
|
||||
TagName: c.tagName,
|
||||
@ -278,9 +278,9 @@ func GetTyped[T any](c *Config, path string) (T, error) {
|
||||
return target.Value, nil
|
||||
}
|
||||
|
||||
// GetTypedWithDefault retrieves a configuration value with a default fallback.
|
||||
// If the path doesn't exist or isn't set, it sets and returns the default value.
|
||||
// This is a convenience function for simple cases where explicit defaults aren't pre-registered.
|
||||
// GetTypedWithDefault retrieves a configuration value with a default fallback
|
||||
// If the path doesn't exist or isn't set, it sets and returns the default value
|
||||
// This is a convenience function for simple cases where explicit defaults aren't pre-registered
|
||||
func GetTypedWithDefault[T any](c *Config, path string, defaultValue T) (T, error) {
|
||||
// Check if path exists and has a value
|
||||
if _, exists := c.Get(path); exists {
|
||||
@ -311,7 +311,7 @@ func GetTypedWithDefault[T any](c *Config, path string, defaultValue T) (T, erro
|
||||
}
|
||||
|
||||
// ScanTyped is a generic wrapper around Scan. It allocates a new instance of type T,
|
||||
// populates it with configuration data from the given base path, and returns a pointer to it.
|
||||
// populates it with configuration data from the given base path, and returns a pointer to it
|
||||
func ScanTyped[T any](c *Config, basePath ...string) (*T, error) {
|
||||
var target T
|
||||
if err := c.Scan(&target, basePath...); err != nil {
|
||||
|
||||
16
decode.go
16
decode.go
@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
// unmarshal is the single authoritative function for decoding configuration
|
||||
// into target structures. All public decoding methods delegate to this.
|
||||
// into target structures. All public decoding methods delegate to this
|
||||
func (c *Config) unmarshal(source Source, target any, basePath ...string) error {
|
||||
// Parse variadic basePath
|
||||
path := ""
|
||||
@ -42,13 +42,13 @@ func (c *Config) unmarshal(source Source, target any, basePath ...string) error
|
||||
if source == "" {
|
||||
// Use current merged state
|
||||
for path, item := range c.items {
|
||||
setNestedValue(nestedMap, path, item.currentValue)
|
||||
SetNestedValue(nestedMap, path, item.currentValue)
|
||||
}
|
||||
} else {
|
||||
// Use specific source
|
||||
for path, item := range c.items {
|
||||
if val, exists := item.values[source]; exists {
|
||||
setNestedValue(nestedMap, path, val)
|
||||
SetNestedValue(nestedMap, path, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -56,13 +56,13 @@ func (c *Config) unmarshal(source Source, target any, basePath ...string) error
|
||||
// Navigate to basePath section
|
||||
sectionData := navigateToPath(nestedMap, path)
|
||||
|
||||
// Ensure we have a map to decode, normalizing if necessary.
|
||||
// Ensure we have a map to decode, normalizing if necessary
|
||||
sectionMap, err := normalizeMap(sectionData)
|
||||
if err != nil {
|
||||
if sectionData == nil {
|
||||
sectionMap = make(map[string]any) // Empty section is valid.
|
||||
sectionMap = make(map[string]any) // Empty section is valid
|
||||
} else {
|
||||
// Path points to a non-map value, which is an error for Scan.
|
||||
// Path points to a non-map value, which is an error for Scan
|
||||
return wrapError(ErrTypeMismatch, fmt.Errorf("path %q refers to non-map value (type %T)", path, sectionData))
|
||||
}
|
||||
}
|
||||
@ -87,7 +87,7 @@ func (c *Config) unmarshal(source Source, target any, basePath ...string) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// normalizeMap ensures that the input data is a map[string]any for the decoder.
|
||||
// normalizeMap ensures that the input data is a map[string]any for the decoder
|
||||
func normalizeMap(data any) (map[string]any, error) {
|
||||
if data == nil {
|
||||
return make(map[string]any), nil
|
||||
@ -105,7 +105,7 @@ func normalizeMap(data any) (map[string]any, error) {
|
||||
return nil, wrapError(ErrTypeMismatch, fmt.Errorf("map keys must be strings, but got %v", v.Type().Key()))
|
||||
}
|
||||
|
||||
// Create a new map[string]any and copy the values.
|
||||
// Create a new map[string]any and copy the values
|
||||
normalized := make(map[string]any, v.Len())
|
||||
iter := v.MapRange()
|
||||
for iter.Next() {
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
# lixenwraith/config LLM Usage Guide
|
||||
# lixenwraith/config Quick Reference Guide
|
||||
|
||||
This guide details the `lixenwraith/config` package for thread-safe Go configuration. It supports multiple sources (files, environment, CLI), type-safe struct population, and live reconfiguration.
|
||||
This guide details the `lixenwraith/config` package for thread-safe Go configuration.
|
||||
It supports multiple sources (files, environment, CLI), type-safe struct population, and live reconfiguration.
|
||||
|
||||
## Quick Start: Recommended Usage
|
||||
|
||||
The recommended pattern uses the **Builder** with a **target struct**. This provides compile-time type safety and eliminates the need for runtime type assertions.
|
||||
The recommended pattern uses the **Builder** with a **target struct** providing compile-time type safety and eliminating the need for runtime type assertions
|
||||
|
||||
```go
|
||||
package main
|
||||
@ -17,7 +18,7 @@ import (
|
||||
"github.com/lixenwraith/config"
|
||||
)
|
||||
|
||||
// 1. Define your application's configuration struct.
|
||||
// 1. Define your application's configuration struct
|
||||
type AppConfig struct {
|
||||
Server struct {
|
||||
Host string `toml:"host"`
|
||||
@ -27,7 +28,7 @@ type AppConfig struct {
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 2. Create a target instance with defaults.
|
||||
// 2. Create a target instance with defaults
|
||||
// Method A: Direct initialization (cleaner for simple defaults)
|
||||
target := &AppConfig{}
|
||||
target.Server.Host = "localhost"
|
||||
@ -35,23 +36,23 @@ func main() {
|
||||
|
||||
// Method B: Use WithDefaults() for explicit separation (shown below)
|
||||
|
||||
// 3. Use the builder to configure and load all sources.
|
||||
// 3. Use the builder to configure and load all sources
|
||||
cfg, err := config.NewBuilder().
|
||||
WithTarget(target). // Enable type-safe mode and register struct fields.
|
||||
// WithDefaults(&AppConfig{...}), // Optional: Override defaults from WithTarget.
|
||||
WithFile("config.toml"). // Load from file (supports .toml, .json, .yaml).
|
||||
WithEnvPrefix("APP_"). // Load from environment (e.g., APP_SERVER_PORT).
|
||||
WithArgs(os.Args[1:]). // Load from command-line flags (e.g., --server.port=9090).
|
||||
Build() // Build the final config object.
|
||||
WithTarget(target). // Enable type-safe mode and register struct fields
|
||||
// WithDefaults(&AppConfig{...}), // Optional: Override defaults from WithTarget
|
||||
WithFile("config.toml"). // Load from file (supports .toml, .json, .yaml)
|
||||
WithEnvPrefix("APP_"). // Load from environment (e.g., APP_SERVER_PORT)
|
||||
WithArgs(os.Args[1:]). // Load from command-line flags (e.g., --server.port=9090)
|
||||
Build() // Build the final config object
|
||||
if err != nil {
|
||||
log.Fatalf("Config build failed: %v", err)
|
||||
}
|
||||
|
||||
// 4. Access the fully populated, type-safe struct.
|
||||
// The `target` variable is now populated with the final merged values.
|
||||
// 4. Access the fully populated, type-safe struct
|
||||
// The `target` variable is now populated with the final merged values
|
||||
fmt.Printf("Running on %s:%d\n", target.Server.Host, target.Server.Port)
|
||||
|
||||
// Or, retrieve the updated struct at any time (e.g., after a live reload).
|
||||
// Or, retrieve the updated struct at any time (e.g., after a live reload)
|
||||
latest, _ := cfg.AsStruct()
|
||||
latestConfig := latest.(*AppConfig)
|
||||
fmt.Printf("Debug mode: %v\n", latestConfig.Debug)
|
||||
@ -60,63 +61,63 @@ func main() {
|
||||
|
||||
## Supported Formats: TOML, JSON, YAML
|
||||
|
||||
The package supports multiple file formats. The format is auto-detected from the file extension (`.toml`, `.json`, `.yaml`, `.yml`) or file content.
|
||||
The package supports multiple file formats. The format is auto-detected from the file extension (`.toml`, `.json`, `.yaml`, `.yml`) or file content
|
||||
|
||||
* To specify a format explicitly, use `Builder.WithFileFormat("json")`.
|
||||
* The default struct tag for field mapping is `toml`, but can be changed with `Builder.WithTagName("json")`.
|
||||
* To specify a format explicitly, use `Builder.WithFileFormat("json")`
|
||||
* The default struct tag for field mapping is `toml`, but can be changed with `Builder.WithTagName("json")`
|
||||
|
||||
## Builder Pattern
|
||||
|
||||
The `Builder` is the primary way to construct a `Config` instance.
|
||||
The `Builder` is the primary way to construct a `Config` instance
|
||||
|
||||
```go
|
||||
// NewBuilder creates a new configuration builder.
|
||||
// NewBuilder creates a new configuration builder
|
||||
func NewBuilder() *Builder
|
||||
// Build finalizes configuration; returns the first of any accumulated errors.
|
||||
// Build finalizes configuration; returns the first of any accumulated errors
|
||||
func (b *Builder) Build() (*Config, error)
|
||||
// MustBuild is like Build but panics on fatal errors.
|
||||
// MustBuild is like Build but panics on fatal errors
|
||||
func (b *Builder) MustBuild() *Config
|
||||
```
|
||||
|
||||
### Builder Methods
|
||||
|
||||
* `WithTarget(target any)`: **(Recommended)** Enables type-safe mode. Registers fields from the target struct and allows access via `AsStruct()`. The target's initial values are used as defaults unless `WithDefaults` is also called.
|
||||
* `WithDefaults(defaults any)`: Explicitly sets a struct containing default values. Overrides any defaults from `WithTarget`.
|
||||
* `WithFile(path string)`: Sets the configuration file path.
|
||||
* `WithFileDiscovery(opts FileDiscoveryOptions)`: Enables automatic config file discovery (searches CLI flags, env vars, XDG paths, and current directory).
|
||||
* `WithArgs(args []string)`: Sets the command-line arguments to parse (e.g., `--server.port=9090`).
|
||||
* `WithEnvPrefix(prefix string)`: Sets the global environment variable prefix (e.g., `MYAPP_`).
|
||||
* `WithSources(sources ...Source)`: Overrides the default source precedence order.
|
||||
* `WithTypedValidator(fn any)`: **(Recommended for validation)** Adds a type-safe validation function that runs *after* the target struct is populated. The function signature must be `func(c *YourConfigType) error`.
|
||||
* `WithValidator(fn ValidatorFunc)`: Adds a validation function that runs *before* type-safe population, operating on the raw `*Config` object.
|
||||
* `WithTagName(tagName string)`: Sets the primary struct tag for field mapping (`"toml"`, `"json"`, `"yaml"`).
|
||||
* `WithPrefix(prefix string)`: Adds a prefix to all registered paths from a struct.
|
||||
* `WithFileFormat(format string)`: Explicitly sets the file format (`"toml"`, `"json"`, `"yaml"`, `"auto"`).
|
||||
* `WithSecurityOptions(opts SecurityOptions)`: Sets security options for file loading (path traversal, file size limits).
|
||||
* `WithEnvTransform(fn EnvTransformFunc)`: Sets a custom environment variable mapping function.
|
||||
* `WithEnvWhitelist(paths ...string)`: Limits environment variable loading to a specific set of paths.
|
||||
* `WithTarget(target any)`: **(Recommended)** Registers fields from the target struct and allows access via `AsStruct()`
|
||||
* `WithDefaults(defaults any)`: Explicitly sets a struct containing default values, overriding any default values already in struct from `WithTarget`
|
||||
* `WithFile(path string)`: Sets the configuration file path
|
||||
* `WithFileDiscovery(opts FileDiscoveryOptions)`: Enables automatic config file discovery (searches CLI flags, env vars, XDG paths, and current directory)
|
||||
* `WithArgs(args []string)`: Sets the command-line arguments to parse (e.g., `--server.port=9090`)
|
||||
* `WithEnvPrefix(prefix string)`: Sets the global environment variable prefix (e.g., `MYAPP_`)
|
||||
* `WithSources(sources ...Source)`: Overrides the default source precedence order
|
||||
* `WithTypedValidator(fn any)`: **(Recommended for validation)** Adds a type-safe validation function with signature `func(c *YourConfigType) error` that runs *after* the target struct is populated
|
||||
* `WithValidator(fn ValidatorFunc)`: Adds a validation function that runs *before* type-safe population, operating on the raw `*Config` object
|
||||
* `WithTagName(tagName string)`: Sets the primary struct tag for field mapping (`"toml"`, `"json"`, `"yaml"`)
|
||||
* `WithPrefix(prefix string)`: Adds a prefix to all registered paths from a struct
|
||||
* `WithFileFormat(format string)`: Explicitly sets the file format (`"toml"`, `"json"`, `"yaml"`, `"auto"`)
|
||||
* `WithSecurityOptions(opts SecurityOptions)`: Sets security options for file loading (path traversal, file size limits)
|
||||
* `WithEnvTransform(fn EnvTransformFunc)`: Sets a custom environment variable mapping function
|
||||
* `WithEnvWhitelist(paths ...string)`: Limits environment variable loading to a specific set of paths
|
||||
|
||||
## Type-Safe Access & Population
|
||||
|
||||
These are the **preferred methods** for accessing configuration data.
|
||||
These are the **preferred methods** for accessing configuration data
|
||||
|
||||
* `AsStruct() (any, error)`: After using `Builder.WithTarget()`, this method returns the populated, type-safe target struct. This is the primary way to access config after initialization or live reload.
|
||||
* `Scan(basePath string, target any)`: Populates a struct with values from a specific config path (e.g., `cfg.Scan("server", &serverConf)`).
|
||||
* `GetTyped[T](c *Config, path string) (T, error)`: Retrieves a single value and decodes it to type `T`, handling type conversion automatically.
|
||||
* `ScanTyped[T](c *Config, basePath ...string) (*T, error)`: A generic wrapper around `Scan` that allocates, populates, and returns a pointer to a struct of type `T`.
|
||||
* `AsStruct() (any, error)`: After using `Builder.WithTarget()`, this method returns the populated, type-safe target struct - This is the primary way to access config after initialization or live reload
|
||||
* `Scan(basePath string, target any)`: Populates a struct with values from a specific config path (e.g., `cfg.Scan("server", &serverConf)`)
|
||||
* `GetTyped[T](c *Config, path string) (T, error)`: Retrieves a single value and decodes it to type `T`, handling type conversion automatically
|
||||
* `ScanTyped[T](c *Config, basePath ...string) (*T, error)`: A generic wrapper around `Scan` that allocates, populates, and returns a pointer to a struct of type `T`
|
||||
|
||||
## Live Reconfiguration
|
||||
|
||||
Enable automatic reloading of configuration when the source file changes.
|
||||
Enable automatic reloading of configuration when the source file changes
|
||||
|
||||
* `AutoUpdate()`: Enables file watching and automatic reloading with default options.
|
||||
* `AutoUpdateWithOptions(opts WatchOptions)`: Enables reloading with custom options (e.g., poll interval, debounce).
|
||||
* `StopAutoUpdate()`: Stops the file watcher.
|
||||
* `Watch() <-chan string`: Returns a channel that receives the paths of changed values.
|
||||
* `WatchFile(filePath string, formatHint ...string)`: Switches the watcher to a new file at runtime.
|
||||
* `IsWatching() bool`: Returns `true` if the file watcher is active.
|
||||
* `AutoUpdate()`: Enables file watching and automatic reloading with default options
|
||||
* `AutoUpdateWithOptions(opts WatchOptions)`: Enables reloading with custom options (e.g., poll interval, debounce)
|
||||
* `StopAutoUpdate()`: Stops the file watcher
|
||||
* `Watch() <-chan string`: Returns a channel that receives the paths of changed values
|
||||
* `WatchFile(filePath string, formatHint ...string)`: Switches the watcher to a new file at runtime
|
||||
* `IsWatching() bool`: Returns `true` if the file watcher is active
|
||||
|
||||
The watch channel also receives special notifications: `"file_deleted"`, `"permissions_changed"`, `"reload_error:..."`.
|
||||
The watch channel also receives special notifications: `"file_deleted"`, `"permissions_changed"`, `"reload_error:..."`
|
||||
|
||||
## Source Precedence
|
||||
|
||||
@ -124,34 +125,40 @@ The default order of precedence (highest to lowest) is:
|
||||
1. **CLI**: Command-line arguments (`--server.port=9090`)
|
||||
2. **Env**: Environment variables (`MYAPP_SERVER_PORT=8888`)
|
||||
3. **File**: Configuration file (`config.toml`)
|
||||
4. **Default**: Values registered from a struct.
|
||||
4. **Default**: Values registered from a struct
|
||||
|
||||
This order can be changed via `Builder.WithSources()` or `Config.SetPrecedence()`.
|
||||
This order can be changed via `Builder.WithSources()` or `Config.SetPrecedence()`
|
||||
|
||||
## Dynamic / Legacy Value Access
|
||||
|
||||
These methods are for dynamic key-value access and should be **avoided when a type-safe struct can be used**. They require runtime type assertions.
|
||||
These methods are for dynamic key-value access that require type assertion and should be **avoided when a type-safe struct can be used**
|
||||
|
||||
* `Get(path string) (any, bool)`: Retrieves the final merged value. The `bool` indicates if the path was registered. Requires a type assertion, e.g., `port := val.(int64)`.
|
||||
* `Set(path string, value any)`: Updates a value in the highest priority source. The path must be registered first.
|
||||
* `GetSource(path string, source Source) (any, bool)`: Retrieves a value from a specific source layer.
|
||||
* `SetSource(path string, source Source, value any)`: Sets a value for a specific source layer.
|
||||
* `Get(path string) (any, bool)`: Retrieves the final merged value. The `bool` indicates if the path was registered. Requires a type assertion, e.g., `port := val.(int64)`
|
||||
* `Set(path string, value any)`: Updates a value in the highest priority source. The path must be registered first
|
||||
* `GetSource(path string, source Source) (any, bool)`: Retrieves a value from a specific source layer
|
||||
* `SetSource(path string, source Source, value any)`: Sets a value for a specific source layer
|
||||
|
||||
## API Reference Summary
|
||||
|
||||
### Core Types
|
||||
|
||||
* `Config`: The primary thread-safe configuration manager.
|
||||
* `Source`: A configuration source (`SourceCLI`, `SourceEnv`, `SourceFile`, `SourceDefault`).
|
||||
* `LoadOptions`: Options for loading configuration from multiple sources.
|
||||
* `Builder`: Fluent API for constructing a `Config` instance.
|
||||
* `Config`: The primary thread-safe configuration manager
|
||||
* `Source`: A configuration source (`SourceCLI`, `SourceEnv`, `SourceFile`, `SourceDefault`)
|
||||
* `LoadOptions`: Options for loading configuration from multiple sources
|
||||
* `Builder`: Fluent API for constructing a `Config` instance
|
||||
|
||||
### Core Methods
|
||||
|
||||
* `New() *Config`: Creates a new `Config` instance.
|
||||
* `Register(path string, defaultValue any)`: Registers a path with a default value.
|
||||
* `RegisterStruct(prefix string, structWithDefaults any)`: Recursively registers fields from a struct using `toml` tags.
|
||||
* `Validate(required ...string)`: Checks that all specified required paths have been set from a non-default source.
|
||||
* `Save(path string)`: Atomically saves the current merged configuration state to a file.
|
||||
* `Clone() *Config`: Creates a deep copy of the configuration state.
|
||||
* `Debug() string`: Returns a formatted string of all values for debugging.
|
||||
* `Register(path string, defaultValue any)`: Registers a path with a default value
|
||||
* `RegisterStruct(prefix string, structWithDefaults any)`: Recursively registers fields from a struct using `toml` tags
|
||||
* `Validate(required ...string)`: Checks that all specified required paths have been set from a non-default source
|
||||
* `Save(path string)`: Atomically saves the current merged configuration state to a file
|
||||
* `Clone() *Config`: Creates a deep copy of the configuration state
|
||||
* `Debug() string`: Returns a formatted string of all values for debugging
|
||||
|
||||
### Utility Functions
|
||||
|
||||
* `FlattenMap(nested map[string]any, prefix string) map[string]any`: Converts a nested map to a flat map with dot-notation keys
|
||||
* `SetNestedValue(nested map[string]any, path string, value any)`: Sets a value in a nested map using a dot-notation path
|
||||
* `IsValidKeySegment(s string) bool`: Checks if a string is a valid path segment
|
||||
10
error.go
10
error.go
@ -8,19 +8,19 @@ import (
|
||||
// Error categories - major error types for client code to check with errors.Is()
|
||||
var (
|
||||
// ErrNotConfigured indicates an operation was attempted on a Config instance
|
||||
// that was not properly prepared for it.
|
||||
// that was not properly prepared for it
|
||||
ErrNotConfigured = errors.New("operation requires additional configuration")
|
||||
|
||||
// ErrConfigNotFound indicates the specified configuration file was not found.
|
||||
// ErrConfigNotFound indicates the specified configuration file was not found
|
||||
ErrConfigNotFound = errors.New("configuration file not found")
|
||||
|
||||
// ErrCLIParse indicates that parsing command-line arguments failed.
|
||||
// ErrCLIParse indicates that parsing command-line arguments failed
|
||||
ErrCLIParse = errors.New("failed to parse command-line arguments")
|
||||
|
||||
// ErrEnvParse indicates that parsing environment variables failed.
|
||||
// ErrEnvParse indicates that parsing environment variables failed
|
||||
ErrEnvParse = errors.New("failed to parse environment variables")
|
||||
|
||||
// ErrValueSize indicates a value larger than MaxValueSize.
|
||||
// ErrValueSize indicates a value larger than MaxValueSize
|
||||
ErrValueSize = errors.New("value size exceeds maximum")
|
||||
|
||||
// ErrPathNotFound indicates the configuration path doesn't exist.
|
||||
|
||||
@ -11,7 +11,7 @@ import (
|
||||
"github.com/lixenwraith/config"
|
||||
)
|
||||
|
||||
// AppConfig defines a richer configuration structure to showcase more features.
|
||||
// AppConfig defines a richer configuration structure to showcase more features
|
||||
type AppConfig struct {
|
||||
Server struct {
|
||||
Host string `toml:"host"`
|
||||
@ -26,17 +26,17 @@ const configFilePath = "config.toml"
|
||||
func main() {
|
||||
// =========================================================================
|
||||
// PART 1: INITIAL SETUP
|
||||
// Create a clean config.toml file on disk for our program to read.
|
||||
// Create a clean config.toml file on disk for our program to read
|
||||
// =========================================================================
|
||||
log.Println("---")
|
||||
log.Println("➡️ PART 1: Creating initial configuration file...")
|
||||
log.Println("PART 1: Creating initial configuration file...")
|
||||
|
||||
// Defer cleanup to run at the end of the program.
|
||||
// Defer cleanup to run at the end of the program
|
||||
defer func() {
|
||||
log.Println("---")
|
||||
log.Println("🧹 Cleaning up...")
|
||||
log.Println("Cleaning up...")
|
||||
os.Remove(configFilePath)
|
||||
// Unset the environment variable we use for testing.
|
||||
// Unset the environment variable we use for testing
|
||||
os.Unsetenv("APP_SERVER_PORT")
|
||||
log.Printf("Removed %s and unset APP_SERVER_PORT.", configFilePath)
|
||||
}()
|
||||
@ -48,67 +48,67 @@ func main() {
|
||||
initialData.FeatureFlags = map[string]bool{"enable_metrics": true}
|
||||
|
||||
if err := createInitialConfigFile(initialData); err != nil {
|
||||
log.Fatalf("❌ Failed during initial file creation: %v", err)
|
||||
log.Fatalf("FAIL - Failed during initial file creation: %v", err)
|
||||
}
|
||||
log.Printf("✅ Initial configuration saved to %s.", configFilePath)
|
||||
log.Printf("PASS - Initial configuration saved to %s.", configFilePath)
|
||||
|
||||
// =========================================================================
|
||||
// PART 2: RECOMMENDED CONFIGURATION USING THE BUILDER
|
||||
// This demonstrates source precedence, validation, and type-safe targets.
|
||||
// This demonstrates source precedence, validation, and type-safe targets
|
||||
// =========================================================================
|
||||
log.Println("---")
|
||||
log.Println("➡️ PART 2: Configuring manager with the Builder...")
|
||||
log.Println("PART 2: Configuring manager with the Builder...")
|
||||
|
||||
// Set an environment variable to demonstrate source precedence (Env > File).
|
||||
// Set an environment variable to demonstrate source precedence (Env > File)
|
||||
os.Setenv("APP_SERVER_PORT", "8888")
|
||||
log.Println(" (Set environment variable APP_SERVER_PORT=8888)")
|
||||
|
||||
// Create a "target" struct. The builder will automatically populate this
|
||||
// and keep it updated when using `AsStruct()`.
|
||||
// and keep it updated when using `AsStruct()`
|
||||
target := &AppConfig{}
|
||||
|
||||
// Use the builder to chain multiple configuration options.
|
||||
// Use the builder to chain multiple configuration options
|
||||
builder := config.NewBuilder().
|
||||
WithTarget(target). // Enables type-safe `AsStruct()` and auto-registration.
|
||||
WithDefaults(initialData). // Explicitly set the source of defaults.
|
||||
WithFile(configFilePath). // Specifies the config file to read.
|
||||
WithEnvPrefix("APP_"). // Sets prefix for environment variables (e.g., APP_SERVER_PORT).
|
||||
WithTarget(target). // Enables type-safe `AsStruct()` and auto-registration
|
||||
WithDefaults(initialData). // Explicitly set the source of defaults
|
||||
WithFile(configFilePath). // Specifies the config file to read
|
||||
WithEnvPrefix("APP_"). // Sets prefix for environment variables (e.g., APP_SERVER_PORT)
|
||||
WithTypedValidator(func(cfg *AppConfig) error { // <-- NEW METHOD
|
||||
// No type assertion needed! `cfg.Server.Port` is guaranteed to be an int64
|
||||
// because the validator runs *after* the target struct is populated.
|
||||
// because the validator runs *after* the target struct is populated
|
||||
if cfg.Server.Port < 1024 || cfg.Server.Port > 65535 {
|
||||
return fmt.Errorf("port %d is outside the recommended range (1024-65535)", cfg.Server.Port)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Build the final config object.
|
||||
// Build the final config object
|
||||
cfg, err := builder.Build()
|
||||
if err != nil {
|
||||
log.Fatalf("❌ Builder failed: %v", err)
|
||||
log.Fatalf("FAIL - Builder failed: %v", err)
|
||||
}
|
||||
|
||||
log.Println("✅ Builder finished successfully. Initial values loaded.")
|
||||
log.Println("PASS - Builder finished successfully. Initial values loaded.")
|
||||
initialTarget, _ := cfg.AsStruct()
|
||||
printCurrentState(initialTarget.(*AppConfig), "Initial State (Env overrides File)")
|
||||
|
||||
// =========================================================================
|
||||
// PART 3: DYNAMIC RELOADING WITH THE WATCHER
|
||||
// We'll now modify the file and verify the watcher updates the config.
|
||||
// We'll now modify the file and verify the watcher updates the config
|
||||
// =========================================================================
|
||||
log.Println("---")
|
||||
log.Println("➡️ PART 3: Testing the file watcher...")
|
||||
log.Println("PART 3: Testing the file watcher...")
|
||||
|
||||
// Use WithOptions to demonstrate customizing the watcher.
|
||||
// Use WithOptions to demonstrate customizing the watcher
|
||||
watchOpts := config.WatchOptions{
|
||||
PollInterval: 250 * time.Millisecond,
|
||||
Debounce: 100 * time.Millisecond,
|
||||
}
|
||||
cfg.AutoUpdateWithOptions(watchOpts)
|
||||
changes := cfg.Watch()
|
||||
log.Println("✅ Watcher is now active with custom options.")
|
||||
log.Println("PASS - Watcher is now active with custom options.")
|
||||
|
||||
// Start a goroutine to modify the file after a short delay.
|
||||
// Start a goroutine to modify the file after a short delay
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go modifyFileOnDiskStructurally(&wg)
|
||||
@ -117,33 +117,33 @@ func main() {
|
||||
log.Println(" (Waiting for watcher notification...)")
|
||||
select {
|
||||
case path := <-changes:
|
||||
log.Printf("✅ Watcher detected a change for path: '%s'", path)
|
||||
log.Printf("PASS - Watcher detected a change for path: '%s'", path)
|
||||
log.Println(" Verifying in-memory config using AsStruct()...")
|
||||
|
||||
// Retrieve the updated, type-safe struct.
|
||||
// Retrieve the updated, type-safe struct
|
||||
updatedTarget, err := cfg.AsStruct()
|
||||
if err != nil {
|
||||
log.Fatalf("❌ AsStruct() failed after update: %v", err)
|
||||
log.Fatalf("FAIL - AsStruct() failed after update: %v", err)
|
||||
}
|
||||
|
||||
// Type-assert and verify the new values.
|
||||
// Type-assert and verify the new values
|
||||
typedCfg := updatedTarget.(*AppConfig)
|
||||
expectedLevel := "debug"
|
||||
if typedCfg.Server.LogLevel != expectedLevel {
|
||||
log.Fatalf("❌ VERIFICATION FAILED: Expected log_level '%s', but got '%s'.", expectedLevel, typedCfg.Server.LogLevel)
|
||||
log.Fatalf("FAIL - VERIFICATION FAILED: Expected log_level '%s', but got '%s'.", expectedLevel, typedCfg.Server.LogLevel)
|
||||
}
|
||||
|
||||
log.Println("✅ VERIFICATION SUCCESSFUL: In-memory config was updated by the watcher.")
|
||||
log.Println("PASS - VERIFICATION SUCCESSFUL: In-memory config was updated by the watcher.")
|
||||
printCurrentState(typedCfg, "Final State (Updated by Watcher)")
|
||||
|
||||
case <-time.After(5 * time.Second):
|
||||
log.Fatalf("❌ TEST FAILED: Timed out waiting for watcher notification.")
|
||||
log.Fatalf("FAIL - TEST FAILED: Timed out waiting for watcher notification.")
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// createInitialConfigFile is a helper to set up the initial file state.
|
||||
// createInitialConfigFile is a helper to set up the initial file state
|
||||
func createInitialConfigFile(data *AppConfig) error {
|
||||
cfg := config.New()
|
||||
if err := cfg.RegisterStruct("", data); err != nil {
|
||||
@ -152,44 +152,44 @@ func createInitialConfigFile(data *AppConfig) error {
|
||||
return cfg.Save(configFilePath)
|
||||
}
|
||||
|
||||
// modifyFileOnDiskStructurally simulates an external program that changes the config file.
|
||||
// modifyFileOnDiskStructurally simulates an external program that changes the config file
|
||||
func modifyFileOnDiskStructurally(wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
time.Sleep(1 * time.Second)
|
||||
log.Println(" (Modifier goroutine: now changing file on disk...)")
|
||||
|
||||
// Create a new, independent config instance to simulate an external process.
|
||||
// Create a new, independent config instance to simulate an external process
|
||||
modifierCfg := config.New()
|
||||
// Register the struct shape so the loader knows what paths are valid.
|
||||
// Register the struct shape so the loader knows what paths are valid
|
||||
if err := modifierCfg.RegisterStruct("", &AppConfig{}); err != nil {
|
||||
log.Fatalf("❌ Modifier failed to register struct: %v", err)
|
||||
log.Fatalf("FAIL - Modifier failed to register struct: %v", err)
|
||||
}
|
||||
// Load the current state from disk.
|
||||
if err := modifierCfg.LoadFile(configFilePath); err != nil {
|
||||
log.Fatalf("❌ Modifier failed to load file: %v", err)
|
||||
log.Fatalf("FAIL - Modifier failed to load file: %v", err)
|
||||
}
|
||||
|
||||
// Change the log level.
|
||||
modifierCfg.Set("server.log_level", "debug")
|
||||
|
||||
// Use the generic GetTyped function. This is safe because modifierCfg has loaded the file.
|
||||
// Use the generic GetTyped function. This is safe because modifierCfg has loaded the file
|
||||
featureFlags, err := config.GetTyped[map[string]bool](modifierCfg, "feature_flags")
|
||||
if err != nil {
|
||||
log.Fatalf("❌ Modifier failed to get typed feature_flags: %v", err)
|
||||
log.Fatalf("FAIL - Modifier failed to get typed feature_flags: %v", err)
|
||||
}
|
||||
|
||||
// Modify the typed map and set it back.
|
||||
featureFlags["enable_metrics"] = false
|
||||
modifierCfg.Set("feature_flags", featureFlags)
|
||||
|
||||
// Save the changes back to disk, which will trigger the watcher in the main goroutine.
|
||||
// Save the changes back to disk, which will trigger the watcher in the main goroutine
|
||||
if err := modifierCfg.Save(configFilePath); err != nil {
|
||||
log.Fatalf("❌ Modifier failed to save file: %v", err)
|
||||
log.Fatalf("FAIL - Modifier failed to save file: %v", err)
|
||||
}
|
||||
log.Println(" (Modifier goroutine: finished.)")
|
||||
}
|
||||
|
||||
// printCurrentState is a helper to display the typed config state.
|
||||
// printCurrentState is a helper to display the typed config state
|
||||
func printCurrentState(cfg *AppConfig, title string) {
|
||||
fmt.Println(" --------------------------------------------------")
|
||||
fmt.Printf(" %s\n", title)
|
||||
|
||||
22
helper.go
22
helper.go
@ -3,8 +3,8 @@ package config
|
||||
|
||||
import "strings"
|
||||
|
||||
// flattenMap converts a nested map[string]any to a flat map[string]any with dot-notation paths.
|
||||
func flattenMap(nested map[string]any, prefix string) map[string]any {
|
||||
// FlattenMap converts a nested map[string]any to a flat map[string]any with dot-notation paths
|
||||
func FlattenMap(nested map[string]any, prefix string) map[string]any {
|
||||
flat := make(map[string]any)
|
||||
|
||||
for key, value := range nested {
|
||||
@ -16,7 +16,7 @@ func flattenMap(nested map[string]any, prefix string) map[string]any {
|
||||
// Check if the value is a map that can be further flattened
|
||||
if nestedMap, isMap := value.(map[string]any); isMap {
|
||||
// Recursively flatten the nested map
|
||||
flattenedSubMap := flattenMap(nestedMap, newPath)
|
||||
flattenedSubMap := FlattenMap(nestedMap, newPath)
|
||||
// Merge the flattened sub-map into the main flat map
|
||||
for subPath, subValue := range flattenedSubMap {
|
||||
flat[subPath] = subValue
|
||||
@ -30,10 +30,10 @@ func flattenMap(nested map[string]any, prefix string) map[string]any {
|
||||
return flat
|
||||
}
|
||||
|
||||
// setNestedValue sets a value in a nested map using a dot-notation path.
|
||||
// It creates intermediate maps if they don't exist.
|
||||
// If a segment exists but is not a map, it will be overwritten by a new map.
|
||||
func setNestedValue(nested map[string]any, path string, value any) {
|
||||
// SetNestedValue sets a value in a nested map using a dot-notation path
|
||||
// It creates intermediate maps if they don't exist
|
||||
// If a segment exists but is not a map, it will be overwritten by a new map
|
||||
func SetNestedValue(nested map[string]any, path string, value any) {
|
||||
segments := strings.Split(path, ".")
|
||||
current := nested
|
||||
|
||||
@ -64,12 +64,12 @@ func setNestedValue(nested map[string]any, path string, value any) {
|
||||
current[lastSegment] = value
|
||||
}
|
||||
|
||||
// isValidKeySegment checks if a single path segment is a valid TOML key part.
|
||||
func isValidKeySegment(s string) bool {
|
||||
// IsValidKeySegment checks if a single path segment is a valid TOML key part
|
||||
func IsValidKeySegment(s string) bool {
|
||||
if len(s) == 0 {
|
||||
return false
|
||||
}
|
||||
// TOML bare keys are sequences of ASCII letters, ASCII digits, underscores, and dashes (A-Za-z0-9_-).
|
||||
// TOML bare keys are sequences of ASCII letters, ASCII digits, underscores, and dashes (A-Za-z0-9_-)
|
||||
if strings.ContainsRune(s, '.') {
|
||||
return false // Segments themselves cannot contain dots
|
||||
}
|
||||
@ -85,4 +85,4 @@ func isValidKeySegment(s string) bool {
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
10
loader.go
10
loader.go
@ -143,7 +143,7 @@ func (c *Config) Save(path string) error {
|
||||
|
||||
nestedData := make(map[string]any)
|
||||
for itemPath, item := range c.items {
|
||||
setNestedValue(nestedData, itemPath, item.currentValue)
|
||||
SetNestedValue(nestedData, itemPath, item.currentValue)
|
||||
}
|
||||
|
||||
c.mutex.RUnlock()
|
||||
@ -215,7 +215,7 @@ func (c *Config) SaveSource(path string, source Source) error {
|
||||
nestedData := make(map[string]any)
|
||||
for itemPath, item := range c.items {
|
||||
if val, exists := item.values[source]; exists {
|
||||
setNestedValue(nestedData, itemPath, val)
|
||||
SetNestedValue(nestedData, itemPath, val)
|
||||
}
|
||||
}
|
||||
|
||||
@ -501,7 +501,7 @@ func (c *Config) loadCLI(args []string) error {
|
||||
return err // Already wrapped with error category in parseArgs
|
||||
}
|
||||
|
||||
flattenedCLI := flattenMap(parsedCLI, "")
|
||||
flattenedCLI := FlattenMap(parsedCLI, "")
|
||||
if len(flattenedCLI) == 0 {
|
||||
return nil // No CLI args to process.
|
||||
}
|
||||
@ -648,13 +648,13 @@ func parseArgs(args []string) (map[string]any, error) {
|
||||
// Validate keyPath segments
|
||||
segments := strings.Split(keyPath, ".")
|
||||
for _, segment := range segments {
|
||||
if !isValidKeySegment(segment) {
|
||||
if !IsValidKeySegment(segment) {
|
||||
return nil, wrapError(ErrInvalidPath, fmt.Errorf("invalid command-line key segment %q in path %q", segment, keyPath))
|
||||
}
|
||||
}
|
||||
|
||||
// Always store as a string. Let Scan handle final type conversion.
|
||||
setNestedValue(result, keyPath, valueStr)
|
||||
SetNestedValue(result, keyPath, valueStr)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
||||
28
register.go
28
register.go
@ -8,10 +8,10 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Register makes a configuration path known to the Config instance.
|
||||
// The path should be dot-separated (e.g., "server.port", "debug").
|
||||
// Each segment of the path must be a valid TOML key identifier.
|
||||
// defaultValue is the value returned by Get if no specific value has been set.
|
||||
// Register makes a configuration path known to the Config instance
|
||||
// The path should be dot-separated (e.g., "server.port", "debug")
|
||||
// Each segment of the path must be a valid TOML key identifier
|
||||
// defaultValue is the value returned by Get if no specific value has been set
|
||||
func (c *Config) Register(path string, defaultValue any) error {
|
||||
if path == "" {
|
||||
return wrapError(ErrInvalidPath, fmt.Errorf("registration path cannot be empty"))
|
||||
@ -20,7 +20,7 @@ func (c *Config) Register(path string, defaultValue any) error {
|
||||
// Validate path segments
|
||||
segments := strings.Split(path, ".")
|
||||
for _, segment := range segments {
|
||||
if !isValidKeySegment(segment) {
|
||||
if !IsValidKeySegment(segment) {
|
||||
return wrapError(ErrInvalidPath, fmt.Errorf("invalid path segment %q in path %q", segment, path))
|
||||
}
|
||||
}
|
||||
@ -60,7 +60,7 @@ func (c *Config) RegisterRequired(path string, defaultValue any) error {
|
||||
return c.Register(path, defaultValue)
|
||||
}
|
||||
|
||||
// Unregister removes a configuration path and all its children.
|
||||
// Unregister removes a configuration path and all its children
|
||||
func (c *Config) Unregister(path string) error {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
@ -96,9 +96,9 @@ func (c *Config) Unregister(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterStruct registers configuration values derived from a struct.
|
||||
// It uses struct tags (`toml:"..."`) to determine the configuration paths.
|
||||
// The prefix is prepended to all paths (e.g., "log."). An empty prefix is allowed.
|
||||
// RegisterStruct registers configuration values derived from a struct
|
||||
// It uses struct tags (`toml:"..."`) to determine the configuration paths
|
||||
// The prefix is prepended to all paths (e.g., "log."). An empty prefix is allowed
|
||||
func (c *Config) RegisterStruct(prefix string, structWithDefaults any) error {
|
||||
return c.RegisterStructWithTags(prefix, structWithDefaults, FormatTOML)
|
||||
}
|
||||
@ -139,7 +139,7 @@ func (c *Config) RegisterStructWithTags(prefix string, structWithDefaults any, t
|
||||
return nil
|
||||
}
|
||||
|
||||
// registerFields is a helper function that handles the recursive field registration.
|
||||
// registerFields is a helper function that handles the recursive field registration
|
||||
func (c *Config) registerFields(v reflect.Value, pathPrefix, fieldPath string, errors *[]string, tagName string) {
|
||||
t := v.Type()
|
||||
|
||||
@ -186,7 +186,7 @@ func (c *Config) registerFields(v reflect.Value, pathPrefix, fieldPath string, e
|
||||
|
||||
if isStruct || isPtrToStruct {
|
||||
// Check if the field's TYPE is one that should be treated as a single value,
|
||||
// even though it's a struct. These types have custom decode hooks.
|
||||
// even though it's a struct. These types have custom decode hooks
|
||||
fieldType := fieldValue.Type()
|
||||
isAtomicStruct := false
|
||||
switch fieldType.String() {
|
||||
@ -194,7 +194,7 @@ func (c *Config) registerFields(v reflect.Value, pathPrefix, fieldPath string, e
|
||||
isAtomicStruct = true
|
||||
}
|
||||
|
||||
// Only recurse if it's a "normal" struct, not an atomic one.
|
||||
// Only recurse if it's a "normal" struct, not an atomic one
|
||||
if !isAtomicStruct {
|
||||
nestedValue := fieldValue
|
||||
if isPtrToStruct {
|
||||
@ -208,7 +208,7 @@ func (c *Config) registerFields(v reflect.Value, pathPrefix, fieldPath string, e
|
||||
c.registerFields(nestedValue, nestedPrefix, fieldPath+field.Name+".", errors, tagName)
|
||||
continue
|
||||
}
|
||||
// If it is an atomic struct, we fall through and register it as a single value.
|
||||
// If it is an atomic struct, we fall through and register it as a single value
|
||||
}
|
||||
|
||||
// Register non-struct fields
|
||||
@ -237,7 +237,7 @@ func (c *Config) registerFields(v reflect.Value, pathPrefix, fieldPath string, e
|
||||
}
|
||||
}
|
||||
|
||||
// GetRegisteredPaths returns all registered configuration paths with the specified prefix.
|
||||
// GetRegisteredPaths returns all registered configuration paths with the specified prefix
|
||||
func (c *Config) GetRegisteredPaths(prefix ...string) map[string]bool {
|
||||
p := ""
|
||||
if len(prefix) > 0 {
|
||||
|
||||
2
watch.go
2
watch.go
@ -137,7 +137,7 @@ func (c *Config) Watch() <-chan string {
|
||||
}
|
||||
|
||||
// WatchFile stops any existing file watcher, loads a new configuration file,
|
||||
// and starts a new watcher on that file path. Optionally accepts format hint.
|
||||
// and starts a new watcher on that file path. Optionally accepts format hint
|
||||
func (c *Config) WatchFile(filePath string, formatHint ...string) error {
|
||||
// Stop any currently running watcher
|
||||
c.StopAutoUpdate()
|
||||
|
||||
@ -14,8 +14,8 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Test-specific timing constants derived from production values.
|
||||
// These accelerate test execution while maintaining timing relationships.
|
||||
// Test-specific timing constants derived from production values
|
||||
// These accelerate test execution while maintaining timing relationships
|
||||
const (
|
||||
// testAcceleration reduces all intervals by this factor for faster tests
|
||||
testAcceleration = 10
|
||||
@ -517,4 +517,4 @@ func BenchmarkWatchOverhead(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = cfg.Get(fmt.Sprintf("value%d", i%100))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user