diff --git a/.gitignore b/.gitignore index db63cda..923937d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ data dev logs +*.log diff --git a/README.md b/README.md index 32b01e0..1aec82d 100644 --- a/README.md +++ b/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. - **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`). -- **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. - **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 @@ -21,7 +24,7 @@ go get github.com/LixenWraith/config Dependencies will be automatically fetched: ```bash github.com/LixenWraith/tinytoml -github.com/google/uuid +github.com/mitchellh/mapstructure ``` ## Usage @@ -32,21 +35,111 @@ github.com/google/uuid // 1. Initialize a new Config instance cfg := config.New() -// 2. Register configuration keys with paths and default values -keyServerHost, err := cfg.Register("server.host", "127.0.0.1") -keyServerPort, err := cfg.Register("server.port", 8080) +// 2. Register configuration paths with default values +err := cfg.Register("server.host", "127.0.0.1") +err = cfg.Register("server.port", 8080) // 3. Load configuration from file with CLI argument overrides fileExists, err := cfg.Load("app_config.toml", os.Args[1:]) -// 4. Access configuration values using the registered keys -serverHost, _ := cfg.Get(keyServerHost) -serverPort, _ := cfg.Get(keyServerPort) +// 4. Access configuration values using the registered paths +serverHost, err := cfg.String("server.host") +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) 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 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. -### `(*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. -- **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) +- **defaultValue**: The value returned if no other value has been set through Load or Set. +- **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`. -- **Returns**: The configuration value and a boolean indicating if the key was registered. +- **path**: The dot-separated path string used during registration. +- **Returns**: The configuration value and a boolean indicating if the path was registered. - **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)` 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 - **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`. +- **Unified Storage Model**: Uses a `configItem` struct to store both default values and current values for each path. +- **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. - **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 - **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 (`.`). + - 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. +- 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 diff --git a/cmd/main.go b/cmd/main.go index 4317a9e..8d24a02 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,183 +1,257 @@ +// Test program for the config package package main import ( "fmt" - "log" + "github.com/LixenWraith/config" "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 ---") +// LogConfig represents logging configuration parameters +type LogConfig struct { + // Basic settings + Level int64 `toml:"level"` + Name string `toml:"name"` + Directory string `toml:"directory"` + Format string `toml:"format"` // "txt" or "json" + Extension string `toml:"extension"` + // Formatting + ShowTimestamp bool `toml:"show_timestamp"` + ShowLevel bool `toml:"show_level"` + // Buffer and size limits + BufferSize int64 `toml:"buffer_size"` // Channel buffer size + MaxSizeMB int64 `toml:"max_size_mb"` // Max size per log file + MaxTotalSizeMB int64 `toml:"max_total_size_mb"` // Max total size of all logs in dir + MinDiskFreeMB int64 `toml:"min_disk_free_mb"` // Minimum free disk space required + // Timers + FlushIntervalMs int64 `toml:"flush_interval_ms"` // Interval for flushing file buffer + 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 } -// Simple error handler -func handleErr(err error) { +func main() { + // Create a temporary file path for our test + 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 { - log.Fatalf("Error: %v", err) + fmt.Printf("Error loading config: %v\n", err) + os.Exit(1) + } + fmt.Printf("Config file exists: %v (expected: false)\n", exists) + + // Unmarshal into LogConfig struct + var logConfig LogConfig + err = cfg.UnmarshalSubtree("log", &logConfig) + if err != nil { + fmt.Printf("Error unmarshaling config: %v\n", err) + os.Exit(1) + } + + // Print current values + fmt.Println("\n=== Default Configuration Values ===") + printLogConfig(logConfig) + + // Modify some values + fmt.Println("\n=== Modifying Configuration Values ===") + fmt.Println("Changing:") + fmt.Println(" - level: 1 → 2") + fmt.Println(" - name: default_logger → modified_logger") + fmt.Println(" - format: txt → json") + fmt.Println(" - max_size_mb: 10 → 50") + fmt.Println(" - retention_period_hrs: 24.0 → 72.0") + fmt.Println(" - enable_adaptive_interval: false → true") + + cfg.Set("log.level", int64(2)) + cfg.Set("log.name", "modified_logger") + cfg.Set("log.format", "json") + cfg.Set("log.max_size_mb", int64(50)) + cfg.Set("log.retention_period_hrs", 72.0) + cfg.Set("log.enable_adaptive_interval", true) + + // Save the configuration + err = cfg.Save(configPath) + if err != nil { + fmt.Printf("Error saving config: %v\n", err) + os.Exit(1) + } + fmt.Printf("\nSaved configuration to: %s\n", configPath) + + // Read the file to verify it contains the expected values + fileBytes, err := os.ReadFile(configPath) + if err != nil { + fmt.Printf("Error reading config file: %v\n", err) + os.Exit(1) + } + + fmt.Println("\n=== Generated TOML File Contents ===") + fmt.Println(string(fileBytes)) + + // Load the config again to verify it can be read back correctly + exists, err = cfg.Load(configPath, nil) + if err != nil { + fmt.Printf("Error reloading config: %v\n", err) + os.Exit(1) + } + fmt.Printf("\nConfig file exists: %v (expected: true)\n", exists) + + // Unmarshal into a new LogConfig to verify loaded values + var loadedConfig LogConfig + err = cfg.UnmarshalSubtree("log", &loadedConfig) + if err != nil { + fmt.Printf("Error unmarshaling reloaded config: %v\n", err) + os.Exit(1) + } + + fmt.Println("\n=== Loaded Configuration Values ===") + printLogConfig(loadedConfig) + + // 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 ===") +} + +// registerLogConfigDefaults registers all default values for the LogConfig struct +func registerLogConfigDefaults(cfg *config.Config) { + fmt.Println("Registering default values...") + + // 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!") } } \ No newline at end of file diff --git a/config.go b/config.go index a6ea0c3..512e608 100644 --- a/config.go +++ b/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 import ( @@ -6,124 +8,228 @@ import ( "path/filepath" "strconv" "strings" - "sync" // Added sync import + "sync" "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. -type registeredItem struct { - path string // Dot-separated path (e.g., "server.port") +// configItem holds both the default and current value for a configuration path +type configItem struct { defaultValue any + currentValue any } // Config manages application configuration loaded from files and CLI arguments. -// It provides thread-safe access to configuration values. 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 + items map[string]configItem // Maps paths to config items (default and current values) + mutex sync.RWMutex // Protects concurrent access } // 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 + items: make(map[string]configItem), } } -// 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"). // 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) { +// defaultValue is the value returned by Get if no specific value has been set. +func (c *Config) Register(path string, defaultValue any) error { if path == "" { - return "", fmt.Errorf("registration path cannot be empty") + 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) + 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, + c.items[path] = configItem{ 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. -// 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) { +// Unregister removes a configuration path and all its children. +func (c *Config) Unregister(path string) error { 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 + if _, exists := c.items[path]; !exists { + return fmt.Errorf("path not registered: %s", path) } - 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 + + // Remove the path itself + delete(c.items, path) + + // Remove any child paths + prefix := path + "." + for childPath := range c.items { + if strings.HasPrefix(childPath, prefix) { + delete(c.items, childPath) } } - return true + + return nil } -// 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) { +// Get retrieves a configuration value using the path. +// It returns the current value (or default if not explicitly set). +// The second return value indicates if the path was registered. +func (c *Config) Get(path string) (any, bool) { c.mutex.RLock() - item, registered := c.registry[key] + defer c.mutex.RUnlock() + + item, registered := c.items[path] 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 + return item.currentValue, true +} - if found { - return value, true +// Set updates a configuration value for the given path. +// 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. -// 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) { @@ -131,7 +237,9 @@ func (c *Config) Load(path string, args []string) (bool, error) { defer c.mutex.Unlock() 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() { 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) } - // Use tinytoml to unmarshal directly into the map - // 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) + if err := tinytoml.Unmarshal(fileData, &nestedData); err != nil { + return false, fmt.Errorf("failed to parse TOML 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) + // Flatten the nested map into path->value pairs + flattenedData := flattenMap(nestedData, "") - // Parse and merge CLI arguments if any + // Parse CLI arguments if any if len(args) > 0 { - overrides, err := parseArgs(args) + cliOverrides, err := parseArgs(args) if err != nil { 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 } -// 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. func (c *Config) Save(path string) error { 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 { 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 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) } 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) } - // Use Rename for atomic replace if err := os.Rename(tempFile.Name(), path); err != nil { 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 } +// 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. // Expects arguments in the format "--key.subkey value" or "--booleanflag". func parseArgs(args []string) (map[string]any, error) { - overrides := make(map[string]any) + result := make(map[string]any) i := 0 for i < len(args) { arg := args[i] @@ -258,77 +453,85 @@ func parseArgs(args []string) (map[string]any, error) { 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 value in the result map + setNestedValue(result, keyPath, value) + } + + return result, nil +} + +// flattenMap converts a nested map to a flat map with dot-notation paths. +func flattenMap(nested map[string]any, prefix string) map[string]any { + flat := make(map[string]any) + + for key, value := range nested { + path := key + if prefix != "" { + path = prefix + "." + key + } + + 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 { - // Override value (or add if key doesn't exist in base) - base[key] = overrideVal - } - } -} - -// 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 + // Add leaf value + flat[path] = value } } - // Successfully traversed the entire path - return current, true + return flat +} + +// 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) func isAlpha(c rune) bool { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') diff --git a/go.mod b/go.mod index b12ab86..94a701d 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,7 @@ 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 ( - 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 diff --git a/go.sum b/go.sum index c684a3b..d294fee 100644 --- a/go.sum +++ b/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/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/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= diff --git a/test.toml b/test.toml deleted file mode 100644 index 19621dc..0000000 --- a/test.toml +++ /dev/null @@ -1,13 +0,0 @@ -debug = true -[server] -host = "localhost" -max_conns = 1000 -port = 9090 -read_timeout = 30000000000 -write_timeout = 30000000000 -[smtp] -auth_pass = "default123" -auth_user = "admin" -from_addr = "noreply@example.com" -host = "mail.example.com" -port = "587"