From 42406af56710fc34a8539e6688452bb7f9ea1a58 Mon Sep 17 00:00:00 2001 From: Albert Vaca Cintora Date: Tue, 19 Apr 2022 21:19:35 +0200 Subject: [PATCH 1/4] Make AllSettings include set-but-empty keys --- viper.go | 4 ++-- viper_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/viper.go b/viper.go index 82d415a..df6dbef 100644 --- a/viper.go +++ b/viper.go @@ -1825,8 +1825,8 @@ func (v *Viper) AllSettings() map[string]interface{} { for _, k := range v.AllKeys() { value := v.Get(k) if value == nil { - // should not happen, since AllKeys() returns only keys holding a value, - // check just in case anything changes + // Key set but empty, include it in the output as a null value + m[k] = nil continue } diff --git a/viper_test.go b/viper_test.go index 21efc4b..e37aa6e 100644 --- a/viper_test.go +++ b/viper_test.go @@ -1818,3 +1818,33 @@ func TestKnownKeys(t *testing.T) { t.Error("SetKnown didn't mark key as known") } } + +func TestEmptySection(t *testing.T) { + + var yamlWithEnvVars = ` +key: + subkey: +another_key: +` + + v := New() + initConfig(v, "yaml", yamlWithEnvVars) + v.SetKnown("is_known") + v.SetDefault("has_default", true) + + // AllKeys includes empty keys + keys := v.AllKeys() + assert.NotContains(t, keys, "key") // Only empty leaf nodes are returned + assert.Contains(t, keys, "key.subkey") + assert.Contains(t, keys, "another_key") + assert.NotContains(t, keys, "is_known") + assert.Contains(t, keys, "has_default") + + // AllSettings includes empty keys + vars := v.AllSettings() + assert.NotContains(t, vars, "key") // Only empty leaf nodes are returned + assert.Contains(t, vars, "key.subkey") + assert.Contains(t, vars, "another_key") + assert.NotContains(t, vars, "is_known") + assert.Contains(t, vars, "has_default") +} From dd14d2c73f2d72211d97d1a14b7741f2ec818bc5 Mon Sep 17 00:00:00 2001 From: Albert Vaca Cintora Date: Wed, 20 Apr 2022 12:38:53 +0200 Subject: [PATCH 2/4] Also consider explicitly set empty dicts as leaf nodes --- viper.go | 4 ++++ viper_test.go | 3 +++ 2 files changed, 7 insertions(+) diff --git a/viper.go b/viper.go index df6dbef..bab5774 100644 --- a/viper.go +++ b/viper.go @@ -1789,6 +1789,10 @@ func (v *Viper) flattenAndMergeMap(shadow map[string]bool, m map[string]interfac shadow[strings.ToLower(fullKey)] = true continue } + if len(m2) == 0 { + // empty dict explicitly present in the map + shadow[strings.ToLower(fullKey)] = true + } // recursively merge to shadow map shadow = v.flattenAndMergeMap(shadow, m2, fullKey) } diff --git a/viper_test.go b/viper_test.go index e37aa6e..64aa766 100644 --- a/viper_test.go +++ b/viper_test.go @@ -1825,6 +1825,7 @@ func TestEmptySection(t *testing.T) { key: subkey: another_key: +empty_dict: {} ` v := New() @@ -1837,6 +1838,7 @@ another_key: assert.NotContains(t, keys, "key") // Only empty leaf nodes are returned assert.Contains(t, keys, "key.subkey") assert.Contains(t, keys, "another_key") + assert.Contains(t, keys, "empty_dict") assert.NotContains(t, keys, "is_known") assert.Contains(t, keys, "has_default") @@ -1845,6 +1847,7 @@ another_key: assert.NotContains(t, vars, "key") // Only empty leaf nodes are returned assert.Contains(t, vars, "key.subkey") assert.Contains(t, vars, "another_key") + assert.Contains(t, vars, "empty_dict") assert.NotContains(t, vars, "is_known") assert.Contains(t, vars, "has_default") } From da54697d4620bf04ad1f982f8013179d98b19916 Mon Sep 17 00:00:00 2001 From: Albert Vaca Cintora Date: Mon, 9 May 2022 15:09:51 +0200 Subject: [PATCH 3/4] Exclude keys bound to env from AllKeys/AllSettings unless env var is set --- viper.go | 20 +++++++++++--------- viper_test.go | 3 +++ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/viper.go b/viper.go index bab5774..7c95d17 100644 --- a/viper.go +++ b/viper.go @@ -339,6 +339,16 @@ func (v *Viper) getEnv(key string) (string, bool) { return val, ok && (v.allowEmptyEnv || val != "") } +func (v *Viper) getAllEnvBoundAndSet() map[string]interface{} { + tgt := map[string]interface{}{} + for key, value := range v.env { + if _, ok := v.getEnv(v.mergeWithEnvPrefix(key)); ok { + tgt[key] = value + } + } + return tgt +} + // ConfigFileUsed returns the file used to populate the config registry. func ConfigFileUsed() string { return v.ConfigFileUsed() } func (v *Viper) ConfigFileUsed() string { return v.configFile } @@ -1571,14 +1581,6 @@ func castToMapStringInterface( return tgt } -func castMapStringSliceToMapInterface(src map[string][]string) map[string]interface{} { - tgt := map[string]interface{}{} - for k, v := range src { - tgt[k] = v - } - return tgt -} - func castMapStringToMapInterface(src map[string]string) map[string]interface{} { tgt := map[string]interface{}{} for k, v := range src { @@ -1745,7 +1747,7 @@ func (v *Viper) AllKeys() []string { m = v.flattenAndMergeMap(m, castMapStringToMapInterface(v.aliases), "") m = v.flattenAndMergeMap(m, v.override, "") m = v.mergeFlatMap(m, castMapFlagToMapInterface(v.pflags)) - m = v.mergeFlatMap(m, castMapStringSliceToMapInterface(v.env)) + m = v.mergeFlatMap(m, v.getAllEnvBoundAndSet()) m = v.flattenAndMergeMap(m, v.config, "") m = v.flattenAndMergeMap(m, v.kvstore, "") m = v.flattenAndMergeMap(m, v.defaults, "") diff --git a/viper_test.go b/viper_test.go index 64aa766..d85adee 100644 --- a/viper_test.go +++ b/viper_test.go @@ -1832,6 +1832,7 @@ empty_dict: {} initConfig(v, "yaml", yamlWithEnvVars) v.SetKnown("is_known") v.SetDefault("has_default", true) + v.BindEnv("is_bound") // AllKeys includes empty keys keys := v.AllKeys() @@ -1841,6 +1842,7 @@ empty_dict: {} assert.Contains(t, keys, "empty_dict") assert.NotContains(t, keys, "is_known") assert.Contains(t, keys, "has_default") + assert.NotContains(t, keys, "is_bound") // AllSettings includes empty keys vars := v.AllSettings() @@ -1850,4 +1852,5 @@ empty_dict: {} assert.Contains(t, vars, "empty_dict") assert.NotContains(t, vars, "is_known") assert.Contains(t, vars, "has_default") + assert.NotContains(t, vars, "is_bound") } From 0b35ab4493163bf062f097293fcf3f55e0325198 Mon Sep 17 00:00:00 2001 From: Albert Vaca Cintora Date: Mon, 9 May 2022 13:19:08 +0200 Subject: [PATCH 4/4] Add Unset method Before, we would set entries to nil to unset them --- viper.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/viper.go b/viper.go index 7c95d17..e7c8edb 100644 --- a/viper.go +++ b/viper.go @@ -1269,6 +1269,22 @@ func (v *Viper) Set(key string, value interface{}) { deepestMap[lastKey] = value } +// Unset removes a value set with Set +// Unset is case-insensitive for a key. +// Values which come from flags, config file, ENV or default can't be unset. +func Unset(key string) { v.Unset(key) } +func (v *Viper) Unset(key string) { + // If alias passed in, then set the proper override + key = v.realKey(strings.ToLower(key)) + + path := strings.Split(key, v.keyDelim) + lastKey := strings.ToLower(path[len(path)-1]) + deepestMap := deepSearch(v.override, path[0:len(path)-1]) + + // unset innermost value + delete(deepestMap, lastKey) +} + // ReadInConfig will discover and load the configuration file from disk // and key/value stores, searching in one of the defined paths. func ReadInConfig() error { return v.ReadInConfig() }