Nested maps (#195)
Fixes #71, #93, #158, #168, #209, #141, #160, #162, #190
* Fixed: indentation in comment
* Fixed: Get() returns nil when nested element not found
* Fixed: insensitiviseMaps() made recursive so that nested keys are lowercased
* Fixed: order of expected<=>actual in assert.Equal() statements
* Fixed: find() looks into "overrides" first
* Fixed: TestBindPFlags() to use a new Viper instance
* Fixed: removed extra aliases from display in Debug()
* Added: test for checking precedence of dot-containing keys.
* Fixed: Set() and SetDefault() insert nested values
* Added: tests for overriding nested values
* Changed: AllKeys() includes all keys / AllSettings() includes overridden nested values
* Added: test for shadowed nested key
* Fixed: properties parsing generates nested maps
* Fixed: Get() and IsSet() work correctly on nested values
* Changed: modifier README.md to reflect changes
diff --git a/README.md b/README.md
index cf17560..f4e72f8 100644
--- a/README.md
+++ b/README.md
@@ -458,16 +458,17 @@
GetString("datastore.metric.host") // (returns "127.0.0.1")
```
-This obeys the precedence rules established above; the search for the root key
-(in this example, `datastore`) will cascade through the remaining configuration
-registries until found. The search for the sub-keys (`metric` and `host`),
-however, will not.
+This obeys the precedence rules established above; the search for the path
+will cascade through the remaining configuration registries until found.
-For example, if the `metric` key was not defined in the configuration loaded
-from file, but was defined in the defaults, Viper would return the zero value.
+For example, given this configuration file, both `datastore.metric.host` and
+`datastore.metric.port` are already defined (and may be overridden). If in addition
+`datastore.metric.protocol` was defined in the defaults, Viper would also find it.
-On the other hand, if the primary key was not defined, Viper would go through
-the remaining registries looking for it.
+However, if `datastore.metric` was overridden (by a flag, an environment variable,
+the `Set()` method, …) with an immediate value, then all sub-keys of
+`datastore.metric` become undefined, they are “shadowed” by the higher-priority
+configuration level.
Lastly, if there exists a key that matches the delimited key path, its value
will be returned instead. E.g.
@@ -491,7 +492,7 @@
}
}
-GetString("datastore.metric.host") //returns "0.0.0.0"
+GetString("datastore.metric.host") // returns "0.0.0.0"
```
### Extract sub-tree
diff --git a/overrides_test.go b/overrides_test.go
new file mode 100644
index 0000000..dd2aa9b
--- /dev/null
+++ b/overrides_test.go
@@ -0,0 +1,173 @@
+package viper
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ "github.com/spf13/cast"
+ "github.com/stretchr/testify/assert"
+)
+
+type layer int
+
+const (
+ defaultLayer layer = iota + 1
+ overrideLayer
+)
+
+func TestNestedOverrides(t *testing.T) {
+ assert := assert.New(t)
+ var v *Viper
+
+ // Case 0: value overridden by a value
+ overrideDefault(assert, "tom", 10, "tom", 20) // "tom" is first given 10 as default value, then overridden by 20
+ override(assert, "tom", 10, "tom", 20) // "tom" is first given value 10, then overridden by 20
+ overrideDefault(assert, "tom.age", 10, "tom.age", 20)
+ override(assert, "tom.age", 10, "tom.age", 20)
+ overrideDefault(assert, "sawyer.tom.age", 10, "sawyer.tom.age", 20)
+ override(assert, "sawyer.tom.age", 10, "sawyer.tom.age", 20)
+
+ // Case 1: key:value overridden by a value
+ v = overrideDefault(assert, "tom.age", 10, "tom", "boy") // "tom.age" is first given 10 as default value, then "tom" is overridden by "boy"
+ assert.Nil(v.Get("tom.age")) // "tom.age" should not exist anymore
+ v = override(assert, "tom.age", 10, "tom", "boy")
+ assert.Nil(v.Get("tom.age"))
+
+ // Case 2: value overridden by a key:value
+ overrideDefault(assert, "tom", "boy", "tom.age", 10) // "tom" is first given "boy" as default value, then "tom" is overridden by map{"age":10}
+ override(assert, "tom.age", 10, "tom", "boy")
+
+ // Case 3: key:value overridden by a key:value
+ v = overrideDefault(assert, "tom.size", 4, "tom.age", 10)
+ assert.Equal(4, v.Get("tom.size")) // value should still be reachable
+ v = override(assert, "tom.size", 4, "tom.age", 10)
+ assert.Equal(4, v.Get("tom.size"))
+ deepCheckValue(assert, v, overrideLayer, []string{"tom", "size"}, 4)
+
+ // Case 4: key:value overridden by a map
+ v = overrideDefault(assert, "tom.size", 4, "tom", map[string]interface{}{"age": 10}) // "tom.size" is first given "4" as default value, then "tom" is overridden by map{"age":10}
+ assert.Equal(4, v.Get("tom.size")) // "tom.size" should still be reachable
+ assert.Equal(10, v.Get("tom.age")) // new value should be there
+ deepCheckValue(assert, v, overrideLayer, []string{"tom", "age"}, 10) // new value should be there
+ v = override(assert, "tom.size", 4, "tom", map[string]interface{}{"age": 10})
+ assert.Nil(v.Get("tom.size"))
+ assert.Equal(10, v.Get("tom.age"))
+ deepCheckValue(assert, v, overrideLayer, []string{"tom", "age"}, 10)
+
+ // Case 5: array overridden by a value
+ overrideDefault(assert, "tom", []int{10, 20}, "tom", 30)
+ override(assert, "tom", []int{10, 20}, "tom", 30)
+ overrideDefault(assert, "tom.age", []int{10, 20}, "tom.age", 30)
+ override(assert, "tom.age", []int{10, 20}, "tom.age", 30)
+
+ // Case 6: array overridden by an array
+ overrideDefault(assert, "tom", []int{10, 20}, "tom", []int{30, 40})
+ override(assert, "tom", []int{10, 20}, "tom", []int{30, 40})
+ overrideDefault(assert, "tom.age", []int{10, 20}, "tom.age", []int{30, 40})
+ v = override(assert, "tom.age", []int{10, 20}, "tom.age", []int{30, 40})
+ // explicit array merge:
+ s, ok := v.Get("tom.age").([]int)
+ if assert.True(ok, "tom[\"age\"] is not a slice") {
+ v.Set("tom.age", append(s, []int{50, 60}...))
+ assert.Equal([]int{30, 40, 50, 60}, v.Get("tom.age"))
+ deepCheckValue(assert, v, overrideLayer, []string{"tom", "age"}, []int{30, 40, 50, 60})
+ }
+}
+
+func overrideDefault(assert *assert.Assertions, firstPath string, firstValue interface{}, secondPath string, secondValue interface{}) *Viper {
+ return overrideFromLayer(defaultLayer, assert, firstPath, firstValue, secondPath, secondValue)
+}
+func override(assert *assert.Assertions, firstPath string, firstValue interface{}, secondPath string, secondValue interface{}) *Viper {
+ return overrideFromLayer(overrideLayer, assert, firstPath, firstValue, secondPath, secondValue)
+}
+
+// overrideFromLayer performs the sequential override and low-level checks.
+//
+// First assignment is made on layer l for path firstPath with value firstValue,
+// the second one on the override layer (i.e., with the Set() function)
+// for path secondPath with value secondValue.
+//
+// firstPath and secondPath can include an arbitrary number of dots to indicate
+// a nested element.
+//
+// After each assignment, the value is checked, retrieved both by its full path
+// and by its key sequence (successive maps).
+func overrideFromLayer(l layer, assert *assert.Assertions, firstPath string, firstValue interface{}, secondPath string, secondValue interface{}) *Viper {
+ v := New()
+ firstKeys := strings.Split(firstPath, v.keyDelim)
+ if assert == nil ||
+ len(firstKeys) == 0 || len(firstKeys[0]) == 0 {
+ return v
+ }
+
+ // Set and check first value
+ switch l {
+ case defaultLayer:
+ v.SetDefault(firstPath, firstValue)
+ case overrideLayer:
+ v.Set(firstPath, firstValue)
+ default:
+ return v
+ }
+ assert.Equal(firstValue, v.Get(firstPath))
+ deepCheckValue(assert, v, l, firstKeys, firstValue)
+
+ // Override and check new value
+ secondKeys := strings.Split(secondPath, v.keyDelim)
+ if len(secondKeys) == 0 || len(secondKeys[0]) == 0 {
+ return v
+ }
+ v.Set(secondPath, secondValue)
+ assert.Equal(secondValue, v.Get(secondPath))
+ deepCheckValue(assert, v, overrideLayer, secondKeys, secondValue)
+
+ return v
+}
+
+// deepCheckValue checks that all given keys correspond to a valid path in the
+// configuration map of the given layer, and that the final value equals the one given
+func deepCheckValue(assert *assert.Assertions, v *Viper, l layer, keys []string, value interface{}) {
+ if assert == nil || v == nil ||
+ len(keys) == 0 || len(keys[0]) == 0 {
+ return
+ }
+
+ // init
+ var val interface{}
+ var ms string
+ switch l {
+ case defaultLayer:
+ val = v.defaults
+ ms = "v.defaults"
+ case overrideLayer:
+ val = v.override
+ ms = "v.override"
+ }
+
+ // loop through map
+ var m map[string]interface{}
+ err := false
+ for _, k := range keys {
+ if val == nil {
+ assert.Fail(fmt.Sprintf("%s is not a map[string]interface{}", ms))
+ return
+ }
+
+ // deep scan of the map to get the final value
+ switch val.(type) {
+ case map[interface{}]interface{}:
+ m = cast.ToStringMap(val)
+ case map[string]interface{}:
+ m = val.(map[string]interface{})
+ default:
+ assert.Fail(fmt.Sprintf("%s is not a map[string]interface{}", ms))
+ return
+ }
+ ms = ms + "[\"" + k + "\"]"
+ val = m[k]
+ }
+ if !err {
+ assert.Equal(value, val)
+ }
+}
diff --git a/util.go b/util.go
index 5f93d65..b0903fb 100644
--- a/util.go
+++ b/util.go
@@ -45,6 +45,10 @@
if key != lower {
delete(m, key)
m[lower] = val
+ if m2, ok := val.(map[string]interface{}); ok {
+ // nested map: recursively insensitivise
+ insensitiviseMap(m2)
+ }
}
}
}
@@ -149,7 +153,12 @@
}
for _, key := range p.Keys() {
value, _ := p.Get(key)
- c[key] = value
+ // recursively build nested maps
+ path := strings.Split(key, ".")
+ lastKey := strings.ToLower(path[len(path)-1])
+ deepestMap := deepSearch(c, path[0:len(path)-1])
+ // set innermost value
+ deepestMap[lastKey] = value
}
}
@@ -199,3 +208,34 @@
return safeMul(uint(size), multiplier)
}
+
+// deepSearch scans deep maps, following the key indexes listed in the
+// sequence "path".
+// The last value is expected to be another map, and is returned.
+//
+// In case intermediate keys do not exist, or map to a non-map value,
+// a new map is created and inserted, and the search continues from there:
+// the initial map "m" may be modified!
+func deepSearch(m map[string]interface{}, path []string) map[string]interface{} {
+ for _, k := range path {
+ m2, ok := m[k]
+ if !ok {
+ // intermediate key does not exist
+ // => create it and continue from there
+ m3 := make(map[string]interface{})
+ m[k] = m3
+ m = m3
+ continue
+ }
+ m3, ok := m2.(map[string]interface{})
+ if !ok {
+ // intermediate key is a value
+ // => replace with a new map
+ m3 = make(map[string]interface{})
+ m[k] = m3
+ }
+ // continue search from here
+ m = m3
+ }
+ return m
+}
diff --git a/viper.go b/viper.go
index 6964041..8f27849 100644
--- a/viper.go
+++ b/viper.go
@@ -107,11 +107,11 @@
// Defaults : {
// "secret": "",
// "user": "default",
-// "endpoint": "https://localhost"
+// "endpoint": "https://localhost"
// }
// Config : {
// "user": "root"
-// "secret": "defaultsecret"
+// "secret": "defaultsecret"
// }
// Env : {
// "secret": "somesecretkey"
@@ -399,8 +399,9 @@
return false
}
+// searchMap recursively searches for a value for path in source map.
+// Returns nil if not found.
func (v *Viper) searchMap(source map[string]interface{}, path []string) interface{} {
-
if len(path) == 0 {
return source
}
@@ -424,11 +425,133 @@
// if the type of `next` is the same as the type being asserted
return v.searchMap(next.(map[string]interface{}), path[1:])
default:
- return next
+ if len(path) == 1 {
+ return next
+ }
+ // got a value but nested key expected, return "nil" for not found
+ return nil
}
- } else {
- return nil
}
+ return nil
+}
+
+// searchMapWithPathPrefixes recursively searches for a value for path in source map.
+//
+// While searchMap() considers each path element as a single map key, this
+// function searches for, and prioritizes, merged path elements.
+// e.g., if in the source, "foo" is defined with a sub-key "bar", and "foo.bar"
+// is also defined, this latter value is returned for path ["foo", "bar"].
+//
+// This should be useful only at config level (other maps may not contain dots
+// in their keys).
+func (v *Viper) searchMapWithPathPrefixes(source map[string]interface{}, path []string) interface{} {
+ if len(path) == 0 {
+ return source
+ }
+
+ // search for path prefixes, starting from the longest one
+ for i := len(path); i > 0; i-- {
+ prefixKey := strings.ToLower(strings.Join(path[0:i], v.keyDelim))
+
+ var ok bool
+ var next interface{}
+ for k, v := range source {
+ if strings.ToLower(k) == prefixKey {
+ ok = true
+ next = v
+ break
+ }
+ }
+
+ if ok {
+ var val interface{}
+ switch next.(type) {
+ case map[interface{}]interface{}:
+ val = v.searchMapWithPathPrefixes(cast.ToStringMap(next), path[i:])
+ case map[string]interface{}:
+ // Type assertion is safe here since it is only reached
+ // if the type of `next` is the same as the type being asserted
+ val = v.searchMapWithPathPrefixes(next.(map[string]interface{}), path[i:])
+ default:
+ if len(path) == i {
+ val = next
+ }
+ // got a value but nested key expected, do nothing and look for next prefix
+ }
+ if val != nil {
+ return val
+ }
+ }
+ }
+
+ // not found
+ return nil
+}
+
+// isPathShadowedInDeepMap makes sure the given path is not shadowed somewhere
+// on its path in the map.
+// e.g., if "foo.bar" has a value in the given map, it “shadows”
+// "foo.bar.baz" in a lower-priority map
+func (v *Viper) isPathShadowedInDeepMap(path []string, m map[string]interface{}) string {
+ var parentVal interface{}
+ for i := 1; i < len(path); i++ {
+ parentVal = v.searchMap(m, path[0:i])
+ if parentVal == nil {
+ // not found, no need to add more path elements
+ return ""
+ }
+ switch parentVal.(type) {
+ case map[interface{}]interface{}:
+ continue
+ case map[string]interface{}:
+ continue
+ default:
+ // parentVal is a regular value which shadows "path"
+ return strings.Join(path[0:i], v.keyDelim)
+ }
+ }
+ return ""
+}
+
+// isPathShadowedInFlatMap makes sure the given path is not shadowed somewhere
+// in a sub-path of the map.
+// e.g., if "foo.bar" has a value in the given map, it “shadows”
+// "foo.bar.baz" in a lower-priority map
+func (v *Viper) isPathShadowedInFlatMap(path []string, mi interface{}) string {
+ // unify input map
+ var m map[string]interface{}
+ switch mi.(type) {
+ case map[string]string, map[string]FlagValue:
+ m = cast.ToStringMap(mi)
+ default:
+ return ""
+ }
+
+ // scan paths
+ var parentKey string
+ for i := 1; i < len(path); i++ {
+ parentKey = strings.Join(path[0:i], v.keyDelim)
+ if _, ok := m[parentKey]; ok {
+ return parentKey
+ }
+ }
+ return ""
+}
+
+// isPathShadowedInAutoEnv makes sure the given path is not shadowed somewhere
+// in the environment, when automatic env is on.
+// e.g., if "foo.bar" has a value in the environment, it “shadows”
+// "foo.bar.baz" in a lower-priority map
+func (v *Viper) isPathShadowedInAutoEnv(path []string) string {
+ var parentKey string
+ var val string
+ for i := 1; i < len(path); i++ {
+ parentKey = strings.Join(path[0:i], v.keyDelim)
+ if val = v.getEnv(v.mergeWithEnvPrefix(parentKey)); val != "" {
+ return parentKey
+ }
+ }
+ return ""
}
// SetTypeByDefaultValue enables or disables the inference of a key value's
@@ -465,46 +588,16 @@
func (v *Viper) Get(key string) interface{} {
lcaseKey := strings.ToLower(key)
val := v.find(lcaseKey)
-
- if val == nil {
- path := strings.Split(key, v.keyDelim)
- source := v.find(strings.ToLower(path[0]))
- if source != nil {
- if reflect.TypeOf(source).Kind() == reflect.Map {
- val = v.searchMap(cast.ToStringMap(source), path[1:])
- }
- }
- }
-
- // if no other value is returned and a flag does exist for the value,
- // get the flag's value even if the flag's value has not changed
- if val == nil {
- if flag, exists := v.pflags[lcaseKey]; exists {
- jww.TRACE.Println(key, "get pflag default", val)
- switch flag.ValueType() {
- case "int", "int8", "int16", "int32", "int64":
- val = cast.ToInt(flag.ValueString())
- case "bool":
- val = cast.ToBool(flag.ValueString())
- default:
- val = flag.ValueString()
- }
- }
- }
-
if val == nil {
return nil
}
- var valType interface{}
- if !v.typeByDefValue {
- valType = val
- } else {
- defVal, defExists := v.defaults[lcaseKey]
- if defExists {
+ valType := val
+ if v.typeByDefValue {
+ path := strings.Split(lcaseKey, v.keyDelim)
+ defVal := v.searchMap(v.defaults, path)
+ if defVal != nil {
valType = defVal
- } else {
- valType = val
}
}
@@ -752,10 +845,27 @@
var val interface{}
var exists bool
+ // compute the path through the nested maps to the nested value
+ path := strings.Split(key, v.keyDelim)
+ if shadow := v.isPathShadowedInDeepMap(path, castMapStringToMapInterface(v.aliases)); shadow != "" {
+ return nil
+ }
+
// if the requested key is an alias, then return the proper key
key = v.realKey(key)
+ // re-compute the path
+ path = strings.Split(key, v.keyDelim)
- // PFlag Override first
+ // Set() override first
+ val = v.searchMap(v.override, path)
+ if val != nil {
+ return val
+ }
+ if shadow := v.isPathShadowedInDeepMap(path, v.override); shadow != "" {
+ return nil
+ }
+
+ // PFlag override next
flag, exists := v.pflags[key]
if exists && flag.HasChanged() {
switch flag.ValueType() {
@@ -770,56 +880,74 @@
return flag.ValueString()
}
}
-
- val, exists = v.override[key]
- if exists {
- return val
+ if shadow := v.isPathShadowedInFlatMap(path, v.pflags); shadow != "" {
+ return nil
}
+ // Env override next
if v.automaticEnvApplied {
// even if it hasn't been registered, if automaticEnv is used,
// check any Get request
if val = v.getEnv(v.mergeWithEnvPrefix(key)); val != "" {
return val
}
+ if shadow := v.isPathShadowedInAutoEnv(path); shadow != "" {
+ return nil
+ }
}
-
envkey, exists := v.env[key]
if exists {
if val = v.getEnv(envkey); val != "" {
return val
}
}
-
- val, exists = v.config[key]
- if exists {
- return val
+ if shadow := v.isPathShadowedInFlatMap(path, v.env); shadow != "" {
+ return nil
}
- // Test for nested config parameter
- if strings.Contains(key, v.keyDelim) {
- path := strings.Split(key, v.keyDelim)
+ // Config file next
+ val = v.searchMapWithPathPrefixes(v.config, path)
+ if val != nil {
+ return val
+ }
+ if shadow := v.isPathShadowedInDeepMap(path, v.config); shadow != "" {
+ return nil
+ }
- source := v.find(path[0])
- if source != nil {
- if reflect.TypeOf(source).Kind() == reflect.Map {
- val := v.searchMap(cast.ToStringMap(source), path[1:])
- if val != nil {
- return val
- }
- }
+ // K/V store next
+ val = v.searchMap(v.kvstore, path)
+ if val != nil {
+ return val
+ }
+ if shadow := v.isPathShadowedInDeepMap(path, v.kvstore); shadow != "" {
+ return nil
+ }
+
+ // Default next
+ val = v.searchMap(v.defaults, path)
+ if val != nil {
+ return val
+ }
+ if shadow := v.isPathShadowedInDeepMap(path, v.defaults); shadow != "" {
+ return nil
+ }
+
+ // last chance: if no other value is returned and a flag does exist for the value,
+ // get the flag's value even if the flag's value has not changed
+ if flag, exists := v.pflags[key]; exists {
+ switch flag.ValueType() {
+ case "int", "int8", "int16", "int32", "int64":
+ return cast.ToInt(flag.ValueString())
+ case "bool":
+ return cast.ToBool(flag.ValueString())
+ case "stringSlice":
+ s := strings.TrimPrefix(flag.ValueString(), "[")
+ return strings.TrimSuffix(s, "]")
+ default:
+ return flag.ValueString()
}
}
-
- val, exists = v.kvstore[key]
- if exists {
- return val
- }
-
- val, exists = v.defaults[key]
- if exists {
- return val
- }
+ // last item, no need to check shadowing
return nil
}
@@ -827,20 +955,8 @@
// IsSet checks to see if the key has been set in any of the data locations.
func IsSet(key string) bool { return v.IsSet(key) }
func (v *Viper) IsSet(key string) bool {
- path := strings.Split(key, v.keyDelim)
-
lcaseKey := strings.ToLower(key)
val := v.find(lcaseKey)
-
- if val == nil {
- source := v.find(strings.ToLower(path[0]))
- if source != nil {
- if reflect.TypeOf(source).Kind() == reflect.Map {
- val = v.searchMap(cast.ToStringMap(source), path[1:])
- }
- }
- }
-
return val != nil
}
@@ -923,7 +1039,13 @@
func (v *Viper) SetDefault(key string, value interface{}) {
// If alias passed in, then set the proper default
key = v.realKey(strings.ToLower(key))
- v.defaults[key] = value
+
+ path := strings.Split(key, v.keyDelim)
+ lastKey := strings.ToLower(path[len(path)-1])
+ deepestMap := deepSearch(v.defaults, path[0:len(path)-1])
+
+ // set innermost value
+ deepestMap[lastKey] = value
}
// Set sets the value for the key in the override regiser.
@@ -933,7 +1055,13 @@
func (v *Viper) Set(key string, value interface{}) {
// If alias passed in, then set the proper override
key = v.realKey(strings.ToLower(key))
- v.override[key] = value
+
+ path := strings.Split(key, v.keyDelim)
+ lastKey := strings.ToLower(path[len(path)-1])
+ deepestMap := deepSearch(v.override, path[0:len(path)-1])
+
+ // set innermost value
+ deepestMap[lastKey] = value
}
// ReadInConfig will discover and load the configuration file from disk
@@ -1013,6 +1141,14 @@
return tgt
}
+func castMapStringToMapInterface(src map[string]string) map[string]interface{} {
+ tgt := map[string]interface{}{}
+ for k, v := range src {
+ tgt[k] = v
+ }
+ return tgt
+}
+
// mergeMaps merges two maps. The `itgt` parameter is for handling go-yaml's
// insistence on parsing nested structures as `map[interface{}]interface{}`
// instead of using a `string` as the key for nest structures beyond one level
@@ -1150,55 +1286,114 @@
return v.kvstore, err
}
-// AllKeys returns all keys regardless where they are set.
+// AllKeys returns all keys holding a value, regardless of where they are set.
+// Nested keys are returned with a v.keyDelim (= ".") separator
func AllKeys() []string { return v.AllKeys() }
func (v *Viper) AllKeys() []string {
- m := map[string]struct{}{}
+ m := map[string]bool{}
+ // add all paths, by order of descending priority to ensure correct shadowing
+ m = v.flattenAndMergeMap(m, castMapStringToMapInterface(v.aliases), "")
+ m = v.flattenAndMergeMap(m, v.override, "")
+ m = v.mergeFlatMap(m, v.pflags)
+ m = v.mergeFlatMap(m, v.env)
+ m = v.flattenAndMergeMap(m, v.config, "")
+ m = v.flattenAndMergeMap(m, v.kvstore, "")
+ m = v.flattenAndMergeMap(m, v.defaults, "")
- for key := range v.defaults {
- m[strings.ToLower(key)] = struct{}{}
- }
-
- for key := range v.pflags {
- m[strings.ToLower(key)] = struct{}{}
- }
-
- for key := range v.env {
- m[strings.ToLower(key)] = struct{}{}
- }
-
- for key := range v.config {
- m[strings.ToLower(key)] = struct{}{}
- }
-
- for key := range v.kvstore {
- m[strings.ToLower(key)] = struct{}{}
- }
-
- for key := range v.override {
- m[strings.ToLower(key)] = struct{}{}
- }
-
- for key := range v.aliases {
- m[strings.ToLower(key)] = struct{}{}
- }
-
+ // convert set of paths to list
a := []string{}
for x := range m {
a = append(a, x)
}
-
return a
}
-// AllSettings returns all settings as a map[string]interface{}.
+// flattenAndMergeMap recursively flattens the given map into a map[string]bool
+// of key paths (used as a set, easier to manipulate than a []string):
+// - each path is merged into a single key string, delimited with v.keyDelim (= ".")
+// - if a path is shadowed by an earlier value in the initial shadow map,
+// it is skipped.
+// The resulting set of paths is merged to the given shadow set at the same time.
+func (v *Viper) flattenAndMergeMap(shadow map[string]bool, m map[string]interface{}, prefix string) map[string]bool {
+ if shadow != nil && prefix != "" && shadow[prefix] {
+ // prefix is shadowed => nothing more to flatten
+ return shadow
+ }
+ if shadow == nil {
+ shadow = make(map[string]bool)
+ }
+
+ var m2 map[string]interface{}
+ if prefix != "" {
+ prefix += v.keyDelim
+ }
+ for k, val := range m {
+ fullKey := prefix + k
+ switch val.(type) {
+ case map[string]interface{}:
+ m2 = val.(map[string]interface{})
+ case map[interface{}]interface{}:
+ m2 = cast.ToStringMap(val)
+ default:
+ // immediate value
+ shadow[strings.ToLower(fullKey)] = true
+ continue
+ }
+ // recursively merge to shadow map
+ shadow = v.flattenAndMergeMap(shadow, m2, fullKey)
+ }
+ return shadow
+}
+
+// mergeFlatMap merges the given maps, excluding values of the second map
+// shadowed by values from the first map.
+func (v *Viper) mergeFlatMap(shadow map[string]bool, mi interface{}) map[string]bool {
+ // unify input map
+ var m map[string]interface{}
+ switch mi.(type) {
+ case map[string]string, map[string]FlagValue:
+ m = cast.ToStringMap(mi)
+ default:
+ return shadow
+ }
+
+ // scan keys
+outer:
+ for k, _ := range m {
+ path := strings.Split(k, v.keyDelim)
+ // scan intermediate paths
+ var parentKey string
+ for i := 1; i < len(path); i++ {
+ parentKey = strings.Join(path[0:i], v.keyDelim)
+ if shadow[parentKey] {
+ // path is shadowed, continue
+ continue outer
+ }
+ }
+ // add key
+ shadow[strings.ToLower(k)] = true
+ }
+ return shadow
+}
+
+// AllSettings merges all settings and returns them as a map[string]interface{}.
func AllSettings() map[string]interface{} { return v.AllSettings() }
func (v *Viper) AllSettings() map[string]interface{} {
m := map[string]interface{}{}
- for _, x := range v.AllKeys() {
- m[x] = v.Get(x)
+ // start from the list of keys, and construct the map one value at a time
+ 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
+ continue
+ }
+ path := strings.Split(k, v.keyDelim)
+ lastKey := strings.ToLower(path[len(path)-1])
+ deepestMap := deepSearch(m, path[0:len(path)-1])
+ // set innermost value
+ deepestMap[lastKey] = value
}
-
return m
}
@@ -1289,7 +1484,6 @@
// purposes.
func Debug() { v.Debug() }
func (v *Viper) Debug() {
- fmt.Println("Aliases:")
fmt.Printf("Aliases:\n%#v\n", v.aliases)
fmt.Printf("Override:\n%#v\n", v.override)
fmt.Printf("PFlags:\n%#v\n", v.pflags)
diff --git a/viper_test.go b/viper_test.go
index 72f695e..02d6eb1 100644
--- a/viper_test.go
+++ b/viper_test.go
@@ -8,6 +8,7 @@
import (
"bytes"
"fmt"
+ "io"
"io/ioutil"
"os"
"path"
@@ -104,8 +105,9 @@
func initConfigs() {
Reset()
+ var r io.Reader
SetConfigType("yaml")
- r := bytes.NewReader(yamlExample)
+ r = bytes.NewReader(yamlExample)
unmarshalReader(r, v.config)
SetConfigType("json")
@@ -259,7 +261,7 @@
assert.False(t, InConfig("state"))
assert.Equal(t, "steve", Get("name"))
assert.Equal(t, []interface{}{"skateboarding", "snowboarding", "go"}, Get("hobbies"))
- assert.Equal(t, map[interface{}]interface{}{"jacket": "leather", "trousers": "denim", "pants": map[interface{}]interface{}{"size": "large"}}, Get("clothing"))
+ assert.Equal(t, map[string]interface{}{"jacket": "leather", "trousers": "denim", "pants": map[interface{}]interface{}{"size": "large"}}, Get("clothing"))
assert.Equal(t, 35, Get("age"))
}
@@ -420,9 +422,9 @@
func TestAllKeys(t *testing.T) {
initConfigs()
- ks := sort.StringSlice{"title", "newkey", "owner", "name", "beard", "ppu", "batters", "hobbies", "clothing", "age", "hacker", "id", "type", "eyes", "p_id", "p_ppu", "p_batters.batter.type", "p_type", "p_name", "foos"}
+ ks := sort.StringSlice{"title", "newkey", "owner.organization", "owner.dob", "owner.bio", "name", "beard", "ppu", "batters.batter", "hobbies", "clothing.jacket", "clothing.trousers", "clothing.pants.size", "age", "hacker", "id", "type", "eyes", "p_id", "p_ppu", "p_batters.batter.type", "p_type", "p_name", "foos"}
dob, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z")
- all := map[string]interface{}{"owner": map[string]interface{}{"organization": "MongoDB", "Bio": "MongoDB Chief Developer Advocate & Hacker at Large", "dob": dob}, "title": "TOML Example", "ppu": 0.55, "eyes": "brown", "clothing": map[interface{}]interface{}{"trousers": "denim", "jacket": "leather", "pants": map[interface{}]interface{}{"size": "large"}}, "id": "0001", "batters": map[string]interface{}{"batter": []interface{}{map[string]interface{}{"type": "Regular"}, map[string]interface{}{"type": "Chocolate"}, map[string]interface{}{"type": "Blueberry"}, map[string]interface{}{"type": "Devil's Food"}}}, "hacker": true, "beard": true, "hobbies": []interface{}{"skateboarding", "snowboarding", "go"}, "age": 35, "type": "donut", "newkey": "remote", "name": "Cake", "p_id": "0001", "p_ppu": "0.55", "p_name": "Cake", "p_batters.batter.type": "Regular", "p_type": "donut", "foos": []map[string]interface{}{map[string]interface{}{"foo": []map[string]interface{}{map[string]interface{}{"key": 1}, map[string]interface{}{"key": 2}, map[string]interface{}{"key": 3}, map[string]interface{}{"key": 4}}}}}
+ all := map[string]interface{}{"owner": map[string]interface{}{"organization": "MongoDB", "bio": "MongoDB Chief Developer Advocate & Hacker at Large", "dob": dob}, "title": "TOML Example", "ppu": 0.55, "eyes": "brown", "clothing": map[string]interface{}{"trousers": "denim", "jacket": "leather", "pants": map[string]interface{}{"size": "large"}}, "id": "0001", "batters": map[string]interface{}{"batter": []interface{}{map[string]interface{}{"type": "Regular"}, map[string]interface{}{"type": "Chocolate"}, map[string]interface{}{"type": "Blueberry"}, map[string]interface{}{"type": "Devil's Food"}}}, "hacker": true, "beard": true, "hobbies": []interface{}{"skateboarding", "snowboarding", "go"}, "age": 35, "type": "donut", "newkey": "remote", "name": "Cake", "p_id": "0001", "p_ppu": "0.55", "p_name": "Cake", "p_batters": map[string]interface{}{"batter": map[string]interface{}{"type": "Regular"}}, "p_type": "donut", "foos": []map[string]interface{}{map[string]interface{}{"foo": []map[string]interface{}{map[string]interface{}{"key": 1}, map[string]interface{}{"key": 2}, map[string]interface{}{"key": 3}, map[string]interface{}{"key": 4}}}}}
var allkeys sort.StringSlice
allkeys = AllKeys()
@@ -468,17 +470,18 @@
t.Fatalf("unable to decode into struct, %v", err)
}
- assert.Equal(t, &C, &config{Name: "Steve", Port: 1313, Duration: time.Second + time.Millisecond})
+ assert.Equal(t, &config{Name: "Steve", Port: 1313, Duration: time.Second + time.Millisecond}, &C)
Set("port", 1234)
err = Unmarshal(&C)
if err != nil {
t.Fatalf("unable to decode into struct, %v", err)
}
- assert.Equal(t, &C, &config{Name: "Steve", Port: 1234, Duration: time.Second + time.Millisecond})
+ assert.Equal(t, &config{Name: "Steve", Port: 1234, Duration: time.Second + time.Millisecond}, &C)
}
func TestBindPFlags(t *testing.T) {
+ v := New() // create independent Viper object
flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
var testValues = map[string]*string{
@@ -497,7 +500,7 @@
testValues[name] = flagSet.String(name, "", "test")
}
- err := BindPFlags(flagSet)
+ err := v.BindPFlags(flagSet)
if err != nil {
t.Fatalf("error binding flag set, %v", err)
}
@@ -508,7 +511,7 @@
})
for name, expected := range mutatedTestValues {
- assert.Equal(t, Get(name), expected)
+ assert.Equal(t, expected, v.Get(name))
}
}
@@ -641,7 +644,7 @@
"name": "Cake",
"hacker": true,
"ppu": 0.55,
- "clothing": map[interface{}]interface{}{
+ "clothing": map[string]interface{}{
"jacket": "leather",
"trousers": "denim",
"pants": map[interface{}]interface{}{
@@ -690,7 +693,7 @@
assert.False(t, v.InConfig("state"))
assert.Equal(t, "steve", v.Get("name"))
assert.Equal(t, []interface{}{"skateboarding", "snowboarding", "go"}, v.Get("hobbies"))
- assert.Equal(t, map[interface{}]interface{}{"jacket": "leather", "trousers": "denim", "pants": map[interface{}]interface{}{"size": "large"}}, v.Get("clothing"))
+ assert.Equal(t, map[string]interface{}{"jacket": "leather", "trousers": "denim", "pants": map[interface{}]interface{}{"size": "large"}}, v.Get("clothing"))
assert.Equal(t, 35, v.Get("age"))
}
@@ -759,10 +762,10 @@
assert.Equal(t, v.Get("clothing.pants.size"), subv.Get("size"))
subv = v.Sub("clothing.pants.size")
- assert.Equal(t, subv, (*Viper)(nil))
+ assert.Equal(t, (*Viper)(nil), subv)
subv = v.Sub("missing.key")
- assert.Equal(t, subv, (*Viper)(nil))
+ assert.Equal(t, (*Viper)(nil), subv)
}
var yamlMergeExampleTgt = []byte(`
@@ -883,28 +886,28 @@
}
func TestUnmarshalingWithAliases(t *testing.T) {
- SetDefault("Id", 1)
- Set("name", "Steve")
- Set("lastname", "Owen")
+ v := New()
+ v.SetDefault("ID", 1)
+ v.Set("name", "Steve")
+ v.Set("lastname", "Owen")
- RegisterAlias("UserID", "Id")
- RegisterAlias("Firstname", "name")
- RegisterAlias("Surname", "lastname")
+ v.RegisterAlias("UserID", "ID")
+ v.RegisterAlias("Firstname", "name")
+ v.RegisterAlias("Surname", "lastname")
type config struct {
- Id int
+ ID int
FirstName string
Surname string
}
var C config
-
- err := Unmarshal(&C)
+ err := v.Unmarshal(&C)
if err != nil {
t.Fatalf("unable to decode into struct, %v", err)
}
- assert.Equal(t, &C, &config{Id: 1, FirstName: "Steve", Surname: "Owen"})
+ assert.Equal(t, &config{ID: 1, FirstName: "Steve", Surname: "Owen"}, &C)
}
func TestSetConfigNameClearsFileCache(t *testing.T) {
@@ -917,9 +920,26 @@
polyester := "polyester"
initYAML()
SetDefault("clothing.shirt", polyester)
+ SetDefault("clothing.jacket.price", 100)
- assert.Equal(t, GetString("clothing.jacket"), "leather")
- assert.Equal(t, GetString("clothing.shirt"), polyester)
+ assert.Equal(t, "leather", GetString("clothing.jacket"))
+ assert.Nil(t, Get("clothing.jacket.price"))
+ assert.Equal(t, polyester, GetString("clothing.shirt"))
+
+ clothingSettings := AllSettings()["clothing"].(map[string]interface{})
+ assert.Equal(t, "leather", clothingSettings["jacket"])
+ assert.Equal(t, polyester, clothingSettings["shirt"])
+}
+
+func TestDotParameter(t *testing.T) {
+ initJSON()
+ // shoud take precedence over batters defined in jsonExample
+ r := bytes.NewReader([]byte(`{ "batters.batter": [ { "type": "Small" } ] }`))
+ unmarshalReader(r, v.config)
+
+ actual := Get("batters.batter")
+ expected := []interface{}{map[string]interface{}{"type": "Small"}}
+ assert.Equal(t, expected, actual)
}
func TestGetBool(t *testing.T) {