Added support for accessing slices (#861)

* Added support for accessing slices

* Processed PR feedback
- renamed searchMapWithPathPrefixes to searchIndexableWithPathPrefixes
- moved source type specific search logic to speparate functions
- Inverted if statments to avoid the arrow pattern

* Quickly return from searchSliceWithPathPrefixes and searchMapWithPathPrefixes functions without intermediate variables
This commit is contained in:
dylandreimerink 2020-10-04 20:07:34 +02:00 committed by GitHub
parent 44e6ee8945
commit 33bcdc91ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 154 additions and 26 deletions

View File

@ -589,6 +589,33 @@ the `Set()` method, …) with an immediate value, then all sub-keys of
`datastore.metric` become undefined, they are “shadowed” by the higher-priority `datastore.metric` become undefined, they are “shadowed” by the higher-priority
configuration level. configuration level.
Viper can access array indices by using numbers in the path. For example:
```json
{
"host": {
"address": "localhost",
"ports": [
5799,
6029
]
},
"datastore": {
"metric": {
"host": "127.0.0.1",
"port": 3099
},
"warehouse": {
"host": "198.0.0.1",
"port": 2112
}
}
}
GetInt("host.ports.1") // returns 6029
```
Lastly, if there exists a key that matches the delimited key path, its value Lastly, if there exists a key that matches the delimited key path, its value
will be returned instead. E.g. will be returned instead. E.g.

View File

@ -30,6 +30,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -582,9 +583,9 @@ func (v *Viper) searchMap(source map[string]interface{}, path []string) interfac
return nil return nil
} }
// searchMapWithPathPrefixes recursively searches for a value for path in source map. // searchIndexableWithPathPrefixes recursively searches for a value for path in source map/slice.
// //
// While searchMap() considers each path element as a single map key, this // While searchMap() considers each path element as a single map key or slice index, this
// function searches for, and prioritizes, merged path elements. // 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" // 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"]. // is also defined, this latter value is returned for path ["foo", "bar"].
@ -593,7 +594,7 @@ func (v *Viper) searchMap(source map[string]interface{}, path []string) interfac
// in their keys). // in their keys).
// //
// Note: This assumes that the path entries and map keys are lower cased. // Note: This assumes that the path entries and map keys are lower cased.
func (v *Viper) searchMapWithPathPrefixes(source map[string]interface{}, path []string) interface{} { func (v *Viper) searchIndexableWithPathPrefixes(source interface{}, path []string) interface{} {
if len(path) == 0 { if len(path) == 0 {
return source return source
} }
@ -602,29 +603,86 @@ func (v *Viper) searchMapWithPathPrefixes(source map[string]interface{}, path []
for i := len(path); i > 0; i-- { for i := len(path); i > 0; i-- {
prefixKey := strings.ToLower(strings.Join(path[0:i], v.keyDelim)) prefixKey := strings.ToLower(strings.Join(path[0:i], v.keyDelim))
next, ok := source[prefixKey]
if ok {
// Fast path
if i == len(path) {
return next
}
// Nested case
var val interface{} var val interface{}
switch next.(type) { switch sourceIndexable := source.(type) {
case map[interface{}]interface{}: case []interface{}:
val = v.searchMapWithPathPrefixes(cast.ToStringMap(next), path[i:]) val = v.searchSliceWithPathPrefixes(sourceIndexable, prefixKey, i, path)
case map[string]interface{}: case map[string]interface{}:
// Type assertion is safe here since it is only reached val = v.searchMapWithPathPrefixes(sourceIndexable, prefixKey, i, path)
// if the type of `next` is the same as the type being asserted
val = v.searchMapWithPathPrefixes(next.(map[string]interface{}), path[i:])
default:
// got a value but nested key expected, do nothing and look for next prefix
} }
if val != nil { if val != nil {
return val return val
} }
} }
// not found
return nil
}
// searchSliceWithPathPrefixes searches for a value for path in sourceSlice
//
// This function is part of the searchIndexableWithPathPrefixes recurring search and
// should not be called directly from functions other than searchIndexableWithPathPrefixes.
func (v *Viper) searchSliceWithPathPrefixes(
sourceSlice []interface{},
prefixKey string,
pathIndex int,
path []string,
) interface{} {
// if the prefixKey is not a number or it is out of bounds of the slice
index, err := strconv.Atoi(prefixKey)
if err != nil || len(sourceSlice) <= index {
return nil
}
next := sourceSlice[index]
// Fast path
if pathIndex == len(path) {
return next
}
switch n := next.(type) {
case map[interface{}]interface{}:
return v.searchIndexableWithPathPrefixes(cast.ToStringMap(n), path[pathIndex:])
case map[string]interface{}, []interface{}:
return v.searchIndexableWithPathPrefixes(n, path[pathIndex:])
default:
// got a value but nested key expected, do nothing and look for next prefix
}
// not found
return nil
}
// searchMapWithPathPrefixes searches for a value for path in sourceMap
//
// This function is part of the searchIndexableWithPathPrefixes recurring search and
// should not be called directly from functions other than searchIndexableWithPathPrefixes.
func (v *Viper) searchMapWithPathPrefixes(
sourceMap map[string]interface{},
prefixKey string,
pathIndex int,
path []string,
) interface{} {
next, ok := sourceMap[prefixKey]
if !ok {
return nil
}
// Fast path
if pathIndex == len(path) {
return next
}
// Nested case
switch n := next.(type) {
case map[interface{}]interface{}:
return v.searchIndexableWithPathPrefixes(cast.ToStringMap(n), path[pathIndex:])
case map[string]interface{}, []interface{}:
return v.searchIndexableWithPathPrefixes(n, path[pathIndex:])
default:
// got a value but nested key expected, do nothing and look for next prefix
} }
// not found // not found
@ -1134,7 +1192,7 @@ func (v *Viper) find(lcaseKey string, flagDefault bool) interface{} {
} }
// Config file next // Config file next
val = v.searchMapWithPathPrefixes(v.config, path) val = v.searchIndexableWithPathPrefixes(v.config, path)
if val != nil { if val != nil {
return val return val
} }

View File

@ -2279,6 +2279,49 @@ func TestKeyDelimiter(t *testing.T) {
assert.Equal(t, expected, actual) assert.Equal(t, expected, actual)
} }
var yamlDeepNestedSlices = []byte(`TV:
- title: "The expanse"
seasons:
- first_released: "December 14, 2015"
episodes:
- title: "Dulcinea"
air_date: "December 14, 2015"
- title: "The Big Empty"
air_date: "December 15, 2015"
- title: "Remember the Cant"
air_date: "December 22, 2015"
- first_released: "February 1, 2017"
episodes:
- title: "Safe"
air_date: "February 1, 2017"
- title: "Doors & Corners"
air_date: "February 1, 2017"
- title: "Static"
air_date: "February 8, 2017"
episodes:
- ["Dulcinea", "The Big Empty", "Remember the Cant"]
- ["Safe", "Doors & Corners", "Static"]
`)
func TestSliceIndexAccess(t *testing.T) {
v.SetConfigType("yaml")
r := strings.NewReader(string(yamlDeepNestedSlices))
err := v.unmarshalReader(r, v.config)
require.NoError(t, err)
assert.Equal(t, "The expanse", v.GetString("tv.0.title"))
assert.Equal(t, "February 1, 2017", v.GetString("tv.0.seasons.1.first_released"))
assert.Equal(t, "Static", v.GetString("tv.0.seasons.1.episodes.2.title"))
assert.Equal(t, "December 15, 2015", v.GetString("tv.0.seasons.0.episodes.1.air_date"))
// Test for index out of bounds
assert.Equal(t, "", v.GetString("tv.0.seasons.2.first_released"))
// Accessing multidimensional arrays
assert.Equal(t, "Static", v.GetString("tv.0.episodes.1.2"))
}
func BenchmarkGetBool(b *testing.B) { func BenchmarkGetBool(b *testing.B) {
key := "BenchmarkGetBool" key := "BenchmarkGetBool"
v = New() v = New()