3 Commits

Author SHA1 Message Date
4c8d8960be fix(precedence): env vars now correctly override config file values
The documented precedence is Set > env > file > defaults, but collapse()
was using maps.Copy(ccm, mapConfig) which let file values (and Set values,
stored in the same map) unconditionally overwrite env values.

Split mapConfig into fileConfig (from ReadInConfig) and overrideConfig
(from Set/SetString/SetBool). collapse() now applies layers in correct
order: defaults, then file, then env (for known keys), then overrides.

Added TestPrecedenceChain to verify the full layering.
2026-03-01 23:37:24 +00:00
6852ffbebb Merge pull request #2 from taigrr/cd/fix-set-env-prefix
fix!: SetEnvPrefix now re-reads env vars with prefix stripping
2026-03-01 15:42:57 -05:00
7f2320f204 Merge pull request #1 from taigrr/cd/update-go-deps-and-tests
chore: update Go 1.26.0, deps, and test coverage to 97%
2026-02-24 02:08:49 -05:00
3 changed files with 77 additions and 18 deletions

23
jety.go
View File

@@ -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()

View File

@@ -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)
}
}

View File

@@ -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]
}
}