Repository: spf13/viper
Pull request #861 (2020-03-05 20:09:10)

Added support for accessing slices
Hi,
I added support for accessing values within a slice as mentioned in #196. For example, if we have a config file which is structured like this:
```json
{
  "server_name": "example.com",
  "interfaces": [
    {
        "interface": "192.168.1.1",
        "port": 80
    },
    {
        "interface": "127.0.0.1",
        "port": 8080
    }
  ]
}
```

We can now get the second interface config like so `viper.Get("interfaces.1")` or specifically the port like so `viper.GetInt("interfaces.1.port")`

I have chosen to implement the index numbers like path parts since it is way more difficult to try to parse the go like syntax `viper.Get("interfaces[1]")` and still respect the other functionality like shadowing checks.

CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/spf13/viper?pullRequest=861) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/spf13/viper?pullRequest=861) before we can accept your contribution.<br/><hr/>**Dylan Reimerink** seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please [add the email address used for this commit to your account](https://help.github.com/articles/why-are-my-commits-linked-to-the-wrong-user/#commits-are-not-linked-to-any-user).<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/spf13/viper?pullRequest=861) it.</sub>
snail007: bro, what's next plan about this feature ?

dylandreimerink: Well, this is one of 102 open pull requests at the moment, it most likely is not on anyone's radar. I think @sagikazarmark should take a look at it and give some feedback or close the pull request. Until then I can't really do much as far as I know.
dylandreimerink: You are correct, missed that after the refactor. Should be all good now.
snail007: hi bro , do you consider for sets value for slices, i hava a toml config file bellow, in go code i want to set 
through such as `Set("cache.memory[0].enable",true)`, i think this feature is a basic requirement.

```ini
[cache]
default="redis"

[[cache.redis]]
debug=true
enable=true
id="default"
address="127.0.0.1:6379"
prefix=""
password=""
# seconds
timeout=10
dbnum=0
maxidle=10
maxactive=30
# seconds
idletimeout=300
# seconds
maxconnlifetime=3600
wait=true

[[cache.memory]]
enable=false
id="default"
# seconds
cleanupinterval=30
```

sagikazarmark: @snail007 you should be able to do

```go
Set("cache.memory", []interface{}{
    map[string]interface{}{
        "enabled": true,
    },
})
```

Patch:
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,28 +603,15 @@ 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
 		}
 	}
 
@@ -631,6 +619,76 @@ func (v *Viper) searchMapWithPathPrefixes(source map[string]interface{}, path []
 	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
+	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”
@@ -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
 	}


Test patch:
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()