From 33bcdc91eace7e804b9dad23614a1b24cbd7f1d0 Mon Sep 17 00:00:00 2001 From: dylandreimerink <97.dylan@gmail.com> Date: Sun, 4 Oct 2020 20:07:34 +0200 Subject: [PATCH] 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 --- README.md | 27 +++++++++++++ viper.go | 110 ++++++++++++++++++++++++++++++++++++++------------ viper_test.go | 43 ++++++++++++++++++++ 3 files changed, 154 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 9dff1d9..270a481 100644 --- a/README.md +++ b/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 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 will be returned instead. E.g. diff --git a/viper.go b/viper.go index abd9a38..67eaa69 100644 --- a/viper.go +++ b/viper.go @@ -30,6 +30,7 @@ import ( "os" "path/filepath" "reflect" + "strconv" "strings" "sync" "time" @@ -582,9 +583,9 @@ func (v *Viper) searchMap(source map[string]interface{}, path []string) interfac 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. // 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"]. @@ -593,7 +594,7 @@ func (v *Viper) searchMap(source map[string]interface{}, path []string) interfac // in their keys). // // 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 { return source } @@ -602,29 +603,86 @@ func (v *Viper) searchMapWithPathPrefixes(source map[string]interface{}, path [] for i := len(path); i > 0; i-- { 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{} - 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: - // got a value but nested key expected, do nothing and look for next prefix - } - if val != nil { - return val - } + var val interface{} + switch sourceIndexable := source.(type) { + case []interface{}: + val = v.searchSliceWithPathPrefixes(sourceIndexable, prefixKey, i, path) + case map[string]interface{}: + val = v.searchMapWithPathPrefixes(sourceIndexable, prefixKey, i, path) } + if val != nil { + 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 @@ -1134,7 +1192,7 @@ func (v *Viper) find(lcaseKey string, flagDefault bool) interface{} { } // Config file next - val = v.searchMapWithPathPrefixes(v.config, path) + val = v.searchIndexableWithPathPrefixes(v.config, path) if val != nil { return val } diff --git a/viper_test.go b/viper_test.go index cfcff5a..45bf8e9 100644 --- a/viper_test.go +++ b/viper_test.go @@ -2279,6 +2279,49 @@ func TestKeyDelimiter(t *testing.T) { 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) { key := "BenchmarkGetBool" v = New()