e1.0.0 More restructuring and feature add, full scope for this stage.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
|||||||
data
|
data
|
||||||
dev
|
dev
|
||||||
logs
|
logs
|
||||||
|
*.log
|
||||||
|
|||||||
183
README.md
183
README.md
@ -7,10 +7,13 @@ A simple, thread-safe configuration management package for Go applications that
|
|||||||
- **Thread-Safe Operations:** Uses `sync.RWMutex` to protect concurrent access during all configuration operations.
|
- **Thread-Safe Operations:** Uses `sync.RWMutex` to protect concurrent access during all configuration operations.
|
||||||
- **TOML Configuration:** Uses [tinytoml](https://github.com/LixenWraith/tinytoml) for loading and saving configuration files.
|
- **TOML Configuration:** Uses [tinytoml](https://github.com/LixenWraith/tinytoml) for loading and saving configuration files.
|
||||||
- **Command-Line Overrides:** Allows overriding configuration values using dot notation in CLI arguments (e.g., `--server.port 9090`).
|
- **Command-Line Overrides:** Allows overriding configuration values using dot notation in CLI arguments (e.g., `--server.port 9090`).
|
||||||
- **Type-Safe Access:** Register configuration paths with default values and receive unique keys (UUIDs) for consistent access.
|
- **Path-Based Access:** Register configuration paths with default values for direct, consistent access with clear error messages.
|
||||||
- **Atomic File Operations:** Ensures configuration files are written atomically to prevent corruption.
|
- **Atomic File Operations:** Ensures configuration files are written atomically to prevent corruption.
|
||||||
- **Path Validation:** Validates configuration path segments against TOML key requirements.
|
- **Path Validation:** Validates configuration path segments against TOML key requirements.
|
||||||
- **Minimal Dependencies:** Relies only on `tinytoml` and `google/uuid`.
|
- **Minimal Dependencies:** Relies only on `tinytoml` and `mitchellh/mapstructure`.
|
||||||
|
- **Struct Unmarshaling:** Supports decoding configuration subtrees into Go structs with the `UnmarshalSubtree` method.
|
||||||
|
- **Type Conversions:** Helper methods for converting configuration values to common Go types with detailed error messages.
|
||||||
|
- **Hierarchical Data Management:** Automatically handles nested structures through dot notation.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -21,7 +24,7 @@ go get github.com/LixenWraith/config
|
|||||||
Dependencies will be automatically fetched:
|
Dependencies will be automatically fetched:
|
||||||
```bash
|
```bash
|
||||||
github.com/LixenWraith/tinytoml
|
github.com/LixenWraith/tinytoml
|
||||||
github.com/google/uuid
|
github.com/mitchellh/mapstructure
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@ -32,21 +35,111 @@ github.com/google/uuid
|
|||||||
// 1. Initialize a new Config instance
|
// 1. Initialize a new Config instance
|
||||||
cfg := config.New()
|
cfg := config.New()
|
||||||
|
|
||||||
// 2. Register configuration keys with paths and default values
|
// 2. Register configuration paths with default values
|
||||||
keyServerHost, err := cfg.Register("server.host", "127.0.0.1")
|
err := cfg.Register("server.host", "127.0.0.1")
|
||||||
keyServerPort, err := cfg.Register("server.port", 8080)
|
err = cfg.Register("server.port", 8080)
|
||||||
|
|
||||||
// 3. Load configuration from file with CLI argument overrides
|
// 3. Load configuration from file with CLI argument overrides
|
||||||
fileExists, err := cfg.Load("app_config.toml", os.Args[1:])
|
fileExists, err := cfg.Load("app_config.toml", os.Args[1:])
|
||||||
|
|
||||||
// 4. Access configuration values using the registered keys
|
// 4. Access configuration values using the registered paths
|
||||||
serverHost, _ := cfg.Get(keyServerHost)
|
serverHost, err := cfg.String("server.host")
|
||||||
serverPort, _ := cfg.Get(keyServerPort)
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
serverPort, err := cfg.Int64("server.port")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
// 5. Save configuration (creates the file if it doesn't exist)
|
// 5. Save configuration (creates the file if it doesn't exist)
|
||||||
err = cfg.Save("app_config.toml")
|
err = cfg.Save("app_config.toml")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Accessing Typed Values
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Register configuration paths
|
||||||
|
cfg.Register("server.port", 8080)
|
||||||
|
cfg.Register("debug", false)
|
||||||
|
cfg.Register("rate.limit", 1.5)
|
||||||
|
cfg.Register("server.name", "default-server")
|
||||||
|
|
||||||
|
// Use type-specific accessor methods
|
||||||
|
port, err := cfg.Int64("server.port")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error getting port: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
debug, err := cfg.Bool("debug")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error getting debug flag: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rate, err := cfg.Float64("rate.limit")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error getting rate limit: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
name, err := cfg.String("server.name")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error getting server name: %v", err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unmarshal Into Structs
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Define a struct to hold configuration
|
||||||
|
type ServerConfig struct {
|
||||||
|
Host string `toml:"host"`
|
||||||
|
Port int `toml:"port"`
|
||||||
|
Timeout int `toml:"timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register default values if needed
|
||||||
|
cfg.Register("server.host", "localhost")
|
||||||
|
cfg.Register("server.port", 8080)
|
||||||
|
cfg.Register("server.timeout", 30)
|
||||||
|
|
||||||
|
// Load from file
|
||||||
|
cfg.Load("config.toml", nil)
|
||||||
|
|
||||||
|
// Unmarshal the "server" subtree into a struct
|
||||||
|
var serverCfg ServerConfig
|
||||||
|
err := cfg.UnmarshalSubtree("server", &serverCfg)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Updating Configuration Values
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Register a configuration path
|
||||||
|
cfg.Register("server.port", 8080)
|
||||||
|
|
||||||
|
// Update the value
|
||||||
|
err := cfg.Set("server.port", 9090)
|
||||||
|
|
||||||
|
// Save to persist the changes
|
||||||
|
cfg.Save("config.toml")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Removing Configuration Values
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Register paths
|
||||||
|
cfg.Register("server.host", "localhost")
|
||||||
|
cfg.Register("server.port", 8080)
|
||||||
|
cfg.Register("server.debug", true)
|
||||||
|
|
||||||
|
// Unregister a single path
|
||||||
|
err := cfg.Unregister("server.port")
|
||||||
|
|
||||||
|
// Unregister a parent path and all its children
|
||||||
|
// This would remove server.host, server.port, and server.debug
|
||||||
|
err = cfg.Unregister("server")
|
||||||
|
```
|
||||||
|
|
||||||
### CLI Arguments
|
### CLI Arguments
|
||||||
|
|
||||||
Command-line arguments override file configuration using dot notation:
|
Command-line arguments override file configuration using dot notation:
|
||||||
@ -67,22 +160,63 @@ Flags without values are treated as boolean `true`.
|
|||||||
|
|
||||||
Creates and returns a new, initialized `*Config` instance ready for use.
|
Creates and returns a new, initialized `*Config` instance ready for use.
|
||||||
|
|
||||||
### `(*Config) Register(path string, defaultValue any) (string, error)`
|
### `(*Config) Register(path string, defaultValue any) error`
|
||||||
|
|
||||||
Registers a configuration path with a default value and returns a unique UUID key.
|
Registers a configuration path with a default value.
|
||||||
|
|
||||||
- **path**: Dot-separated path corresponding to the TOML structure. Each segment must be a valid TOML key.
|
- **path**: Dot-separated path corresponding to the TOML structure. Each segment must be a valid TOML key.
|
||||||
- **defaultValue**: The value returned by `Get` if not found in configuration or CLI.
|
- **defaultValue**: The value returned if no other value has been set through Load or Set.
|
||||||
- **Returns**: UUID string (key) for use with `Get` and error (nil on success)
|
- **Returns**: Error (nil on success)
|
||||||
|
|
||||||
### `(*Config) Get(key string) (any, bool)`
|
### `(*Config) Get(path string) (any, bool)`
|
||||||
|
|
||||||
Retrieves a configuration value using the UUID key from `Register`.
|
Retrieves a configuration value using the registered path.
|
||||||
|
|
||||||
- **key**: The UUID string returned by `Register`.
|
- **path**: The dot-separated path string used during registration.
|
||||||
- **Returns**: The configuration value and a boolean indicating if the key was registered.
|
- **Returns**: The configuration value and a boolean indicating if the path was registered.
|
||||||
- **Value precedence**: CLI Argument > Config File Value > Registered Default Value
|
- **Value precedence**: CLI Argument > Config File Value > Registered Default Value
|
||||||
|
|
||||||
|
### `(*Config) String(path string) (string, error)`
|
||||||
|
### `(*Config) Int64(path string) (int64, error)`
|
||||||
|
### `(*Config) Bool(path string) (bool, error)`
|
||||||
|
### `(*Config) Float64(path string) (float64, error)`
|
||||||
|
|
||||||
|
Type-specific accessor methods that retrieve and attempt to convert configuration values to the desired type.
|
||||||
|
|
||||||
|
- **path**: The dot-separated path string used during registration.
|
||||||
|
- **Returns**: The typed value and an error (nil on success).
|
||||||
|
- **Errors**: Detailed error messages when:
|
||||||
|
- The path is not registered
|
||||||
|
- The value cannot be converted to the requested type
|
||||||
|
- Type conversion fails (with the specific reason)
|
||||||
|
|
||||||
|
### `(*Config) Set(path string, value any) error`
|
||||||
|
|
||||||
|
Updates a configuration value using the registered path.
|
||||||
|
|
||||||
|
- **path**: The dot-separated path string used during registration.
|
||||||
|
- **value**: The new value to set.
|
||||||
|
- **Returns**: Error if the path wasn't registered or if setting the value fails.
|
||||||
|
|
||||||
|
### `(*Config) Unregister(path string) error`
|
||||||
|
|
||||||
|
Removes a configuration path and all its children from the configuration.
|
||||||
|
|
||||||
|
- **path**: The dot-separated path string used during registration.
|
||||||
|
- **Effects**:
|
||||||
|
- Removes the specified path
|
||||||
|
- Recursively removes all child paths (e.g., unregistering "server" also removes "server.host", "server.port", etc.)
|
||||||
|
- Completely removes both registration and data
|
||||||
|
- **Returns**: Error if the path wasn't registered.
|
||||||
|
|
||||||
|
### `(*Config) UnmarshalSubtree(basePath string, target any) error`
|
||||||
|
|
||||||
|
Decodes a section of the configuration into a struct or map.
|
||||||
|
|
||||||
|
- **basePath**: Dot-separated path to the configuration subtree.
|
||||||
|
- **target**: Pointer to a struct or map where the configuration should be unmarshaled.
|
||||||
|
- **Returns**: Error if unmarshaling fails.
|
||||||
|
|
||||||
### `(*Config) Load(filePath string, args []string) (bool, error)`
|
### `(*Config) Load(filePath string, args []string) (bool, error)`
|
||||||
|
|
||||||
Loads configuration from a TOML file and merges overrides from command-line arguments.
|
Loads configuration from a TOML file and merges overrides from command-line arguments.
|
||||||
@ -103,25 +237,28 @@ Saves the current configuration to the specified TOML file path, performing an a
|
|||||||
### Key Design Choices
|
### Key Design Choices
|
||||||
|
|
||||||
- **Thread Safety**: All operations are protected by a `sync.RWMutex` to support concurrent access.
|
- **Thread Safety**: All operations are protected by a `sync.RWMutex` to support concurrent access.
|
||||||
- **UUID-based Access**: Using UUIDs for configuration keys ensures type safety and prevents string typos during runtime access via `Get`. The path remains the persistent identifier in the config file.
|
- **Unified Storage Model**: Uses a `configItem` struct to store both default values and current values for each path.
|
||||||
- **Map Structure**: Configuration is stored in nested maps of type `map[string]any`.
|
- **Path-Based Access**: Using path strings directly as configuration keys provides a simple, intuitive API while maintaining the path as the persistent identifier in the config file.
|
||||||
|
- **Hierarchical Management**: Automatically handles conversion between flat storage and nested TOML structure.
|
||||||
- **Path Validation**: Configuration paths are validated to ensure they contain only valid TOML key segments.
|
- **Path Validation**: Configuration paths are validated to ensure they contain only valid TOML key segments.
|
||||||
- **Atomic Saving**: Configuration is written to a temporary file first, then atomically renamed.
|
- **Atomic Saving**: Configuration is written to a temporary file first, then atomically renamed.
|
||||||
- **CLI Argument Types**: Command-line values are automatically parsed into bool, int64, float64, or string. See note below on Type Handling.
|
- **CLI Argument Types**: Command-line values are automatically parsed into bool, int64, float64, or string.
|
||||||
|
- **Struct Unmarshaling**: The `UnmarshalSubtree` method uses `mapstructure` to decode configuration subtrees into Go structs.
|
||||||
|
|
||||||
### Naming Conventions
|
### Naming Conventions
|
||||||
|
|
||||||
- **Paths**: Configuration paths provided to `Register` (e.g., `"server.port"`) are dot-separated strings.
|
- **Paths**: Configuration paths provided to `Register` (e.g., `"server.port"`) are dot-separated strings.
|
||||||
- **Segments**: Each part of the path between dots (a "segment") must adhere to TOML key naming rules:
|
- **Segments**: Each part of the path between dots (a "segment") must adhere to TOML key naming rules:
|
||||||
- Must start with a letter (a-z, A-Z) or an underscore (`_`).
|
- Must start with a letter (a-z, A-Z) or an underscore (`_`).
|
||||||
- Subsequent characters can be letters, numbers (0-9), underscores (`_`), or hyphens (`-`).
|
- Subsequent characters can be letters, numbers (0-9), underscores (`_`), or hyphens (`-`).
|
||||||
- Segments *cannot* contain dots (`.`).
|
- Segments *cannot* contain dots (`.`).
|
||||||
|
|
||||||
### Type Handling Note
|
### Type Handling Note
|
||||||
|
|
||||||
- Values loaded from TOML files or parsed from CLI arguments often result in specific types (e.g., `int64` for integers, `float64` for floats) due to the underlying `tinytoml` and `strconv` packages.
|
- Values loaded from TOML files or parsed from CLI arguments often result in specific types (e.g., `int64` for integers, `float64` for floats) due to the underlying `tinytoml` and `strconv` packages.
|
||||||
- This might differ from the type of a default value provided during `Register` (e.g., default `int(8080)` vs. loaded `int64(8080)`).
|
- This might differ from the type of a default value provided during `Register` (e.g., default `int(8080)` vs. loaded `int64(8080)`).
|
||||||
- When retrieving values using `Get`, be mindful of this potential difference and use appropriate type assertions or checks. Consider using `int64` or `float64` for default values where applicable to maintain consistency.
|
- When retrieving values using `Get`, be mindful of this potential difference and use appropriate type assertions or checks. Consider using `int64` or `float64` for default values where applicable to maintain consistency.
|
||||||
|
- Alternatively, use the type-specific accessors (`Int64`, `Bool`, `Float64`, `String`) which attempt to convert values to the desired type and provide detailed error messages if conversion fails.
|
||||||
|
|
||||||
### Merge Behavior Note
|
### Merge Behavior Note
|
||||||
|
|
||||||
|
|||||||
378
cmd/main.go
378
cmd/main.go
@ -1,183 +1,257 @@
|
|||||||
|
// Test program for the config package
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"github.com/LixenWraith/config"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/LixenWraith/config"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
// LogConfig represents logging configuration parameters
|
||||||
initialConfigFile = "config_initial.toml"
|
type LogConfig struct {
|
||||||
finalConfigFile = "config_final.toml"
|
// Basic settings
|
||||||
)
|
Level int64 `toml:"level"`
|
||||||
|
Name string `toml:"name"`
|
||||||
// Sample TOML content for the initial configuration file
|
Directory string `toml:"directory"`
|
||||||
var initialTomlContent = `
|
Format string `toml:"format"` // "txt" or "json"
|
||||||
debug = true
|
Extension string `toml:"extension"`
|
||||||
log_level = "info" # This will be overridden by default
|
// Formatting
|
||||||
|
ShowTimestamp bool `toml:"show_timestamp"`
|
||||||
[server]
|
ShowLevel bool `toml:"show_level"`
|
||||||
host = "localhost"
|
// Buffer and size limits
|
||||||
port = 8080 # This will be overridden by CLI
|
BufferSize int64 `toml:"buffer_size"` // Channel buffer size
|
||||||
|
MaxSizeMB int64 `toml:"max_size_mb"` // Max size per log file
|
||||||
[smtp]
|
MaxTotalSizeMB int64 `toml:"max_total_size_mb"` // Max total size of all logs in dir
|
||||||
host = "mail.example.com" # This will be overridden by CLI
|
MinDiskFreeMB int64 `toml:"min_disk_free_mb"` // Minimum free disk space required
|
||||||
port = 587
|
// Timers
|
||||||
auth_user = "file_user"
|
FlushIntervalMs int64 `toml:"flush_interval_ms"` // Interval for flushing file buffer
|
||||||
# auth_pass is missing, will use default
|
TraceDepth int64 `toml:"trace_depth"` // Default trace depth (0-10)
|
||||||
`
|
RetentionPeriodHrs float64 `toml:"retention_period_hrs"` // Hours to keep logs (0=disabled)
|
||||||
|
RetentionCheckMins float64 `toml:"retention_check_mins"` // How often to check retention
|
||||||
|
// Disk check settings
|
||||||
|
DiskCheckIntervalMs int64 `toml:"disk_check_interval_ms"` // Base interval for disk checks
|
||||||
|
EnableAdaptiveInterval bool `toml:"enable_adaptive_interval"` // Adjust interval based on log rate
|
||||||
|
MinCheckIntervalMs int64 `toml:"min_check_interval_ms"` // Minimum adaptive interval
|
||||||
|
MaxCheckIntervalMs int64 `toml:"max_check_interval_ms"` // Maximum adaptive interval
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// --- Setup: Create a temporary directory for config files ---
|
// Create a temporary file path for our test
|
||||||
tempDir, err := os.MkdirTemp("", "config_example")
|
tempDir := os.TempDir()
|
||||||
|
configPath := filepath.Join(tempDir, "logconfig_test.toml")
|
||||||
|
|
||||||
|
// Clean up any existing file from previous runs
|
||||||
|
os.Remove(configPath)
|
||||||
|
|
||||||
|
fmt.Println("=== LogConfig Test Program ===")
|
||||||
|
fmt.Printf("Using temporary config file: %s\n\n", configPath)
|
||||||
|
|
||||||
|
// Initialize the Config instance
|
||||||
|
cfg := config.New()
|
||||||
|
|
||||||
|
// Register default values for all LogConfig fields
|
||||||
|
registerLogConfigDefaults(cfg)
|
||||||
|
|
||||||
|
// Load the configuration (will use defaults since file doesn't exist yet)
|
||||||
|
exists, err := cfg.Load(configPath, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to create temp dir: %v", err)
|
fmt.Printf("Error loading config: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tempDir) // Clean up temp directory
|
fmt.Printf("Config file exists: %v (expected: false)\n", exists)
|
||||||
|
|
||||||
initialPath := filepath.Join(tempDir, initialConfigFile)
|
// Unmarshal into LogConfig struct
|
||||||
finalPath := filepath.Join(tempDir, finalConfigFile)
|
var logConfig LogConfig
|
||||||
|
err = cfg.UnmarshalSubtree("log", &logConfig)
|
||||||
// Write the initial TOML config file
|
|
||||||
err = os.WriteFile(initialPath, []byte(initialTomlContent), 0644)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to write initial config file: %v", err)
|
fmt.Printf("Error unmarshaling config: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
fmt.Printf("Wrote initial config to: %s\n", initialPath)
|
|
||||||
|
|
||||||
// --- Step 1: Create and Register Configuration ---
|
// Print current values
|
||||||
fmt.Println("\n--- Step 1: Initialize Config and Register Keys ---")
|
fmt.Println("\n=== Default Configuration Values ===")
|
||||||
c := config.New()
|
printLogConfig(logConfig)
|
||||||
|
|
||||||
// Register keys and store their UUIDs
|
// Modify some values
|
||||||
// Provide default values that might be overridden by file or CLI
|
fmt.Println("\n=== Modifying Configuration Values ===")
|
||||||
keyDebug, err := c.Register("debug", false) // Default false, overridden by file
|
fmt.Println("Changing:")
|
||||||
handleErr(err)
|
fmt.Println(" - level: 1 → 2")
|
||||||
keyLogLevel, err := c.Register("log_level", "warn") // Default warn, file has "info"
|
fmt.Println(" - name: default_logger → modified_logger")
|
||||||
handleErr(err)
|
fmt.Println(" - format: txt → json")
|
||||||
keyServerHost, err := c.Register("server.host", "127.0.0.1") // Default 127.0.0.1, overridden by file
|
fmt.Println(" - max_size_mb: 10 → 50")
|
||||||
handleErr(err)
|
fmt.Println(" - retention_period_hrs: 24.0 → 72.0")
|
||||||
keyServerPort, err := c.Register("server.port", 9090) // Default 9090, file 8080, CLI 9999
|
fmt.Println(" - enable_adaptive_interval: false → true")
|
||||||
handleErr(err)
|
|
||||||
keySmtpHost, err := c.Register("smtp.host", "default.mail.com") // Default, file mail.example.com, CLI override.mail.com
|
|
||||||
handleErr(err)
|
|
||||||
keySmtpPort, err := c.Register("smtp.port", 25) // Default 25, overridden by file
|
|
||||||
handleErr(err)
|
|
||||||
keySmtpUser, err := c.Register("smtp.auth_user", "default_user") // Default, overridden by file
|
|
||||||
handleErr(err)
|
|
||||||
keySmtpPass, err := c.Register("smtp.auth_pass", "default_pass") // Default, not in file or CLI
|
|
||||||
handleErr(err)
|
|
||||||
keyNewCliFlag, err := c.Register("new_cli_flag", false) // Default false, set true by CLI
|
|
||||||
handleErr(err)
|
|
||||||
keyOnlyDefault, err := c.Register("only_default", "this_is_the_default") // Only has a default value
|
|
||||||
handleErr(err)
|
|
||||||
|
|
||||||
fmt.Println("Registered configuration keys with defaults.")
|
cfg.Set("log.level", int64(2))
|
||||||
fmt.Printf(" - debug (default: false): %s\n", keyDebug)
|
cfg.Set("log.name", "modified_logger")
|
||||||
fmt.Printf(" - server.port (default: 9090): %s\n", keyServerPort)
|
cfg.Set("log.format", "json")
|
||||||
fmt.Printf(" - smtp.auth_pass (default: 'default_pass'): %s\n", keySmtpPass)
|
cfg.Set("log.max_size_mb", int64(50))
|
||||||
fmt.Printf(" - only_default (default: 'this_is_the_default'): %s\n", keyOnlyDefault)
|
cfg.Set("log.retention_period_hrs", 72.0)
|
||||||
|
cfg.Set("log.enable_adaptive_interval", true)
|
||||||
|
|
||||||
// --- Step 2: Load Configuration from File and CLI Args ---
|
// Save the configuration
|
||||||
fmt.Println("\n--- Step 2: Load from File and CLI ---")
|
err = cfg.Save(configPath)
|
||||||
// Simulate command-line arguments
|
if err != nil {
|
||||||
cliArgs := []string{
|
fmt.Printf("Error saving config: %v\n", err)
|
||||||
"--server.port", "9999", // Override file value
|
os.Exit(1)
|
||||||
"--smtp.host", "override.mail.com", // Override file value
|
|
||||||
"--new_cli_flag", // Set boolean flag to true
|
|
||||||
"--unregistered.cli.arg", "some_value", // This will be loaded but not accessible via registered Get
|
|
||||||
}
|
}
|
||||||
fmt.Printf("Simulated CLI Args: %v\n", cliArgs)
|
fmt.Printf("\nSaved configuration to: %s\n", configPath)
|
||||||
|
|
||||||
foundFile, err := c.Load(initialPath, cliArgs)
|
// Read the file to verify it contains the expected values
|
||||||
handleErr(err)
|
fileBytes, err := os.ReadFile(configPath)
|
||||||
fmt.Printf("Config file loaded: %t\n", foundFile)
|
if err != nil {
|
||||||
|
fmt.Printf("Error reading config file: %v\n", err)
|
||||||
// --- Step 3: Access Merged Configuration Values ---
|
os.Exit(1)
|
||||||
fmt.Println("\n--- Step 3: Access Merged Values via Get() ---")
|
|
||||||
|
|
||||||
// Retrieve values using the UUID keys. Observe the precedence: Default -> File -> CLI
|
|
||||||
debugVal, _ := c.Get(keyDebug) // Expect: true (from file)
|
|
||||||
logLevelVal, _ := c.Get(keyLogLevel) // Expect: "info" (from file)
|
|
||||||
serverHostVal, _ := c.Get(keyServerHost) // Expect: "localhost" (from file)
|
|
||||||
serverPortVal, _ := c.Get(keyServerPort) // Expect: 9999 (int64 from CLI)
|
|
||||||
smtpHostVal, _ := c.Get(keySmtpHost) // Expect: "override.mail.com" (from CLI)
|
|
||||||
smtpPortVal, _ := c.Get(keySmtpPort) // Expect: 587 (int64 from file - parseArgs converts numbers)
|
|
||||||
smtpUserVal, _ := c.Get(keySmtpUser) // Expect: "file_user" (from file)
|
|
||||||
smtpPassVal, _ := c.Get(keySmtpPass) // Expect: "default_pass" (only default exists)
|
|
||||||
newCliFlagVal, _ := c.Get(keyNewCliFlag) // Expect: true (from CLI flag)
|
|
||||||
onlyDefaultVal, _ := c.Get(keyOnlyDefault) // Expect: "this_is_the_default" (only default exists)
|
|
||||||
_, registered := c.Get("nonexistent-uuid") // Expect: registered = false
|
|
||||||
|
|
||||||
fmt.Printf("Debug (File): %v (%T)\n", debugVal, debugVal)
|
|
||||||
fmt.Printf("LogLevel (File): %v (%T)\n", logLevelVal, logLevelVal)
|
|
||||||
fmt.Printf("Server Host (File): %v (%T)\n", serverHostVal, serverHostVal)
|
|
||||||
fmt.Printf("Server Port (CLI) : %v (%T)\n", serverPortVal, serverPortVal)
|
|
||||||
fmt.Printf("SMTP Host (CLI) : %v (%T)\n", smtpHostVal, smtpHostVal)
|
|
||||||
fmt.Printf("SMTP Port (File): %v (%T)\n", smtpPortVal, smtpPortVal) // Note: parseArgs converts to int64
|
|
||||||
fmt.Printf("SMTP User (File): %v (%T)\n", smtpUserVal, smtpUserVal)
|
|
||||||
fmt.Printf("SMTP Pass (Default): %v (%T)\n", smtpPassVal, smtpPassVal)
|
|
||||||
fmt.Printf("New CLI Flag (CLI) : %v (%T)\n", newCliFlagVal, newCliFlagVal)
|
|
||||||
fmt.Printf("Only Default (Default): %v (%T)\n", onlyDefaultVal, onlyDefaultVal)
|
|
||||||
fmt.Printf("Unregistered UUID : Found=%t\n", registered)
|
|
||||||
|
|
||||||
// --- Step 4: Save the Final Configuration ---
|
|
||||||
fmt.Println("\n--- Step 4: Save Final Configuration ---")
|
|
||||||
err = c.Save(finalPath)
|
|
||||||
handleErr(err)
|
|
||||||
fmt.Printf("Saved final merged configuration to: %s\n", finalPath)
|
|
||||||
|
|
||||||
// Optional: Print the content of the saved file
|
|
||||||
savedContent, _ := os.ReadFile(finalPath)
|
|
||||||
fmt.Println("--- Content of saved file (config_final.toml): ---")
|
|
||||||
fmt.Println(string(savedContent))
|
|
||||||
fmt.Println("-------------------------------------------------")
|
|
||||||
|
|
||||||
// --- Step 5: Load Saved Config into a New Instance and Compare ---
|
|
||||||
fmt.Println("\n--- Step 5: Reload Saved Config and Verify ---")
|
|
||||||
c2 := config.New()
|
|
||||||
|
|
||||||
// NOTE: For c2 to use Get(), keys would need to be registered again.
|
|
||||||
// For simple verification here, we load and access the underlying map.
|
|
||||||
// In a real app, you'd likely register keys consistently at startup.
|
|
||||||
foundSavedFile, err := c2.Load(finalPath, nil) // Load without CLI args this time
|
|
||||||
handleErr(err)
|
|
||||||
if !foundSavedFile {
|
|
||||||
log.Fatalf("Failed to find the saved config file '%s' for reloading", finalPath)
|
|
||||||
}
|
}
|
||||||
fmt.Println("Reloaded final config into a new instance.")
|
|
||||||
|
|
||||||
// Directly compare some values from the internal data map for verification
|
fmt.Println("\n=== Generated TOML File Contents ===")
|
||||||
// This requires accessing the unexported 'data' field via a helper or reflection,
|
fmt.Println(string(fileBytes))
|
||||||
// OR rely on saving/loading being correct (which is what we test here).
|
|
||||||
// Let's assume Save/Load worked and verify the expected final values are present after load.
|
|
||||||
|
|
||||||
// We need to register keys again in c2 to use Get() for comparison
|
// Load the config again to verify it can be read back correctly
|
||||||
key2ServerPort, _ := c2.Register("server.port", 0) // Default doesn't matter now
|
exists, err = cfg.Load(configPath, nil)
|
||||||
key2SmtpHost, _ := c2.Register("smtp.host", "")
|
if err != nil {
|
||||||
key2NewCliFlag, _ := c2.Register("new_cli_flag", false)
|
fmt.Printf("Error reloading config: %v\n", err)
|
||||||
key2Unregistered, _ := c2.Register("unregistered.cli.arg", "") // Register the CLI-only arg
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("\nConfig file exists: %v (expected: true)\n", exists)
|
||||||
|
|
||||||
reloadedPort, _ := c2.Get(key2ServerPort)
|
// Unmarshal into a new LogConfig to verify loaded values
|
||||||
reloadedSmtpHost, _ := c2.Get(key2SmtpHost)
|
var loadedConfig LogConfig
|
||||||
reloadedCliFlag, _ := c2.Get(key2NewCliFlag)
|
err = cfg.UnmarshalSubtree("log", &loadedConfig)
|
||||||
reloadedUnregistered, _ := c2.Get(key2Unregistered) // Get the value added only via CLI initially
|
if err != nil {
|
||||||
|
fmt.Printf("Error unmarshaling reloaded config: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("Comparing reloaded values:")
|
fmt.Println("\n=== Loaded Configuration Values ===")
|
||||||
fmt.Printf(" - Reloaded Server Port: %v (Expected: 9999) - Match: %t\n", reloadedPort, reloadedPort == int64(9999))
|
printLogConfig(loadedConfig)
|
||||||
fmt.Printf(" - Reloaded SMTP Host : %v (Expected: override.mail.com) - Match: %t\n", reloadedSmtpHost, reloadedSmtpHost == "override.mail.com")
|
|
||||||
fmt.Printf(" - Reloaded CLI Flag : %v (Expected: true) - Match: %t\n", reloadedCliFlag, reloadedCliFlag == true)
|
|
||||||
fmt.Printf(" - Reloaded Unreg Arg : %v (Expected: some_value) - Match: %t\n", reloadedUnregistered, reloadedUnregistered == "some_value")
|
|
||||||
|
|
||||||
fmt.Println("\n--- Example Finished ---")
|
// Verify specific values were changed correctly
|
||||||
|
fmt.Println("\n=== Verification ===")
|
||||||
|
verifyConfig(loadedConfig)
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
os.Remove(configPath)
|
||||||
|
fmt.Println("\nCleanup: Temporary file removed.")
|
||||||
|
fmt.Println("\n=== Test Complete ===")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple error handler
|
// registerLogConfigDefaults registers all default values for the LogConfig struct
|
||||||
func handleErr(err error) {
|
func registerLogConfigDefaults(cfg *config.Config) {
|
||||||
if err != nil {
|
fmt.Println("Registering default values...")
|
||||||
log.Fatalf("Error: %v", err)
|
|
||||||
|
// Basic settings
|
||||||
|
cfg.Register("log.level", int64(1))
|
||||||
|
cfg.Register("log.name", "default_logger")
|
||||||
|
cfg.Register("log.directory", "./logs")
|
||||||
|
cfg.Register("log.format", "txt")
|
||||||
|
cfg.Register("log.extension", ".log")
|
||||||
|
|
||||||
|
// Formatting
|
||||||
|
cfg.Register("log.show_timestamp", true)
|
||||||
|
cfg.Register("log.show_level", true)
|
||||||
|
|
||||||
|
// Buffer and size limits
|
||||||
|
cfg.Register("log.buffer_size", int64(1000))
|
||||||
|
cfg.Register("log.max_size_mb", int64(10))
|
||||||
|
cfg.Register("log.max_total_size_mb", int64(100))
|
||||||
|
cfg.Register("log.min_disk_free_mb", int64(500))
|
||||||
|
|
||||||
|
// Timers
|
||||||
|
cfg.Register("log.flush_interval_ms", int64(1000))
|
||||||
|
cfg.Register("log.trace_depth", int64(3))
|
||||||
|
cfg.Register("log.retention_period_hrs", 24.0)
|
||||||
|
cfg.Register("log.retention_check_mins", 15.0)
|
||||||
|
|
||||||
|
// Disk check settings
|
||||||
|
cfg.Register("log.disk_check_interval_ms", int64(60000))
|
||||||
|
cfg.Register("log.enable_adaptive_interval", false)
|
||||||
|
cfg.Register("log.min_check_interval_ms", int64(5000))
|
||||||
|
cfg.Register("log.max_check_interval_ms", int64(300000))
|
||||||
|
}
|
||||||
|
|
||||||
|
// printLogConfig prints the values of a LogConfig struct
|
||||||
|
func printLogConfig(cfg LogConfig) {
|
||||||
|
fmt.Println("Basic settings:")
|
||||||
|
fmt.Printf(" - Level: %d\n", cfg.Level)
|
||||||
|
fmt.Printf(" - Name: %s\n", cfg.Name)
|
||||||
|
fmt.Printf(" - Directory: %s\n", cfg.Directory)
|
||||||
|
fmt.Printf(" - Format: %s\n", cfg.Format)
|
||||||
|
fmt.Printf(" - Extension: %s\n", cfg.Extension)
|
||||||
|
|
||||||
|
fmt.Println("Formatting:")
|
||||||
|
fmt.Printf(" - ShowTimestamp: %t\n", cfg.ShowTimestamp)
|
||||||
|
fmt.Printf(" - ShowLevel: %t\n", cfg.ShowLevel)
|
||||||
|
|
||||||
|
fmt.Println("Buffer and size limits:")
|
||||||
|
fmt.Printf(" - BufferSize: %d\n", cfg.BufferSize)
|
||||||
|
fmt.Printf(" - MaxSizeMB: %d\n", cfg.MaxSizeMB)
|
||||||
|
fmt.Printf(" - MaxTotalSizeMB: %d\n", cfg.MaxTotalSizeMB)
|
||||||
|
fmt.Printf(" - MinDiskFreeMB: %d\n", cfg.MinDiskFreeMB)
|
||||||
|
|
||||||
|
fmt.Println("Timers:")
|
||||||
|
fmt.Printf(" - FlushIntervalMs: %d\n", cfg.FlushIntervalMs)
|
||||||
|
fmt.Printf(" - TraceDepth: %d\n", cfg.TraceDepth)
|
||||||
|
fmt.Printf(" - RetentionPeriodHrs: %.1f\n", cfg.RetentionPeriodHrs)
|
||||||
|
fmt.Printf(" - RetentionCheckMins: %.1f\n", cfg.RetentionCheckMins)
|
||||||
|
|
||||||
|
fmt.Println("Disk check settings:")
|
||||||
|
fmt.Printf(" - DiskCheckIntervalMs: %d\n", cfg.DiskCheckIntervalMs)
|
||||||
|
fmt.Printf(" - EnableAdaptiveInterval: %t\n", cfg.EnableAdaptiveInterval)
|
||||||
|
fmt.Printf(" - MinCheckIntervalMs: %d\n", cfg.MinCheckIntervalMs)
|
||||||
|
fmt.Printf(" - MaxCheckIntervalMs: %d\n", cfg.MaxCheckIntervalMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyConfig checks if the modified values were set correctly
|
||||||
|
func verifyConfig(cfg LogConfig) {
|
||||||
|
allCorrect := true
|
||||||
|
|
||||||
|
// Check each modified value
|
||||||
|
if cfg.Level != 2 {
|
||||||
|
fmt.Printf("ERROR: Level is %d, expected 2\n", cfg.Level)
|
||||||
|
allCorrect = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Name != "modified_logger" {
|
||||||
|
fmt.Printf("ERROR: Name is %s, expected 'modified_logger'\n", cfg.Name)
|
||||||
|
allCorrect = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Format != "json" {
|
||||||
|
fmt.Printf("ERROR: Format is %s, expected 'json'\n", cfg.Format)
|
||||||
|
allCorrect = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.MaxSizeMB != 50 {
|
||||||
|
fmt.Printf("ERROR: MaxSizeMB is %d, expected 50\n", cfg.MaxSizeMB)
|
||||||
|
allCorrect = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.RetentionPeriodHrs != 72.0 {
|
||||||
|
fmt.Printf("ERROR: RetentionPeriodHrs is %.1f, expected 72.0\n", cfg.RetentionPeriodHrs)
|
||||||
|
allCorrect = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg.EnableAdaptiveInterval {
|
||||||
|
fmt.Printf("ERROR: EnableAdaptiveInterval is %t, expected true\n", cfg.EnableAdaptiveInterval)
|
||||||
|
allCorrect = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that unmodified values retained their defaults
|
||||||
|
if cfg.Directory != "./logs" {
|
||||||
|
fmt.Printf("ERROR: Directory changed to %s, expected './logs'\n", cfg.Directory)
|
||||||
|
allCorrect = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.BufferSize != 1000 {
|
||||||
|
fmt.Printf("ERROR: BufferSize changed to %d, expected 1000\n", cfg.BufferSize)
|
||||||
|
allCorrect = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if allCorrect {
|
||||||
|
fmt.Println("SUCCESS: All configuration values match expected values!")
|
||||||
|
} else {
|
||||||
|
fmt.Println("FAILURE: Some configuration values don't match expected values!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
503
config.go
503
config.go
@ -1,3 +1,5 @@
|
|||||||
|
// Package config provides thread-safe configuration management for Go applications
|
||||||
|
// with support for TOML files, command-line overrides, and default values.
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -6,124 +8,228 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync" // Added sync import
|
"sync"
|
||||||
|
|
||||||
"github.com/LixenWraith/tinytoml"
|
"github.com/LixenWraith/tinytoml"
|
||||||
"github.com/google/uuid" // Added uuid import
|
"github.com/mitchellh/mapstructure"
|
||||||
)
|
)
|
||||||
|
|
||||||
// registeredItem holds metadata for a configuration value registered for access.
|
// configItem holds both the default and current value for a configuration path
|
||||||
type registeredItem struct {
|
type configItem struct {
|
||||||
path string // Dot-separated path (e.g., "server.port")
|
|
||||||
defaultValue any
|
defaultValue any
|
||||||
|
currentValue any
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config manages application configuration loaded from files and CLI arguments.
|
// Config manages application configuration loaded from files and CLI arguments.
|
||||||
// It provides thread-safe access to configuration values.
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
data map[string]any // Stores the actual configuration data (nested map)
|
items map[string]configItem // Maps paths to config items (default and current values)
|
||||||
registry map[string]registeredItem // Maps generated UUIDs to registered items
|
mutex sync.RWMutex // Protects concurrent access
|
||||||
mutex sync.RWMutex // Protects concurrent access to data and registry
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates and initializes a new Config instance.
|
// New creates and initializes a new Config instance.
|
||||||
func New() *Config {
|
func New() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
data: make(map[string]any),
|
items: make(map[string]configItem),
|
||||||
registry: make(map[string]registeredItem),
|
|
||||||
// mutex is implicitly initialized
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register makes a configuration path known to the Config instance and returns a unique key (UUID) for accessing it.
|
// Register makes a configuration path known to the Config instance.
|
||||||
// The path should be dot-separated (e.g., "server.port", "debug").
|
// The path should be dot-separated (e.g., "server.port", "debug").
|
||||||
// Each segment of the path must be a valid TOML key identifier.
|
// Each segment of the path must be a valid TOML key identifier.
|
||||||
// defaultValue is returned by Get if the value is not found in the loaded configuration.
|
// defaultValue is the value returned by Get if no specific value has been set.
|
||||||
func (c *Config) Register(path string, defaultValue any) (string, error) {
|
func (c *Config) Register(path string, defaultValue any) error {
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return "", fmt.Errorf("registration path cannot be empty")
|
return fmt.Errorf("registration path cannot be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate path segments
|
// Validate path segments
|
||||||
segments := strings.Split(path, ".")
|
segments := strings.Split(path, ".")
|
||||||
for _, segment := range segments {
|
for _, segment := range segments {
|
||||||
// tinytoml.isValidKey doesn't exist, but we can use its logic criteria.
|
|
||||||
// Assuming isValidKey checks for alphanumeric, underscore, dash, starting with letter/underscore.
|
|
||||||
// We adapt the validation logic here based on tinytoml's description.
|
|
||||||
if !isValidKeySegment(segment) {
|
if !isValidKeySegment(segment) {
|
||||||
return "", fmt.Errorf("invalid path segment %q in path %q", segment, path)
|
return fmt.Errorf("invalid path segment %q in path %q", segment, path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
newUUID := uuid.NewString()
|
c.items[path] = configItem{
|
||||||
item := registeredItem{
|
|
||||||
path: path,
|
|
||||||
defaultValue: defaultValue,
|
defaultValue: defaultValue,
|
||||||
|
currentValue: defaultValue, // Initially set to default
|
||||||
}
|
}
|
||||||
c.registry[newUUID] = item
|
|
||||||
|
|
||||||
return newUUID, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unregister removes a configuration key from the registry.
|
// Unregister removes a configuration path and all its children.
|
||||||
// Subsequent calls to Get with this key will return (nil, false).
|
func (c *Config) Unregister(path string) error {
|
||||||
// This does not remove the value from the underlying configuration data map,
|
|
||||||
// only the ability to access it via this specific registration key.
|
|
||||||
func (c *Config) Unregister(key string) {
|
|
||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
delete(c.registry, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// isValidKeySegment checks if a single path segment is valid.
|
if _, exists := c.items[path]; !exists {
|
||||||
// Adapts the logic described for tinytoml's isValidKey.
|
return fmt.Errorf("path not registered: %s", path)
|
||||||
func isValidKeySegment(s string) bool {
|
|
||||||
if len(s) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
firstChar := rune(s[0])
|
|
||||||
// Using simplified check: must not contain dots and must be valid TOML key part
|
// Remove the path itself
|
||||||
if strings.ContainsRune(s, '.') {
|
delete(c.items, path)
|
||||||
return false // Segments themselves cannot contain dots
|
|
||||||
}
|
// Remove any child paths
|
||||||
if !isAlpha(firstChar) && firstChar != '_' {
|
prefix := path + "."
|
||||||
return false
|
for childPath := range c.items {
|
||||||
}
|
if strings.HasPrefix(childPath, prefix) {
|
||||||
for _, r := range s[1:] {
|
delete(c.items, childPath)
|
||||||
if !isAlpha(r) && !isNumeric(r) && r != '-' && r != '_' {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get retrieves a configuration value using the unique key (UUID) obtained from Register.
|
// Get retrieves a configuration value using the path.
|
||||||
// It returns the value found in the loaded configuration or the registered default value.
|
// It returns the current value (or default if not explicitly set).
|
||||||
// The second return value (bool) indicates if the key was successfully registered (true) or not (false).
|
// The second return value indicates if the path was registered.
|
||||||
func (c *Config) Get(key string) (any, bool) {
|
func (c *Config) Get(path string) (any, bool) {
|
||||||
c.mutex.RLock()
|
c.mutex.RLock()
|
||||||
item, registered := c.registry[key]
|
defer c.mutex.RUnlock()
|
||||||
|
|
||||||
|
item, registered := c.items[path]
|
||||||
if !registered {
|
if !registered {
|
||||||
c.mutex.RUnlock()
|
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lookup value in the data map using the item's path
|
return item.currentValue, true
|
||||||
value, found := getValueFromMap(c.data, item.path)
|
}
|
||||||
c.mutex.RUnlock() // Unlock after accessing both registry and data
|
|
||||||
|
|
||||||
if found {
|
// Set updates a configuration value for the given path.
|
||||||
return value, true
|
// It returns an error if the path is not registered.
|
||||||
|
func (c *Config) Set(path string, value any) error {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
item, registered := c.items[path]
|
||||||
|
if !registered {
|
||||||
|
return fmt.Errorf("path %s is not registered", path)
|
||||||
}
|
}
|
||||||
// Key was registered, but value not found in data, return default
|
|
||||||
return item.defaultValue, true
|
item.currentValue = value
|
||||||
|
c.items[path] = item
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String retrieves a string configuration value using the path.
|
||||||
|
func (c *Config) String(path string) (string, error) {
|
||||||
|
val, found := c.Get(path)
|
||||||
|
if !found {
|
||||||
|
return "", fmt.Errorf("path not registered: %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strVal, ok := val.(string); ok {
|
||||||
|
return strVal, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to convert other types to string
|
||||||
|
switch v := val.(type) {
|
||||||
|
case fmt.Stringer:
|
||||||
|
return v.String(), nil
|
||||||
|
case error:
|
||||||
|
return v.Error(), nil
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%v", val), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int64 retrieves an int64 configuration value using the path.
|
||||||
|
func (c *Config) Int64(path string) (int64, error) {
|
||||||
|
val, found := c.Get(path)
|
||||||
|
if !found {
|
||||||
|
return 0, fmt.Errorf("path not registered: %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type assertion
|
||||||
|
if intVal, ok := val.(int64); ok {
|
||||||
|
return intVal, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to convert other numeric types
|
||||||
|
switch v := val.(type) {
|
||||||
|
case int:
|
||||||
|
return int64(v), nil
|
||||||
|
case float64:
|
||||||
|
return int64(v), nil
|
||||||
|
case string:
|
||||||
|
if i, err := strconv.ParseInt(v, 10, 64); err == nil {
|
||||||
|
return i, nil
|
||||||
|
} else {
|
||||||
|
return 0, fmt.Errorf("cannot convert string '%s' to int64: %w", v, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, fmt.Errorf("cannot convert %T to int64", val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bool retrieves a boolean configuration value using the path.
|
||||||
|
func (c *Config) Bool(path string) (bool, error) {
|
||||||
|
val, found := c.Get(path)
|
||||||
|
if !found {
|
||||||
|
return false, fmt.Errorf("path not registered: %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type assertion
|
||||||
|
if boolVal, ok := val.(bool); ok {
|
||||||
|
return boolVal, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to convert string to bool
|
||||||
|
if strVal, ok := val.(string); ok {
|
||||||
|
if b, err := strconv.ParseBool(strVal); err == nil {
|
||||||
|
return b, nil
|
||||||
|
} else {
|
||||||
|
return false, fmt.Errorf("cannot convert string '%s' to bool: %w", strVal, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to interpret numbers
|
||||||
|
switch v := val.(type) {
|
||||||
|
case int:
|
||||||
|
return v != 0, nil
|
||||||
|
case int64:
|
||||||
|
return v != 0, nil
|
||||||
|
case float64:
|
||||||
|
return v != 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, fmt.Errorf("cannot convert %T to bool", val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float64 retrieves a float64 configuration value using the path.
|
||||||
|
func (c *Config) Float64(path string) (float64, error) {
|
||||||
|
val, found := c.Get(path)
|
||||||
|
if !found {
|
||||||
|
return 0.0, fmt.Errorf("path not registered: %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type assertion
|
||||||
|
if floatVal, ok := val.(float64); ok {
|
||||||
|
return floatVal, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to convert other numeric types
|
||||||
|
switch v := val.(type) {
|
||||||
|
case int:
|
||||||
|
return float64(v), nil
|
||||||
|
case int64:
|
||||||
|
return float64(v), nil
|
||||||
|
case string:
|
||||||
|
if f, err := strconv.ParseFloat(v, 64); err == nil {
|
||||||
|
return f, nil
|
||||||
|
} else {
|
||||||
|
return 0.0, fmt.Errorf("cannot convert string '%s' to float64: %w", v, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0.0, fmt.Errorf("cannot convert %T to float64", val)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load reads configuration from a TOML file and merges overrides from command-line arguments.
|
// Load reads configuration from a TOML file and merges overrides from command-line arguments.
|
||||||
// It populates the Config instance's internal data map.
|
|
||||||
// 'args' should be the command-line arguments (e.g., os.Args[1:]).
|
// 'args' should be the command-line arguments (e.g., os.Args[1:]).
|
||||||
// Returns true if the configuration file was found and loaded, false otherwise.
|
// Returns true if the configuration file was found and loaded, false otherwise.
|
||||||
func (c *Config) Load(path string, args []string) (bool, error) {
|
func (c *Config) Load(path string, args []string) (bool, error) {
|
||||||
@ -131,7 +237,9 @@ func (c *Config) Load(path string, args []string) (bool, error) {
|
|||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
configExists := false
|
configExists := false
|
||||||
loadedData := make(map[string]any) // Load into a temporary map first
|
|
||||||
|
// First, build a nested map for file data (if it exists)
|
||||||
|
nestedData := make(map[string]any)
|
||||||
|
|
||||||
if stat, err := os.Stat(path); err == nil && !stat.IsDir() {
|
if stat, err := os.Stat(path); err == nil && !stat.IsDir() {
|
||||||
configExists = true
|
configExists = true
|
||||||
@ -140,42 +248,61 @@ func (c *Config) Load(path string, args []string) (bool, error) {
|
|||||||
return false, fmt.Errorf("failed to read config file '%s': %w", path, err)
|
return false, fmt.Errorf("failed to read config file '%s': %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use tinytoml to unmarshal directly into the map
|
if err := tinytoml.Unmarshal(fileData, &nestedData); err != nil {
|
||||||
// Pass a pointer to the map for Unmarshal
|
return false, fmt.Errorf("failed to parse TOML config file '%s': %w", path, err)
|
||||||
if err := tinytoml.Unmarshal(fileData, &loadedData); err != nil {
|
|
||||||
return false, fmt.Errorf("failed to parse config file '%s': %w", path, err)
|
|
||||||
}
|
}
|
||||||
} else if !os.IsNotExist(err) {
|
} else if !os.IsNotExist(err) {
|
||||||
// Handle potential errors from os.Stat other than file not existing
|
|
||||||
return false, fmt.Errorf("failed to check config file '%s': %w", path, err)
|
return false, fmt.Errorf("failed to check config file '%s': %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge loaded data into the main config data
|
// Flatten the nested map into path->value pairs
|
||||||
// This ensures existing data (e.g. from defaults set programmatically before load) isn't wiped out
|
flattenedData := flattenMap(nestedData, "")
|
||||||
mergeMaps(c.data, loadedData)
|
|
||||||
|
|
||||||
// Parse and merge CLI arguments if any
|
// Parse CLI arguments if any
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
overrides, err := parseArgs(args)
|
cliOverrides, err := parseArgs(args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return configExists, fmt.Errorf("failed to parse CLI args: %w", err)
|
return configExists, fmt.Errorf("failed to parse CLI args: %w", err)
|
||||||
}
|
}
|
||||||
// Merge overrides into the potentially file-loaded data
|
|
||||||
mergeMaps(c.data, overrides)
|
// Merge CLI overrides into flattened data (CLI takes precedence)
|
||||||
|
for path, value := range flattenMap(cliOverrides, "") {
|
||||||
|
flattenedData[path] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update configItems with loaded values
|
||||||
|
for path, value := range flattenedData {
|
||||||
|
if item, registered := c.items[path]; registered {
|
||||||
|
// Update existing item
|
||||||
|
item.currentValue = value
|
||||||
|
c.items[path] = item
|
||||||
|
} else {
|
||||||
|
// Create new item with default = current = loaded value
|
||||||
|
c.items[path] = configItem{
|
||||||
|
defaultValue: value,
|
||||||
|
currentValue: value,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return configExists, nil
|
return configExists, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save writes the current configuration stored in the Config instance to a TOML file.
|
// Save writes the current configuration to a TOML file.
|
||||||
// It performs an atomic write using a temporary file.
|
// It performs an atomic write using a temporary file.
|
||||||
func (c *Config) Save(path string) error {
|
func (c *Config) Save(path string) error {
|
||||||
c.mutex.RLock()
|
c.mutex.RLock()
|
||||||
// Marshal requires the actual value, not a pointer if data is already a map
|
|
||||||
dataToMarshal := c.data
|
|
||||||
c.mutex.RUnlock() // Unlock before potentially long I/O
|
|
||||||
|
|
||||||
tomlData, err := tinytoml.Marshal(dataToMarshal)
|
// Build a nested map from our flat structure
|
||||||
|
nestedData := make(map[string]any)
|
||||||
|
for path, item := range c.items {
|
||||||
|
setNestedValue(nestedData, path, item.currentValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mutex.RUnlock() // Release lock before I/O operations
|
||||||
|
|
||||||
|
tomlData, err := tinytoml.Marshal(nestedData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to marshal config: %w", err)
|
return fmt.Errorf("failed to marshal config: %w", err)
|
||||||
}
|
}
|
||||||
@ -193,7 +320,7 @@ func (c *Config) Save(path string) error {
|
|||||||
defer os.Remove(tempFile.Name()) // Clean up temp file if rename fails
|
defer os.Remove(tempFile.Name()) // Clean up temp file if rename fails
|
||||||
|
|
||||||
if _, err := tempFile.Write(tomlData); err != nil {
|
if _, err := tempFile.Write(tomlData); err != nil {
|
||||||
tempFile.Close() // Close file before attempting remove on error path
|
tempFile.Close()
|
||||||
return fmt.Errorf("failed to write temp config file '%s': %w", tempFile.Name(), err)
|
return fmt.Errorf("failed to write temp config file '%s': %w", tempFile.Name(), err)
|
||||||
}
|
}
|
||||||
if err := tempFile.Sync(); err != nil {
|
if err := tempFile.Sync(); err != nil {
|
||||||
@ -204,24 +331,92 @@ func (c *Config) Save(path string) error {
|
|||||||
return fmt.Errorf("failed to close temp config file '%s': %w", tempFile.Name(), err)
|
return fmt.Errorf("failed to close temp config file '%s': %w", tempFile.Name(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use Rename for atomic replace
|
|
||||||
if err := os.Rename(tempFile.Name(), path); err != nil {
|
if err := os.Rename(tempFile.Name(), path); err != nil {
|
||||||
return fmt.Errorf("failed to rename temp file to '%s': %w", path, err)
|
return fmt.Errorf("failed to rename temp file to '%s': %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set permissions after successful rename
|
|
||||||
if err := os.Chmod(path, 0644); err != nil {
|
if err := os.Chmod(path, 0644); err != nil {
|
||||||
// Log or handle this non-critical error? For now, return it.
|
|
||||||
return fmt.Errorf("failed to set permissions on config file '%s': %w", path, err)
|
return fmt.Errorf("failed to set permissions on config file '%s': %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnmarshalSubtree decodes the configuration data under a specific base path into the target struct or map.
|
||||||
|
func (c *Config) UnmarshalSubtree(basePath string, target any) error {
|
||||||
|
c.mutex.RLock()
|
||||||
|
defer c.mutex.RUnlock()
|
||||||
|
|
||||||
|
// Build the nested map from our flat structure
|
||||||
|
fullNestedMap := make(map[string]any)
|
||||||
|
for path, item := range c.items {
|
||||||
|
setNestedValue(fullNestedMap, path, item.currentValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
var subtreeData any
|
||||||
|
|
||||||
|
if basePath == "" {
|
||||||
|
// Use the entire data structure
|
||||||
|
subtreeData = fullNestedMap
|
||||||
|
} else {
|
||||||
|
// Navigate to the specific subtree
|
||||||
|
segments := strings.Split(basePath, ".")
|
||||||
|
current := any(fullNestedMap)
|
||||||
|
|
||||||
|
for _, segment := range segments {
|
||||||
|
currentMap, ok := current.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
// Path segment is not a map
|
||||||
|
return fmt.Errorf("configuration path segment %q is not a table (map)", segment)
|
||||||
|
}
|
||||||
|
|
||||||
|
value, exists := currentMap[segment]
|
||||||
|
if !exists {
|
||||||
|
// If the path doesn't exist, return an empty map
|
||||||
|
subtreeData = make(map[string]any)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
current = value
|
||||||
|
}
|
||||||
|
|
||||||
|
if subtreeData == nil {
|
||||||
|
subtreeData = current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we have a map for decoding
|
||||||
|
subtreeMap, ok := subtreeData.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("configuration path %q does not refer to a table (map)", basePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use mapstructure to decode
|
||||||
|
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||||
|
Result: target,
|
||||||
|
TagName: "toml",
|
||||||
|
WeaklyTypedInput: true,
|
||||||
|
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
||||||
|
mapstructure.StringToTimeDurationHookFunc(),
|
||||||
|
mapstructure.StringToSliceHookFunc(","),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create mapstructure decoder: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = decoder.Decode(subtreeMap)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decode subtree %q: %w", basePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// parseArgs processes command-line arguments into a nested map structure.
|
// parseArgs processes command-line arguments into a nested map structure.
|
||||||
// Expects arguments in the format "--key.subkey value" or "--booleanflag".
|
// Expects arguments in the format "--key.subkey value" or "--booleanflag".
|
||||||
func parseArgs(args []string) (map[string]any, error) {
|
func parseArgs(args []string) (map[string]any, error) {
|
||||||
overrides := make(map[string]any)
|
result := make(map[string]any)
|
||||||
i := 0
|
i := 0
|
||||||
for i < len(args) {
|
for i < len(args) {
|
||||||
arg := args[i]
|
arg := args[i]
|
||||||
@ -258,77 +453,85 @@ func parseArgs(args []string) (map[string]any, error) {
|
|||||||
value = valueStr // Keep as string if parsing fails
|
value = valueStr // Keep as string if parsing fails
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build nested map structure based on dots in the keyPath
|
// Set the value in the result map
|
||||||
keys := strings.Split(keyPath, ".")
|
setNestedValue(result, keyPath, value)
|
||||||
currentMap := overrides
|
}
|
||||||
for j, key := range keys[:len(keys)-1] {
|
|
||||||
// Ensure intermediate paths are maps
|
return result, nil
|
||||||
if existingVal, ok := currentMap[key]; ok {
|
}
|
||||||
if nestedMap, isMap := existingVal.(map[string]any); isMap {
|
|
||||||
currentMap = nestedMap // Navigate deeper
|
// flattenMap converts a nested map to a flat map with dot-notation paths.
|
||||||
} else {
|
func flattenMap(nested map[string]any, prefix string) map[string]any {
|
||||||
// Error: trying to overwrite a non-map value with a nested structure
|
flat := make(map[string]any)
|
||||||
return nil, fmt.Errorf("conflicting CLI key: %q is not a table but has subkey %q", strings.Join(keys[:j+1], "."), keys[j+1])
|
|
||||||
}
|
for key, value := range nested {
|
||||||
} else {
|
path := key
|
||||||
// Create intermediate map
|
if prefix != "" {
|
||||||
newMap := make(map[string]any)
|
path = prefix + "." + key
|
||||||
currentMap[key] = newMap
|
}
|
||||||
currentMap = newMap
|
|
||||||
|
if nestedMap, isMap := value.(map[string]any); isMap {
|
||||||
|
// Recursively flatten nested maps
|
||||||
|
for subPath, subValue := range flattenMap(nestedMap, path) {
|
||||||
|
flat[subPath] = subValue
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Set the final value
|
|
||||||
lastKey := keys[len(keys)-1]
|
|
||||||
currentMap[lastKey] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
return overrides, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// mergeMaps recursively merges the 'override' map into the 'base' map.
|
|
||||||
// Values in 'override' take precedence. If both values are maps, they are merged recursively.
|
|
||||||
func mergeMaps(base map[string]any, override map[string]any) {
|
|
||||||
if base == nil || override == nil {
|
|
||||||
return // Avoid panic on nil maps, though caller should initialize
|
|
||||||
}
|
|
||||||
for key, overrideVal := range override {
|
|
||||||
baseVal, _ := base[key]
|
|
||||||
// Check if both values are maps for recursive merge
|
|
||||||
baseMap, baseIsMap := baseVal.(map[string]any)
|
|
||||||
overrideMap, overrideIsMap := overrideVal.(map[string]any)
|
|
||||||
|
|
||||||
if baseIsMap && overrideIsMap {
|
|
||||||
// Recursively merge nested maps
|
|
||||||
mergeMaps(baseMap, overrideMap)
|
|
||||||
} else {
|
} else {
|
||||||
// Override value (or add if key doesn't exist in base)
|
// Add leaf value
|
||||||
base[key] = overrideVal
|
flat[path] = value
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getValueFromMap retrieves a value from a nested map using a dot-separated path.
|
|
||||||
func getValueFromMap(data map[string]any, path string) (any, bool) {
|
|
||||||
keys := strings.Split(path, ".")
|
|
||||||
current := any(data) // Start with the top-level map
|
|
||||||
|
|
||||||
for _, key := range keys {
|
|
||||||
if currentMap, ok := current.(map[string]any); ok {
|
|
||||||
value, exists := currentMap[key]
|
|
||||||
if !exists {
|
|
||||||
return nil, false // Key not found at this level
|
|
||||||
}
|
|
||||||
current = value // Move to the next level
|
|
||||||
} else {
|
|
||||||
return nil, false // Path segment is not a map, cannot traverse further
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Successfully traversed the entire path
|
return flat
|
||||||
return current, true
|
}
|
||||||
|
|
||||||
|
// setNestedValue sets a value in a nested map using a dot-notation path.
|
||||||
|
func setNestedValue(nested map[string]any, path string, value any) {
|
||||||
|
segments := strings.Split(path, ".")
|
||||||
|
|
||||||
|
if len(segments) == 1 {
|
||||||
|
// Base case: set the value directly
|
||||||
|
nested[segments[0]] = value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure parent map exists
|
||||||
|
if _, exists := nested[segments[0]]; !exists {
|
||||||
|
nested[segments[0]] = make(map[string]any)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the existing value is a map, or replace it
|
||||||
|
current := nested[segments[0]]
|
||||||
|
currentMap, isMap := current.(map[string]any)
|
||||||
|
if !isMap {
|
||||||
|
currentMap = make(map[string]any)
|
||||||
|
nested[segments[0]] = currentMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse with remaining path
|
||||||
|
setNestedValue(currentMap, strings.Join(segments[1:], "."), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidKeySegment checks if a single path segment is valid.
|
||||||
|
func isValidKeySegment(s string) bool {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
firstChar := rune(s[0])
|
||||||
|
// Using simplified check: must not contain dots and must be valid TOML key part
|
||||||
|
if strings.ContainsRune(s, '.') {
|
||||||
|
return false // Segments themselves cannot contain dots
|
||||||
|
}
|
||||||
|
if !isAlpha(firstChar) && firstChar != '_' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, r := range s[1:] {
|
||||||
|
if !isAlpha(r) && !isNumeric(r) && r != '-' && r != '_' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions adapted from tinytoml internal logic (as it's not exported)
|
|
||||||
// isAlpha checks if a character is a letter (A-Z, a-z)
|
// isAlpha checks if a character is a letter (A-Z, a-z)
|
||||||
func isAlpha(c rune) bool {
|
func isAlpha(c rune) bool {
|
||||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
||||||
|
|||||||
11
go.mod
11
go.mod
@ -1,12 +1,7 @@
|
|||||||
module github.com/LixenWraith/config
|
module github.com/LixenWraith/config
|
||||||
|
|
||||||
go 1.24.0
|
go 1.24.2
|
||||||
|
|
||||||
toolchain go1.24.2
|
require github.com/LixenWraith/tinytoml v0.0.0-20250305012228-6862ba843264
|
||||||
|
|
||||||
require (
|
require github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/LixenWraith/tinytoml v0.0.0-20250305012228-6862ba843264
|
|
||||||
github.com/google/uuid v1.6.0
|
|
||||||
)
|
|
||||||
|
|
||||||
require github.com/mitchellh/mapstructure v1.5.0 // indirect
|
|
||||||
|
|||||||
4
go.sum
4
go.sum
@ -1,8 +1,4 @@
|
|||||||
github.com/LixenWraith/tinytoml v0.0.0-20241125164826-37e61dcbf33b h1:zjNL89uvvL9xB65qKXQGrzVOAH0CWkxRmcbU2uyyUk4=
|
|
||||||
github.com/LixenWraith/tinytoml v0.0.0-20241125164826-37e61dcbf33b/go.mod h1:27w5bMp6NIrEuelM/8a+htswf0Dohs/AZ9tSsQ+lnN0=
|
|
||||||
github.com/LixenWraith/tinytoml v0.0.0-20250305012228-6862ba843264 h1:p2hpE672qTRuhR9FAt7SIHp8aP0pJbBKushCiIRNRpo=
|
github.com/LixenWraith/tinytoml v0.0.0-20250305012228-6862ba843264 h1:p2hpE672qTRuhR9FAt7SIHp8aP0pJbBKushCiIRNRpo=
|
||||||
github.com/LixenWraith/tinytoml v0.0.0-20250305012228-6862ba843264/go.mod h1:pm+BQlZ/VQC30uaB5Vfeih2b77QkGIiMvu+QgG/XOTk=
|
github.com/LixenWraith/tinytoml v0.0.0-20250305012228-6862ba843264/go.mod h1:pm+BQlZ/VQC30uaB5Vfeih2b77QkGIiMvu+QgG/XOTk=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
|||||||
Reference in New Issue
Block a user