AllKeys() should include slices allowing overriding of keys with slices with environment variables

This commit is contained in:
Arieh Schneier 2021-08-05 02:00:45 +10:00
parent a7cfd8b8e0
commit 29dcff61f2
2 changed files with 83 additions and 5 deletions

View file

@ -1035,8 +1035,8 @@ func (v *Viper) Unmarshal(rawVal interface{}, opts ...DecoderConfigOption) error
return decode(v.AllSettings(), defaultDecoderConfig(rawVal, opts...)) return decode(v.AllSettings(), defaultDecoderConfig(rawVal, opts...))
} }
// defaultDecoderConfig returns default mapsstructure.DecoderConfig with suppot // defaultDecoderConfig returns a default mapstructure.DecoderConfig with support
// of time.Duration values & string slices // for time.Duration values & string slices
func defaultDecoderConfig(output interface{}, opts ...DecoderConfigOption) *mapstructure.DecoderConfig { func defaultDecoderConfig(output interface{}, opts ...DecoderConfigOption) *mapstructure.DecoderConfig {
c := &mapstructure.DecoderConfig{ c := &mapstructure.DecoderConfig{
Metadata: nil, Metadata: nil,
@ -1976,6 +1976,8 @@ func (v *Viper) flattenAndMergeMap(shadow map[string]bool, m map[string]interfac
m2 = val.(map[string]interface{}) m2 = val.(map[string]interface{})
case map[interface{}]interface{}: case map[interface{}]interface{}:
m2 = cast.ToStringMap(val) m2 = cast.ToStringMap(val)
case []interface{}:
m2 = castSliceToStringMap(val.([]interface{}))
default: default:
// immediate value // immediate value
shadow[strings.ToLower(fullKey)] = true shadow[strings.ToLower(fullKey)] = true
@ -1987,6 +1989,17 @@ func (v *Viper) flattenAndMergeMap(shadow map[string]bool, m map[string]interfac
return shadow return shadow
} }
// castSliceToStringMap converts a slice to a map where the keys are the indices.
func castSliceToStringMap(v []interface{}) map[string]interface{} {
m := make(map[string]interface{}, len(v))
for i, val := range v {
m[strconv.Itoa(i)] = val
}
return m
}
// mergeFlatMap merges the given maps, excluding values of the second map // mergeFlatMap merges the given maps, excluding values of the second map
// shadowed by values from the first map. // shadowed by values from the first map.
func (v *Viper) mergeFlatMap(shadow map[string]bool, m map[string]interface{}) map[string]bool { func (v *Viper) mergeFlatMap(shadow map[string]bool, m map[string]interface{}) map[string]bool {
@ -2028,9 +2041,46 @@ func (v *Viper) AllSettings() map[string]interface{} {
// set innermost value // set innermost value
deepestMap[lastKey] = value deepestMap[lastKey] = value
} }
// convert any maps of integer keys back to slices
i := convertMapsToSlices(m)
if m2, ok := i.(map[string]interface{}); ok {
m = m2
}
return m return m
} }
// convertMapsToSlices will do a deep check for any maps where the keys are all integers (as strings)
// as well as being contiguous from 0, and convert them to a slice.
func convertMapsToSlices(m map[string]interface{}) interface{} {
allInts := true
for k, v := range m {
if _, ok := strconv.Atoi(k); ok != nil {
allInts = false
}
if m2, ok := v.(map[string]interface{}); ok {
m[k] = convertMapsToSlices(m2)
}
}
if !allInts {
return m
}
s := make([]interface{}, len(m))
for i := 0; i < len(m); i++ {
v, ok := m[strconv.Itoa(i)]
if !ok {
return m
}
s[i] = v
}
return s
}
// SetFs sets the filesystem to use to read configuration. // SetFs sets the filesystem to use to read configuration.
func SetFs(fs afero.Fs) { v.SetFs(fs) } func SetFs(fs afero.Fs) { v.SetFs(fs) }

View file

@ -570,6 +570,29 @@ func TestAutoEnv(t *testing.T) {
assert.Equal(t, "13", Get("foo_bar")) assert.Equal(t, "13", Get("foo_bar"))
} }
func TestAutoEnvWithSlice(t *testing.T) {
initJSON()
AutomaticEnv()
type config struct {
Batters struct {
Batter []struct {
Type string
}
}
}
testutil.Setenv(t, "BATTERS.BATTER.1.TYPE", "Small")
var C config
err := Unmarshal(&C)
if err != nil {
t.Fatalf("unable to decode into struct, %v", err)
}
assert.Equal(t, []struct{ Type string }{{"Regular"}, {"Small"}, {"Blueberry"}, {"Devil's Food"}}, C.Batters.Batter)
}
func TestAutoEnvWithPrefix(t *testing.T) { func TestAutoEnvWithPrefix(t *testing.T) {
Reset() Reset()
@ -620,8 +643,13 @@ func TestAllKeys(t *testing.T) {
"name", "name",
"beard", "beard",
"ppu", "ppu",
"batters.batter", "batters.batter.0.type",
"hobbies", "batters.batter.1.type",
"batters.batter.2.type",
"batters.batter.3.type",
"hobbies.0",
"hobbies.1",
"hobbies.2",
"clothing.jacket", "clothing.jacket",
"clothing.trousers", "clothing.trousers",
"default.import_path", "default.import_path",
@ -1983,7 +2011,7 @@ clothing:
func TestDotParameter(t *testing.T) { func TestDotParameter(t *testing.T) {
initJSON() initJSON()
// shoud take precedence over batters defined in jsonExample // should take precedence over batters defined in jsonExample
r := bytes.NewReader([]byte(`{ "batters.batter": [ { "type": "Small" } ] }`)) r := bytes.NewReader([]byte(`{ "batters.batter": [ { "type": "Small" } ] }`))
unmarshalReader(r, v.config) unmarshalReader(r, v.config)