diff --git a/LICENSE b/LICENSE index 30c1c3e..08cdc7b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2025, Lixen Wraith +Copyright (c) 2026, Lixen Wraith Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/config.go b/config.go index 8e82ffd..73b5e6c 100644 --- a/config.go +++ b/config.go @@ -1,4 +1,3 @@ -// FILE: lixenwraith/config/config.go // Package config provides thread-safe configuration management for Go applications // with support for multiple sources: TOML files, environment variables, command-line // arguments, and default values with configurable precedence. @@ -7,6 +6,7 @@ package config import ( "fmt" "reflect" + "slices" "sync" "sync/atomic" ) @@ -50,6 +50,9 @@ type Config struct { version atomic.Int64 structCache *structCache + // CLI paths that matched no registered path (reset on each CLI load) + unknownCLIKeys []string + // File watching support watcher *watcher configFilePath string // Track loaded file path @@ -249,6 +252,7 @@ func (c *Config) SetSource(source Source, path string, value any) error { c.envData[path] = value case SourceCLI: c.cliData[path] = value + c.unknownCLIKeys = nil } c.invalidateCache() // Invalidate cache after changes @@ -337,6 +341,13 @@ func (c *Config) AsStruct() (any, error) { return c.structCache.target, nil } +// UnknownCLIKeys returns CLI paths that matched no registered config path +func (c *Config) UnknownCLIKeys() []string { + c.mutex.RLock() + defer c.mutex.RUnlock() + return slices.Clone(c.unknownCLIKeys) +} + // computeValue determines the current value based on precedence func (c *Config) computeValue(item configItem) any { // Check sources in precedence order @@ -372,4 +383,4 @@ func (c *Config) populateStruct() error { // invalidateCache override Set methods to invalidate cache func (c *Config) invalidateCache() { c.version.Add(1) -} \ No newline at end of file +} diff --git a/go.mod b/go.mod index d68e9fe..d4aebb9 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module github.com/lixenwraith/config -go 1.25.4 +go 1.26.0 require ( - github.com/BurntSushi/toml v1.5.0 - github.com/go-viper/mapstructure/v2 v2.4.0 + github.com/BurntSushi/toml v1.6.0 + github.com/go-viper/mapstructure/v2 v2.5.0 github.com/stretchr/testify v1.10.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index ba939f1..e2e2ab4 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,9 @@ -github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= -github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= diff --git a/loader.go b/loader.go index 219a1ad..4e1494a 100644 --- a/loader.go +++ b/loader.go @@ -435,7 +435,7 @@ func (c *Config) loadEnv(opts LoadOptions) error { transform = defaultEnvTransform(opts.EnvPrefix) } - // -- 1. Prepare data (Read-Lock to get paths) + // 1. Prepare data (Read-Lock to get paths) c.mutex.RLock() paths := make([]string, 0, len(c.items)) for p := range c.items { @@ -443,7 +443,7 @@ func (c *Config) loadEnv(opts LoadOptions) error { } c.mutex.RUnlock() - // -- 2. Process env vars (No Lock) + // 2. Process env vars (No Lock) foundEnvVars := make(map[string]string) for _, path := range paths { if opts.EnvWhitelist != nil && !opts.EnvWhitelist[path] { @@ -506,6 +506,9 @@ func (c *Config) loadCLI(args []string) error { c.cliData = flattenedCLI + // Reset per load; reloads must not accumulate duplicates + c.unknownCLIKeys = c.unknownCLIKeys[:0] + for path, value := range flattenedCLI { if item, exists := c.items[path]; exists { if item.values == nil { @@ -514,6 +517,9 @@ func (c *Config) loadCLI(args []string) error { item.values[SourceCLI] = value item.currentValue = c.computeValue(item) c.items[path] = item + } else { + // Record CLI keys that match no registered path + c.unknownCLIKeys = append(c.unknownCLIKeys, path) } } @@ -693,4 +699,4 @@ func detectFormatFromContent(data []byte) string { } return "" -} \ No newline at end of file +}