Merge pull request #3 from taigrr/cd/fix-precedence

fix(precedence): env vars now correctly override config file values
This commit is contained in:
2026-03-01 18:40:39 -05:00
committed by GitHub
3 changed files with 77 additions and 18 deletions

23
jety.go
View File

@@ -4,7 +4,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"maps"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -33,7 +32,8 @@ type (
configPath string configPath string
configFileUsed string configFileUsed string
configType configType configType configType
mapConfig map[string]ConfigMap overrideConfig map[string]ConfigMap
fileConfig map[string]ConfigMap
defaultConfig map[string]ConfigMap defaultConfig map[string]ConfigMap
envConfig map[string]ConfigMap envConfig map[string]ConfigMap
combinedConfig map[string]ConfigMap combinedConfig map[string]ConfigMap
@@ -50,7 +50,8 @@ var (
func NewConfigManager() *ConfigManager { func NewConfigManager() *ConfigManager {
cm := ConfigManager{} cm := ConfigManager{}
cm.envConfig = make(map[string]ConfigMap) 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.defaultConfig = make(map[string]ConfigMap)
cm.combinedConfig = make(map[string]ConfigMap) cm.combinedConfig = make(map[string]ConfigMap)
envSet := os.Environ() envSet := os.Environ()
@@ -99,13 +100,21 @@ func (c *ConfigManager) collapse() {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
ccm := make(map[string]ConfigMap) ccm := make(map[string]ConfigMap)
// Precedence (highest to lowest): overrides (Set) > env > file > defaults
for k, v := range c.defaultConfig { for k, v := range c.defaultConfig {
ccm[k] = v 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 c.combinedConfig = ccm
} }
@@ -216,7 +225,7 @@ func (c *ConfigManager) ReadInConfig() error {
conf[lower] = ConfigMap{Key: k, Value: v} conf[lower] = ConfigMap{Key: k, Value: v}
} }
c.mutex.Lock() c.mutex.Lock()
c.mapConfig = conf c.fileConfig = conf
c.configFileUsed = configFile c.configFileUsed = configFile
c.mutex.Unlock() c.mutex.Unlock()
c.collapse() c.collapse()

View File

@@ -66,7 +66,7 @@ func TestNewConfigManager(t *testing.T) {
if cm.envConfig == nil { if cm.envConfig == nil {
t.Error("envConfig not initialized") t.Error("envConfig not initialized")
} }
if cm.mapConfig == nil { if cm.overrideConfig == nil {
t.Error("mapConfig not initialized") t.Error("mapConfig not initialized")
} }
if cm.defaultConfig == nil { 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") os.Setenv("PORT", "5000")
defer os.Unsetenv("PORT") defer os.Unsetenv("PORT")
@@ -575,9 +575,9 @@ func TestConfigFileOverridesEnv(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
// Config file should override env and default // Env should override config file (env > file > defaults)
if got := cm.GetInt("port"); got != 9000 { if got := cm.GetInt("port"); got != 5000 {
t.Errorf("GetInt(port) = %d, want 9000 (from file)", got) 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) 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)
}
}

View File

@@ -8,7 +8,7 @@ func (c *ConfigManager) SetBool(key string, value bool) {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
lower := strings.ToLower(key) 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} c.combinedConfig[lower] = ConfigMap{Key: key, Value: value}
} }
@@ -16,7 +16,7 @@ func (c *ConfigManager) SetString(key string, value string) {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
lower := strings.ToLower(key) 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} c.combinedConfig[lower] = ConfigMap{Key: key, Value: value}
} }
@@ -24,7 +24,7 @@ func (c *ConfigManager) Set(key string, value any) {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
lower := strings.ToLower(key) 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} c.combinedConfig[lower] = ConfigMap{Key: key, Value: value}
} }
@@ -33,14 +33,14 @@ func (c *ConfigManager) SetDefault(key string, value any) {
defer c.mutex.Unlock() defer c.mutex.Unlock()
lower := strings.ToLower(key) lower := strings.ToLower(key)
c.defaultConfig[lower] = ConfigMap{Key: key, Value: value} 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 { 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} c.combinedConfig[lower] = ConfigMap{Key: key, Value: envVal.Value}
} else { } else {
c.combinedConfig[lower] = ConfigMap{Key: key, Value: value} c.combinedConfig[lower] = ConfigMap{Key: key, Value: value}
} }
} else { } else {
c.combinedConfig[lower] = c.mapConfig[lower] c.combinedConfig[lower] = c.overrideConfig[lower]
} }
} }