e0.1.0 Project restructure and feature add.
This commit is contained in:
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
BSD 3-Clause License
|
BSD 3-Clause License
|
||||||
|
|
||||||
Copyright (c) 2024, Lixen Wraith
|
Copyright (c) 2025, Lixen Wraith
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
modification, are permitted provided that the following conditions are met:
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|||||||
151
README.md
151
README.md
@ -1,70 +1,143 @@
|
|||||||
# Config
|
# Config
|
||||||
|
|
||||||
A simple configuration management package for Go applications that supports TOML files and CLI arguments.
|
A simple, thread-safe configuration management package for Go applications that supports TOML files, command-line argument overrides, and registered default values.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- TOML configuration with [tinytoml](https://github.com/LixenWraith/tinytoml)
|
- **Thread-Safe Operations:** Uses `sync.RWMutex` to protect concurrent access during all configuration operations.
|
||||||
- Command line argument overrides with dot notation
|
- **TOML Configuration:** Uses [tinytoml](https://github.com/LixenWraith/tinytoml) for loading and saving configuration files.
|
||||||
- Default config handling
|
- **Command-Line Overrides:** Allows overriding configuration values using dot notation in CLI arguments (e.g., `--server.port 9090`).
|
||||||
- Atomic file operations
|
- **Type-Safe Access:** Register configuration paths with default values and receive unique keys (UUIDs) for consistent access.
|
||||||
- No external dependencies beyond tinytoml
|
- **Atomic File Operations:** Ensures configuration files are written atomically to prevent corruption.
|
||||||
|
- **Path Validation:** Validates configuration path segments against TOML key requirements.
|
||||||
|
- **Minimal Dependencies:** Relies only on `tinytoml` and `google/uuid`.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go get github.com/example/config
|
go get github.com/LixenWraith/config
|
||||||
|
```
|
||||||
|
|
||||||
|
Dependencies will be automatically fetched:
|
||||||
|
```bash
|
||||||
|
github.com/LixenWraith/tinytoml
|
||||||
|
github.com/google/uuid
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
### Basic Usage Pattern
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type AppConfig struct {
|
// 1. Initialize a new Config instance
|
||||||
Server struct {
|
cfg := config.New()
|
||||||
Host string `toml:"host"`
|
|
||||||
Port int `toml:"port"`
|
|
||||||
} `toml:"server"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
// 2. Register configuration keys with paths and default values
|
||||||
cfg := AppConfig{
|
keyServerHost, err := cfg.Register("server.host", "127.0.0.1")
|
||||||
Host: "localhost",
|
keyServerPort, err := cfg.Register("server.port", 8080)
|
||||||
Port: 8080,
|
|
||||||
} // default config
|
|
||||||
exists, err := config.LoadConfig("config.toml", &cfg, os.Args[1:])
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !exists {
|
// 3. Load configuration from file with CLI argument overrides
|
||||||
if err := config.SaveConfig("config.toml", &cfg); err != nil {
|
fileExists, err := cfg.Load("app_config.toml", os.Args[1:])
|
||||||
log.Fatal(err)
|
|
||||||
}
|
// 4. Access configuration values using the registered keys
|
||||||
}
|
serverHost, _ := cfg.Get(keyServerHost)
|
||||||
}
|
serverPort, _ := cfg.Get(keyServerPort)
|
||||||
|
|
||||||
|
// 5. Save configuration (creates the file if it doesn't exist)
|
||||||
|
err = cfg.Save("app_config.toml")
|
||||||
```
|
```
|
||||||
|
|
||||||
### CLI Arguments
|
### CLI Arguments
|
||||||
|
|
||||||
Override config values using dot notation:
|
Command-line arguments override file configuration using dot notation:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./app --server.host localhost --server.port 8080
|
# Override server port and enable debug mode
|
||||||
|
./your_app --server.port 9090 --debug
|
||||||
|
|
||||||
|
# Override nested database setting
|
||||||
|
./your_app --database.connection.pool_size 50
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Flags without values are treated as boolean `true`.
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
### `LoadConfig(path string, config interface{}, args []string) (bool, error)`
|
### `New() *Config`
|
||||||
Loads configuration from TOML file and CLI args. Returns true if config file exists.
|
|
||||||
|
|
||||||
### `SaveConfig(path string, config interface{}) error`
|
Creates and returns a new, initialized `*Config` instance ready for use.
|
||||||
Saves configuration to TOML file atomically.
|
|
||||||
|
|
||||||
## Limitations
|
### `(*Config) Register(path string, defaultValue any) (string, error)`
|
||||||
|
|
||||||
- Supports only basic Go types and structures supported by tinytoml
|
Registers a configuration path with a default value and returns a unique UUID key.
|
||||||
- CLI arguments must use `--key value` format
|
|
||||||
- Indirect dependency on [mapstructure](https://github.com/mitchellh/mapstructure) through tinytoml
|
- **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.
|
||||||
|
- **Returns**: UUID string (key) for use with `Get` and error (nil on success)
|
||||||
|
|
||||||
|
### `(*Config) Get(key string) (any, bool)`
|
||||||
|
|
||||||
|
Retrieves a configuration value using the UUID key from `Register`.
|
||||||
|
|
||||||
|
- **key**: The UUID string returned by `Register`.
|
||||||
|
- **Returns**: The configuration value and a boolean indicating if the key was registered.
|
||||||
|
- **Value precedence**: CLI Argument > Config File Value > Registered Default Value
|
||||||
|
|
||||||
|
### `(*Config) Load(filePath string, args []string) (bool, error)`
|
||||||
|
|
||||||
|
Loads configuration from a TOML file and merges overrides from command-line arguments.
|
||||||
|
|
||||||
|
- **filePath**: Path to the TOML configuration file.
|
||||||
|
- **args**: Command-line arguments (e.g., `os.Args[1:]`).
|
||||||
|
- **Returns**: Boolean indicating if the file existed and a nil error on success.
|
||||||
|
|
||||||
|
### `(*Config) Save(filePath string) error`
|
||||||
|
|
||||||
|
Saves the current configuration to the specified TOML file path, performing an atomic write.
|
||||||
|
|
||||||
|
- **filePath**: Path where the TOML configuration file will be written.
|
||||||
|
- **Returns**: Error if marshaling or file operations fail, nil on success.
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Key Design Choices
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
- **Map Structure**: Configuration is stored in nested maps of type `map[string]any`.
|
||||||
|
- **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.
|
||||||
|
- **CLI Argument Types**: Command-line values are automatically parsed into bool, int64, float64, or string. See note below on Type Handling.
|
||||||
|
|
||||||
|
### Naming Conventions
|
||||||
|
|
||||||
|
- **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:
|
||||||
|
- Must start with a letter (a-z, A-Z) or an underscore (`_`).
|
||||||
|
- Subsequent characters can be letters, numbers (0-9), underscores (`_`), or hyphens (`-`).
|
||||||
|
- Segments *cannot* contain dots (`.`).
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
### Merge Behavior Note
|
||||||
|
|
||||||
|
- The internal merging logic (used during `Load`) performs a deep merge for nested maps.
|
||||||
|
- However, non-map types like slices are assigned by reference during the merge. If the source map containing the slice is modified after merging, the change might be reflected in the config data. This is generally not an issue with the standard Load workflow but should be noted if using merge logic independently.
|
||||||
|
|
||||||
|
### Limitations
|
||||||
|
|
||||||
|
- Supports only basic Go types and structures compatible with the tinytoml package.
|
||||||
|
- CLI arguments must use `--key value` or `--booleanflag` format.
|
||||||
|
- Path segments must start with letter/underscore, followed by letters/numbers/dashes/underscores.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Complete example programs demonstrating the config package are available in the `cmd` directory.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
BSD-3
|
BSD-3-Clause
|
||||||
183
cmd/main.go
Normal file
183
cmd/main.go
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/LixenWraith/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
initialConfigFile = "config_initial.toml"
|
||||||
|
finalConfigFile = "config_final.toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sample TOML content for the initial configuration file
|
||||||
|
var initialTomlContent = `
|
||||||
|
debug = true
|
||||||
|
log_level = "info" # This will be overridden by default
|
||||||
|
|
||||||
|
[server]
|
||||||
|
host = "localhost"
|
||||||
|
port = 8080 # This will be overridden by CLI
|
||||||
|
|
||||||
|
[smtp]
|
||||||
|
host = "mail.example.com" # This will be overridden by CLI
|
||||||
|
port = 587
|
||||||
|
auth_user = "file_user"
|
||||||
|
# auth_pass is missing, will use default
|
||||||
|
`
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// --- Setup: Create a temporary directory for config files ---
|
||||||
|
tempDir, err := os.MkdirTemp("", "config_example")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir) // Clean up temp directory
|
||||||
|
|
||||||
|
initialPath := filepath.Join(tempDir, initialConfigFile)
|
||||||
|
finalPath := filepath.Join(tempDir, finalConfigFile)
|
||||||
|
|
||||||
|
// Write the initial TOML config file
|
||||||
|
err = os.WriteFile(initialPath, []byte(initialTomlContent), 0644)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to write initial config file: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Wrote initial config to: %s\n", initialPath)
|
||||||
|
|
||||||
|
// --- Step 1: Create and Register Configuration ---
|
||||||
|
fmt.Println("\n--- Step 1: Initialize Config and Register Keys ---")
|
||||||
|
c := config.New()
|
||||||
|
|
||||||
|
// Register keys and store their UUIDs
|
||||||
|
// Provide default values that might be overridden by file or CLI
|
||||||
|
keyDebug, err := c.Register("debug", false) // Default false, overridden by file
|
||||||
|
handleErr(err)
|
||||||
|
keyLogLevel, err := c.Register("log_level", "warn") // Default warn, file has "info"
|
||||||
|
handleErr(err)
|
||||||
|
keyServerHost, err := c.Register("server.host", "127.0.0.1") // Default 127.0.0.1, overridden by file
|
||||||
|
handleErr(err)
|
||||||
|
keyServerPort, err := c.Register("server.port", 9090) // Default 9090, file 8080, CLI 9999
|
||||||
|
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.")
|
||||||
|
fmt.Printf(" - debug (default: false): %s\n", keyDebug)
|
||||||
|
fmt.Printf(" - server.port (default: 9090): %s\n", keyServerPort)
|
||||||
|
fmt.Printf(" - smtp.auth_pass (default: 'default_pass'): %s\n", keySmtpPass)
|
||||||
|
fmt.Printf(" - only_default (default: 'this_is_the_default'): %s\n", keyOnlyDefault)
|
||||||
|
|
||||||
|
// --- Step 2: Load Configuration from File and CLI Args ---
|
||||||
|
fmt.Println("\n--- Step 2: Load from File and CLI ---")
|
||||||
|
// Simulate command-line arguments
|
||||||
|
cliArgs := []string{
|
||||||
|
"--server.port", "9999", // Override file value
|
||||||
|
"--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)
|
||||||
|
|
||||||
|
foundFile, err := c.Load(initialPath, cliArgs)
|
||||||
|
handleErr(err)
|
||||||
|
fmt.Printf("Config file loaded: %t\n", foundFile)
|
||||||
|
|
||||||
|
// --- Step 3: Access Merged Configuration Values ---
|
||||||
|
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
|
||||||
|
// This requires accessing the unexported 'data' field via a helper or reflection,
|
||||||
|
// 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
|
||||||
|
key2ServerPort, _ := c2.Register("server.port", 0) // Default doesn't matter now
|
||||||
|
key2SmtpHost, _ := c2.Register("smtp.host", "")
|
||||||
|
key2NewCliFlag, _ := c2.Register("new_cli_flag", false)
|
||||||
|
key2Unregistered, _ := c2.Register("unregistered.cli.arg", "") // Register the CLI-only arg
|
||||||
|
|
||||||
|
reloadedPort, _ := c2.Get(key2ServerPort)
|
||||||
|
reloadedSmtpHost, _ := c2.Get(key2SmtpHost)
|
||||||
|
reloadedCliFlag, _ := c2.Get(key2NewCliFlag)
|
||||||
|
reloadedUnregistered, _ := c2.Get(key2Unregistered) // Get the value added only via CLI initially
|
||||||
|
|
||||||
|
fmt.Println("Comparing reloaded values:")
|
||||||
|
fmt.Printf(" - Reloaded Server Port: %v (Expected: 9999) - Match: %t\n", reloadedPort, reloadedPort == int64(9999))
|
||||||
|
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 ---")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple error handler
|
||||||
|
func handleErr(err error) {
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
345
config.go
345
config.go
@ -1,149 +1,340 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync" // Added sync import
|
||||||
|
|
||||||
"github.com/LixenWraith/tinytoml"
|
"github.com/LixenWraith/tinytoml"
|
||||||
|
"github.com/google/uuid" // Added uuid import
|
||||||
)
|
)
|
||||||
|
|
||||||
type cliArg struct {
|
// registeredItem holds metadata for a configuration value registered for access.
|
||||||
key string
|
type registeredItem struct {
|
||||||
value string
|
path string // Dot-separated path (e.g., "server.port")
|
||||||
|
defaultValue any
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load(path string, config interface{}, args []string) (bool, error) {
|
// Config manages application configuration loaded from files and CLI arguments.
|
||||||
if config == nil {
|
// It provides thread-safe access to configuration values.
|
||||||
return false, fmt.Errorf("config cannot be nil")
|
type Config struct {
|
||||||
|
data map[string]any // Stores the actual configuration data (nested map)
|
||||||
|
registry map[string]registeredItem // Maps generated UUIDs to registered items
|
||||||
|
mutex sync.RWMutex // Protects concurrent access to data and registry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New creates and initializes a new Config instance.
|
||||||
|
func New() *Config {
|
||||||
|
return &Config{
|
||||||
|
data: make(map[string]any),
|
||||||
|
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.
|
||||||
|
// 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 returned by Get if the value is not found in the loaded configuration.
|
||||||
|
func (c *Config) Register(path string, defaultValue any) (string, error) {
|
||||||
|
if path == "" {
|
||||||
|
return "", fmt.Errorf("registration path cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate path segments
|
||||||
|
segments := strings.Split(path, ".")
|
||||||
|
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) {
|
||||||
|
return "", fmt.Errorf("invalid path segment %q in path %q", segment, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
newUUID := uuid.NewString()
|
||||||
|
item := registeredItem{
|
||||||
|
path: path,
|
||||||
|
defaultValue: defaultValue,
|
||||||
|
}
|
||||||
|
c.registry[newUUID] = item
|
||||||
|
|
||||||
|
return newUUID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unregister removes a configuration key from the registry.
|
||||||
|
// Subsequent calls to Get with this key will return (nil, false).
|
||||||
|
// 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()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
delete(c.registry, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidKeySegment checks if a single path segment is valid.
|
||||||
|
// Adapts the logic described for tinytoml's isValidKey.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a configuration value using the unique key (UUID) obtained from Register.
|
||||||
|
// It returns the value found in the loaded configuration or the registered default value.
|
||||||
|
// The second return value (bool) indicates if the key was successfully registered (true) or not (false).
|
||||||
|
func (c *Config) Get(key string) (any, bool) {
|
||||||
|
c.mutex.RLock()
|
||||||
|
item, registered := c.registry[key]
|
||||||
|
if !registered {
|
||||||
|
c.mutex.RUnlock()
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup value in the data map using the item's path
|
||||||
|
value, found := getValueFromMap(c.data, item.path)
|
||||||
|
c.mutex.RUnlock() // Unlock after accessing both registry and data
|
||||||
|
|
||||||
|
if found {
|
||||||
|
return value, true
|
||||||
|
}
|
||||||
|
// Key was registered, but value not found in data, return default
|
||||||
|
return item.defaultValue, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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:]).
|
||||||
|
// Returns true if the configuration file was found and loaded, false otherwise.
|
||||||
|
func (c *Config) Load(path string, args []string) (bool, error) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
configExists := false
|
configExists := false
|
||||||
|
loadedData := make(map[string]any) // Load into a temporary map first
|
||||||
|
|
||||||
if stat, err := os.Stat(path); err == nil && !stat.IsDir() {
|
if stat, err := os.Stat(path); err == nil && !stat.IsDir() {
|
||||||
configExists = true
|
configExists = true
|
||||||
data, err := os.ReadFile(path)
|
fileData, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("failed to read config file: %w", err)
|
return false, fmt.Errorf("failed to read config file '%s': %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tinytoml.Unmarshal(data, config); err != nil {
|
// Use tinytoml to unmarshal directly into the map
|
||||||
return false, fmt.Errorf("failed to parse config file: %w", err)
|
// Pass a pointer to the map for Unmarshal
|
||||||
|
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) {
|
||||||
|
// Handle potential errors from os.Stat other than file not existing
|
||||||
|
return false, fmt.Errorf("failed to check config file '%s': %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merge loaded data into the main config data
|
||||||
|
// This ensures existing data (e.g. from defaults set programmatically before load) isn't wiped out
|
||||||
|
mergeMaps(c.data, loadedData)
|
||||||
|
|
||||||
|
// Parse and merge CLI arguments if any
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
overrides, err := parseArgs(args)
|
overrides, 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
|
||||||
if err := mergeConfig(config, overrides); err != nil {
|
mergeMaps(c.data, overrides)
|
||||||
return configExists, fmt.Errorf("failed to merge CLI args: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return configExists, nil
|
return configExists, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Save(path string, config interface{}) error {
|
// Save writes the current configuration stored in the Config instance to a TOML file.
|
||||||
v := reflect.ValueOf(config)
|
// It performs an atomic write using a temporary file.
|
||||||
if v.Kind() == reflect.Ptr {
|
func (c *Config) Save(path string) error {
|
||||||
v = v.Elem()
|
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
|
||||||
|
|
||||||
if v.Kind() != reflect.Struct {
|
tomlData, err := tinytoml.Marshal(dataToMarshal)
|
||||||
return fmt.Errorf("config must be a struct or pointer to struct")
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := tinytoml.Marshal(v.Interface())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to marshal config: %w", err)
|
return fmt.Errorf("failed to marshal config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Atomic write logic
|
||||||
dir := filepath.Dir(path)
|
dir := filepath.Dir(path)
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create config directory: %w", err)
|
return fmt.Errorf("failed to create config directory '%s': %w", dir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tempFile := path + ".tmp"
|
tempFile, err := os.CreateTemp(dir, filepath.Base(path)+".*.tmp")
|
||||||
if err := os.WriteFile(tempFile, data, 0644); err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to write temp config file: %w", err)
|
return fmt.Errorf("failed to create temporary config file: %w", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tempFile.Name()) // Clean up temp file if rename fails
|
||||||
|
|
||||||
|
if _, err := tempFile.Write(tomlData); err != nil {
|
||||||
|
tempFile.Close() // Close file before attempting remove on error path
|
||||||
|
return fmt.Errorf("failed to write temp config file '%s': %w", tempFile.Name(), err)
|
||||||
|
}
|
||||||
|
if err := tempFile.Sync(); err != nil {
|
||||||
|
tempFile.Close()
|
||||||
|
return fmt.Errorf("failed to sync temp config file '%s': %w", tempFile.Name(), err)
|
||||||
|
}
|
||||||
|
if err := tempFile.Close(); err != nil {
|
||||||
|
return fmt.Errorf("failed to close temp config file '%s': %w", tempFile.Name(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.Rename(tempFile, path); err != nil {
|
// Use Rename for atomic replace
|
||||||
os.Remove(tempFile)
|
if err := os.Rename(tempFile.Name(), path); err != nil {
|
||||||
return fmt.Errorf("failed to save config file: %w", 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 {
|
||||||
|
// 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 nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseArgs(args []string) (map[string]interface{}, error) {
|
// parseArgs processes command-line arguments into a nested map structure.
|
||||||
parsed := make([]cliArg, 0, len(args))
|
// Expects arguments in the format "--key.subkey value" or "--booleanflag".
|
||||||
for i := 0; i < len(args); i++ {
|
func parseArgs(args []string) (map[string]any, error) {
|
||||||
|
overrides := make(map[string]any)
|
||||||
|
i := 0
|
||||||
|
for i < len(args) {
|
||||||
arg := args[i]
|
arg := args[i]
|
||||||
if !strings.HasPrefix(arg, "--") {
|
if !strings.HasPrefix(arg, "--") {
|
||||||
|
i++ // Skip non-flag arguments
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
key := strings.TrimPrefix(arg, "--")
|
keyPath := strings.TrimPrefix(arg, "--")
|
||||||
|
if keyPath == "" {
|
||||||
|
i++ // Skip "--" argument
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var valueStr string
|
||||||
|
// Check if it's a boolean flag (next arg starts with -- or end of args)
|
||||||
if i+1 >= len(args) || strings.HasPrefix(args[i+1], "--") {
|
if i+1 >= len(args) || strings.HasPrefix(args[i+1], "--") {
|
||||||
parsed = append(parsed, cliArg{key: key, value: "true"})
|
valueStr = "true" // Assume boolean flag if no value provided
|
||||||
continue
|
i++ // Consume only the flag
|
||||||
}
|
|
||||||
|
|
||||||
parsed = append(parsed, cliArg{key: key, value: args[i+1]})
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make(map[string]interface{})
|
|
||||||
for _, arg := range parsed {
|
|
||||||
keys := strings.Split(arg.key, ".")
|
|
||||||
current := result
|
|
||||||
for i, k := range keys[:len(keys)-1] {
|
|
||||||
if _, exists := current[k]; !exists {
|
|
||||||
current[k] = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
if nested, ok := current[k].(map[string]interface{}); ok {
|
|
||||||
current = nested
|
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("invalid nested key at %s", strings.Join(keys[:i+1], "."))
|
valueStr = args[i+1]
|
||||||
}
|
i += 2 // Consume flag and value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to parse the value into bool, int, float, otherwise keep as string
|
||||||
|
var value any
|
||||||
|
if v, err := strconv.ParseBool(valueStr); err == nil {
|
||||||
|
value = v
|
||||||
|
} else if v, err := strconv.ParseInt(valueStr, 10, 64); err == nil {
|
||||||
|
value = v // Store as int64
|
||||||
|
} else if v, err := strconv.ParseFloat(valueStr, 64); err == nil {
|
||||||
|
value = v
|
||||||
|
} else {
|
||||||
|
value = valueStr // Keep as string if parsing fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build nested map structure based on dots in the keyPath
|
||||||
|
keys := strings.Split(keyPath, ".")
|
||||||
|
currentMap := overrides
|
||||||
|
for j, key := range keys[:len(keys)-1] {
|
||||||
|
// Ensure intermediate paths are maps
|
||||||
|
if existingVal, ok := currentMap[key]; ok {
|
||||||
|
if nestedMap, isMap := existingVal.(map[string]any); isMap {
|
||||||
|
currentMap = nestedMap // Navigate deeper
|
||||||
|
} else {
|
||||||
|
// Error: trying to overwrite a non-map value with a nested structure
|
||||||
|
return nil, fmt.Errorf("conflicting CLI key: %q is not a table but has subkey %q", strings.Join(keys[:j+1], "."), keys[j+1])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create intermediate map
|
||||||
|
newMap := make(map[string]any)
|
||||||
|
currentMap[key] = newMap
|
||||||
|
currentMap = newMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Set the final value
|
||||||
lastKey := keys[len(keys)-1]
|
lastKey := keys[len(keys)-1]
|
||||||
if val, err := strconv.ParseBool(arg.value); err == nil {
|
currentMap[lastKey] = value
|
||||||
current[lastKey] = val
|
}
|
||||||
} else if val, err := strconv.ParseInt(arg.value, 10, 64); err == nil {
|
|
||||||
current[lastKey] = val
|
return overrides, nil
|
||||||
} else if val, err := strconv.ParseFloat(arg.value, 64); err == nil {
|
}
|
||||||
current[lastKey] = val
|
|
||||||
|
// 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 {
|
||||||
current[lastKey] = arg.value
|
// Override value (or add if key doesn't exist in base)
|
||||||
|
base[key] = overrideVal
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
// 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mergeConfig(base interface{}, override map[string]interface{}) error {
|
// Successfully traversed the entire path
|
||||||
baseValue := reflect.ValueOf(base)
|
return current, true
|
||||||
if baseValue.Kind() != reflect.Ptr || baseValue.IsNil() {
|
|
||||||
return fmt.Errorf("base config must be a non-nil pointer")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.Marshal(override)
|
// Helper functions adapted from tinytoml internal logic (as it's not exported)
|
||||||
if err != nil {
|
// isAlpha checks if a character is a letter (A-Z, a-z)
|
||||||
return fmt.Errorf("failed to marshal override values: %w", err)
|
func isAlpha(c rune) bool {
|
||||||
|
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(data, base); err != nil {
|
// isNumeric checks if a character is a digit (0-9)
|
||||||
return fmt.Errorf("failed to merge override values: %w", err)
|
func isNumeric(c rune) bool {
|
||||||
}
|
return c >= '0' && c <= '9'
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
@ -1,77 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/LixenWraith/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SMTPConfig struct {
|
|
||||||
Host string `toml:"host"`
|
|
||||||
Port string `toml:"port"`
|
|
||||||
FromAddr string `toml:"from_addr"`
|
|
||||||
AuthUser string `toml:"auth_user"`
|
|
||||||
AuthPass string `toml:"auth_pass"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServerConfig struct {
|
|
||||||
Host string `toml:"host"`
|
|
||||||
Port int `toml:"port"`
|
|
||||||
ReadTimeout time.Duration `toml:"read_timeout"`
|
|
||||||
WriteTimeout time.Duration `toml:"write_timeout"`
|
|
||||||
MaxConns int `toml:"max_conns"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AppConfig struct {
|
|
||||||
SMTP SMTPConfig `toml:"smtp"`
|
|
||||||
Server ServerConfig `toml:"server"`
|
|
||||||
Debug bool `toml:"debug"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
defaultConfig := AppConfig{
|
|
||||||
SMTP: SMTPConfig{
|
|
||||||
Host: "smtp.example.com",
|
|
||||||
Port: "587",
|
|
||||||
FromAddr: "noreply@example.com",
|
|
||||||
AuthUser: "admin",
|
|
||||||
AuthPass: "default123",
|
|
||||||
},
|
|
||||||
Server: ServerConfig{
|
|
||||||
Host: "localhost",
|
|
||||||
Port: 8080,
|
|
||||||
ReadTimeout: time.Second * 30,
|
|
||||||
WriteTimeout: time.Second * 30,
|
|
||||||
MaxConns: 1000,
|
|
||||||
},
|
|
||||||
Debug: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
configPath := filepath.Join(".", "test.toml")
|
|
||||||
cfg := defaultConfig
|
|
||||||
|
|
||||||
// CLI argument usage example to override SMTP host and server port of existing and default config:
|
|
||||||
// ./main --smtp.host mail.example.com --server.port 9090
|
|
||||||
|
|
||||||
exists, err := config.Load(configPath, &cfg, os.Args[1:])
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Failed to load config: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
if err := config.Save(configPath, &cfg); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Failed to save default config: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
fmt.Println("Created default config at:", configPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Running with config:\n")
|
|
||||||
fmt.Printf("Server: %s:%d\n", cfg.Server.Host, cfg.Server.Port)
|
|
||||||
fmt.Printf("SMTP: %s:%s\n", cfg.SMTP.Host, cfg.SMTP.Port)
|
|
||||||
fmt.Printf("Debug: %v\n", cfg.Debug)
|
|
||||||
}
|
|
||||||
9
go.mod
9
go.mod
@ -1,7 +1,12 @@
|
|||||||
module github.com/LixenWraith/config
|
module github.com/LixenWraith/config
|
||||||
|
|
||||||
go 1.23.4
|
go 1.24.0
|
||||||
|
|
||||||
require github.com/LixenWraith/tinytoml v0.0.0-20241125164826-37e61dcbf33b
|
toolchain go1.24.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
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
|
require github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
|||||||
4
go.sum
4
go.sum
@ -1,4 +1,8 @@
|
|||||||
github.com/LixenWraith/tinytoml v0.0.0-20241125164826-37e61dcbf33b h1:zjNL89uvvL9xB65qKXQGrzVOAH0CWkxRmcbU2uyyUk4=
|
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-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/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