mirror of
https://github.com/spf13/viper
synced 2024-12-22 19:47:01 +00:00
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:
parent
44e6ee8945
commit
33bcdc91ea
3 changed files with 154 additions and 26 deletions
27
README.md
27
README.md
|
@ -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.
|
||||||
|
|
||||||
|
|
98
viper.go
98
viper.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue