diff --git a/jety.go b/jety.go index e55547f..a153faf 100644 --- a/jety.go +++ b/jety.go @@ -4,7 +4,6 @@ import ( "encoding/json" "errors" "fmt" - "maps" "os" "path/filepath" "strings" @@ -33,7 +32,8 @@ type ( configPath string configFileUsed string configType configType - mapConfig map[string]ConfigMap + overrideConfig map[string]ConfigMap + fileConfig map[string]ConfigMap defaultConfig map[string]ConfigMap envConfig map[string]ConfigMap combinedConfig map[string]ConfigMap @@ -50,7 +50,8 @@ var ( func NewConfigManager() *ConfigManager { cm := ConfigManager{} cm.envConfig = make(map[string]ConfigMap) - cm.mapConfig = make(map[string]ConfigMap) + cm.overrideConfig = make(map[string]ConfigMap) + cm.fileConfig = make(map[string]ConfigMap) cm.defaultConfig = make(map[string]ConfigMap) cm.combinedConfig = make(map[string]ConfigMap) envSet := os.Environ() @@ -99,13 +100,21 @@ func (c *ConfigManager) collapse() { c.mutex.Lock() defer c.mutex.Unlock() ccm := make(map[string]ConfigMap) + // Precedence (highest to lowest): overrides (Set) > env > file > defaults for k, v := range c.defaultConfig { ccm[k] = v - if _, ok := c.envConfig[k]; ok { - ccm[k] = c.envConfig[k] + } + for k, v := range c.fileConfig { + ccm[k] = v + } + for k := range c.defaultConfig { + if v, ok := c.envConfig[k]; ok { + ccm[k] = v } } - maps.Copy(ccm, c.mapConfig) + for k, v := range c.overrideConfig { + ccm[k] = v + } c.combinedConfig = ccm } @@ -216,7 +225,7 @@ func (c *ConfigManager) ReadInConfig() error { conf[lower] = ConfigMap{Key: k, Value: v} } c.mutex.Lock() - c.mapConfig = conf + c.fileConfig = conf c.configFileUsed = configFile c.mutex.Unlock() c.collapse() diff --git a/jety_test.go b/jety_test.go index 56d9f0f..e71e0db 100644 --- a/jety_test.go +++ b/jety_test.go @@ -66,7 +66,7 @@ func TestNewConfigManager(t *testing.T) { if cm.envConfig == nil { t.Error("envConfig not initialized") } - if cm.mapConfig == nil { + if cm.overrideConfig == nil { t.Error("mapConfig not initialized") } if cm.defaultConfig == nil { @@ -554,7 +554,7 @@ func TestEnvOverridesDefault(t *testing.T) { } } -func TestConfigFileOverridesEnv(t *testing.T) { +func TestEnvOverridesConfigFile(t *testing.T) { os.Setenv("PORT", "5000") defer os.Unsetenv("PORT") @@ -575,9 +575,9 @@ func TestConfigFileOverridesEnv(t *testing.T) { t.Fatal(err) } - // Config file should override env and default - if got := cm.GetInt("port"); got != 9000 { - t.Errorf("GetInt(port) = %d, want 9000 (from file)", got) + // Env should override config file (env > file > defaults) + if got := cm.GetInt("port"); got != 5000 { + t.Errorf("GetInt(port) = %d, want 5000 (env overrides file)", got) } } @@ -1347,3 +1347,53 @@ func TestPackageLevelSetEnvPrefixOverrides(t *testing.T) { t.Fatalf("subprocess failed: %v\n%s", err, out) } } + +func TestPrecedenceChain(t *testing.T) { + // Verify: Set > env > file > defaults + os.Setenv("PORT", "5000") + os.Setenv("HOST", "envhost") + os.Setenv("LOG", "envlog") + defer os.Unsetenv("PORT") + defer os.Unsetenv("HOST") + defer os.Unsetenv("LOG") + + dir := t.TempDir() + configFile := filepath.Join(dir, "config.yaml") + if err := os.WriteFile(configFile, []byte("port: 9000\nhost: filehost\nlog: filelog\nname: filename"), 0o644); err != nil { + t.Fatal(err) + } + + cm := NewConfigManager() + cm.SetDefault("port", 8080) + cm.SetDefault("host", "defaulthost") + cm.SetDefault("log", "defaultlog") + cm.SetDefault("name", "defaultname") + cm.SetDefault("extra", "defaultextra") + + cm.SetConfigFile(configFile) + if err := cm.SetConfigType("yaml"); err != nil { + t.Fatal(err) + } + if err := cm.ReadInConfig(); err != nil { + t.Fatal(err) + } + + cm.Set("port", 1111) // Set overrides everything + + // port: Set(1111) > env(5000) > file(9000) > default(8080) → 1111 + if got := cm.GetInt("port"); got != 1111 { + t.Errorf("port: got %d, want 1111 (Set overrides all)", got) + } + // host: env(envhost) > file(filehost) > default(defaulthost) → envhost + if got := cm.GetString("host"); got != "envhost" { + t.Errorf("host: got %q, want envhost (env overrides file)", got) + } + // name: file(filename) > default(defaultname) → filename + if got := cm.GetString("name"); got != "filename" { + t.Errorf("name: got %q, want filename (file overrides default)", got) + } + // extra: only default → defaultextra + if got := cm.GetString("extra"); got != "defaultextra" { + t.Errorf("extra: got %q, want defaultextra (default)", got) + } +} diff --git a/setters.go b/setters.go index c5d04fa..b0b7b5a 100644 --- a/setters.go +++ b/setters.go @@ -8,7 +8,7 @@ func (c *ConfigManager) SetBool(key string, value bool) { c.mutex.Lock() defer c.mutex.Unlock() lower := strings.ToLower(key) - c.mapConfig[lower] = ConfigMap{Key: key, Value: value} + c.overrideConfig[lower] = ConfigMap{Key: key, Value: value} c.combinedConfig[lower] = ConfigMap{Key: key, Value: value} } @@ -16,7 +16,7 @@ func (c *ConfigManager) SetString(key string, value string) { c.mutex.Lock() defer c.mutex.Unlock() lower := strings.ToLower(key) - c.mapConfig[lower] = ConfigMap{Key: key, Value: value} + c.overrideConfig[lower] = ConfigMap{Key: key, Value: value} c.combinedConfig[lower] = ConfigMap{Key: key, Value: value} } @@ -24,7 +24,7 @@ func (c *ConfigManager) Set(key string, value any) { c.mutex.Lock() defer c.mutex.Unlock() lower := strings.ToLower(key) - c.mapConfig[lower] = ConfigMap{Key: key, Value: value} + c.overrideConfig[lower] = ConfigMap{Key: key, Value: value} c.combinedConfig[lower] = ConfigMap{Key: key, Value: value} } @@ -33,14 +33,14 @@ func (c *ConfigManager) SetDefault(key string, value any) { defer c.mutex.Unlock() lower := strings.ToLower(key) c.defaultConfig[lower] = ConfigMap{Key: key, Value: value} - if _, ok := c.mapConfig[lower]; !ok { + if _, ok := c.overrideConfig[lower]; !ok { if envVal, ok := c.envConfig[lower]; ok { - c.mapConfig[lower] = ConfigMap{Key: key, Value: envVal.Value} + c.overrideConfig[lower] = ConfigMap{Key: key, Value: envVal.Value} c.combinedConfig[lower] = ConfigMap{Key: key, Value: envVal.Value} } else { c.combinedConfig[lower] = ConfigMap{Key: key, Value: value} } } else { - c.combinedConfig[lower] = c.mapConfig[lower] + c.combinedConfig[lower] = c.overrideConfig[lower] } }