mirror of
https://github.com/taigrr/jety.git
synced 2026-04-01 19:08:58 -07:00
feat: add IsSet, AllKeys, AllSettings; fix env precedence for file-only keys
- collapse() now applies env vars for keys present in fileConfig, not just defaultConfig. Previously, env vars couldn't override file values unless a default was also set for that key. - SetDefault no longer pollutes overrideConfig; it correctly resolves the value by checking override > env > file > default. - Remove unused explicitDefaults field and UseExplicitDefaults method. - Add IsSet, AllKeys, AllSettings methods + package-level wrappers. - Add missing package-level wrappers: Get, SetBool, SetString, SetConfigDir, WithEnvPrefix. - Add tests for all new methods and the env-over-file-without-default fix.
This commit is contained in:
32
default.go
32
default.go
@@ -67,3 +67,35 @@ func GetStringMap(key string) map[string]any {
|
||||
func GetStringSlice(key string) []string {
|
||||
return defaultConfigManager.GetStringSlice(key)
|
||||
}
|
||||
|
||||
func Get(key string) any {
|
||||
return defaultConfigManager.Get(key)
|
||||
}
|
||||
|
||||
func SetBool(key string, value bool) {
|
||||
defaultConfigManager.SetBool(key, value)
|
||||
}
|
||||
|
||||
func SetString(key string, value string) {
|
||||
defaultConfigManager.SetString(key, value)
|
||||
}
|
||||
|
||||
func SetConfigDir(path string) {
|
||||
defaultConfigManager.SetConfigDir(path)
|
||||
}
|
||||
|
||||
func WithEnvPrefix(prefix string) *ConfigManager {
|
||||
return defaultConfigManager.WithEnvPrefix(prefix)
|
||||
}
|
||||
|
||||
func IsSet(key string) bool {
|
||||
return defaultConfigManager.IsSet(key)
|
||||
}
|
||||
|
||||
func AllKeys() []string {
|
||||
return defaultConfigManager.AllKeys()
|
||||
}
|
||||
|
||||
func AllSettings() map[string]any {
|
||||
return defaultConfigManager.AllSettings()
|
||||
}
|
||||
|
||||
73
jety.go
73
jety.go
@@ -28,17 +28,16 @@ type (
|
||||
}
|
||||
|
||||
ConfigManager struct {
|
||||
configName string
|
||||
configPath string
|
||||
configFileUsed string
|
||||
configType configType
|
||||
overrideConfig map[string]ConfigMap
|
||||
fileConfig map[string]ConfigMap
|
||||
defaultConfig map[string]ConfigMap
|
||||
envConfig map[string]ConfigMap
|
||||
combinedConfig map[string]ConfigMap
|
||||
mutex sync.RWMutex
|
||||
explicitDefaults bool
|
||||
configName string
|
||||
configPath string
|
||||
configFileUsed string
|
||||
configType configType
|
||||
overrideConfig map[string]ConfigMap
|
||||
fileConfig map[string]ConfigMap
|
||||
defaultConfig map[string]ConfigMap
|
||||
envConfig map[string]ConfigMap
|
||||
combinedConfig map[string]ConfigMap
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
)
|
||||
|
||||
@@ -90,10 +89,48 @@ func (c *ConfigManager) ConfigFileUsed() string {
|
||||
return c.configFileUsed
|
||||
}
|
||||
|
||||
func (c *ConfigManager) UseExplicitDefaults(enable bool) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
c.explicitDefaults = enable
|
||||
// IsSet checks whether a key has been set in any configuration source.
|
||||
func (c *ConfigManager) IsSet(key string) bool {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
lower := strings.ToLower(key)
|
||||
if _, ok := c.combinedConfig[lower]; ok {
|
||||
return true
|
||||
}
|
||||
_, ok := c.envConfig[lower]
|
||||
return ok
|
||||
}
|
||||
|
||||
// AllKeys returns all keys from all configuration sources, deduplicated.
|
||||
func (c *ConfigManager) AllKeys() []string {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
seen := make(map[string]struct{})
|
||||
var keys []string
|
||||
for k := range c.combinedConfig {
|
||||
if _, ok := seen[k]; !ok {
|
||||
seen[k] = struct{}{}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
for k := range c.envConfig {
|
||||
if _, ok := seen[k]; !ok {
|
||||
seen[k] = struct{}{}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// AllSettings returns all settings as a flat map of key to value.
|
||||
func (c *ConfigManager) AllSettings() map[string]any {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
result := make(map[string]any, len(c.combinedConfig))
|
||||
for k, v := range c.combinedConfig {
|
||||
result[k] = v.Value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *ConfigManager) collapse() {
|
||||
@@ -107,8 +144,10 @@ func (c *ConfigManager) collapse() {
|
||||
for k, v := range c.fileConfig {
|
||||
ccm[k] = v
|
||||
}
|
||||
for k := range c.defaultConfig {
|
||||
if v, ok := c.envConfig[k]; ok {
|
||||
for k, v := range c.envConfig {
|
||||
if _, inDefaults := c.defaultConfig[k]; inDefaults {
|
||||
ccm[k] = v
|
||||
} else if _, inFile := c.fileConfig[k]; inFile {
|
||||
ccm[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
91
jety_test.go
91
jety_test.go
@@ -760,17 +760,6 @@ func TestPackageLevelFunctions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseExplicitDefaults(t *testing.T) {
|
||||
cm := NewConfigManager()
|
||||
cm.UseExplicitDefaults(true)
|
||||
|
||||
// Just verify it doesn't panic and the field is set
|
||||
cm.SetDefault("key", "value")
|
||||
if got := cm.GetString("key"); got != "value" {
|
||||
t.Errorf("GetString(key) = %q, want %q", got, "value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetString(t *testing.T) {
|
||||
cm := NewConfigManager()
|
||||
cm.SetString("name", "test")
|
||||
@@ -1397,3 +1386,83 @@ func TestPrecedenceChain(t *testing.T) {
|
||||
t.Errorf("extra: got %q, want defaultextra (default)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSet(t *testing.T) {
|
||||
cm := NewConfigManager()
|
||||
cm.Set("exists", "yes")
|
||||
|
||||
if !cm.IsSet("exists") {
|
||||
t.Error("IsSet(exists) = false, want true")
|
||||
}
|
||||
if cm.IsSet("nope") {
|
||||
t.Error("IsSet(nope) = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllKeys(t *testing.T) {
|
||||
cm := NewConfigManager()
|
||||
cm.SetDefault("a", 1)
|
||||
cm.Set("b", 2)
|
||||
|
||||
keys := cm.AllKeys()
|
||||
found := make(map[string]bool)
|
||||
for _, k := range keys {
|
||||
found[k] = true
|
||||
}
|
||||
if !found["a"] || !found["b"] {
|
||||
t.Errorf("AllKeys() = %v, want to contain a and b", keys)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllSettings(t *testing.T) {
|
||||
cm := NewConfigManager()
|
||||
cm.Set("port", 8080)
|
||||
cm.Set("host", "localhost")
|
||||
|
||||
settings := cm.AllSettings()
|
||||
if settings["port"] != 8080 || settings["host"] != "localhost" {
|
||||
t.Errorf("AllSettings() = %v", settings)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvOverridesFileWithoutDefault(t *testing.T) {
|
||||
// Bug fix: env should override file even when no default is set for that key
|
||||
os.Setenv("HOST", "envhost")
|
||||
defer os.Unsetenv("HOST")
|
||||
|
||||
dir := t.TempDir()
|
||||
configFile := filepath.Join(dir, "config.yaml")
|
||||
if err := os.WriteFile(configFile, []byte("host: filehost"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cm := NewConfigManager()
|
||||
cm.SetConfigFile(configFile)
|
||||
if err := cm.SetConfigType("yaml"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := cm.ReadInConfig(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// No SetDefault("host", ...) was called — env should still win
|
||||
if got := cm.GetString("host"); got != "envhost" {
|
||||
t.Errorf("GetString(host) = %q, want envhost (env overrides file even without default)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackageLevelIsSet(t *testing.T) {
|
||||
defaultConfigManager = NewConfigManager()
|
||||
Set("x", 1)
|
||||
if !IsSet("x") {
|
||||
t.Error("IsSet(x) = false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackageLevelGet(t *testing.T) {
|
||||
defaultConfigManager = NewConfigManager()
|
||||
Set("key", "val")
|
||||
if Get("key") != "val" {
|
||||
t.Error("Get(key) failed")
|
||||
}
|
||||
}
|
||||
|
||||
16
setters.go
16
setters.go
@@ -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.overrideConfig[lower]; !ok {
|
||||
if envVal, ok := c.envConfig[lower]; ok {
|
||||
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}
|
||||
}
|
||||
// Update combinedConfig respecting precedence: override > env > file > default
|
||||
if v, ok := c.overrideConfig[lower]; ok {
|
||||
c.combinedConfig[lower] = v
|
||||
} else if v, ok := c.envConfig[lower]; ok {
|
||||
c.combinedConfig[lower] = ConfigMap{Key: key, Value: v.Value}
|
||||
} else if v, ok := c.fileConfig[lower]; ok {
|
||||
c.combinedConfig[lower] = v
|
||||
} else {
|
||||
c.combinedConfig[lower] = c.overrideConfig[lower]
|
||||
c.combinedConfig[lower] = ConfigMap{Key: key, Value: value}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user