From a1b9194c7ab4b06584530ecde4084befcf56263b Mon Sep 17 00:00:00 2001 From: Kiril Zvezdarov Date: Sun, 26 Apr 2015 15:02:19 -0400 Subject: [PATCH 1/4] Added accessing deep keys by recursive hierarchical search --- viper.go | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/viper.go b/viper.go index e099839..a148209 100644 --- a/viper.go +++ b/viper.go @@ -107,6 +107,10 @@ func (rce RemoteConfigError) Error() string { // "endpoint": "https://localhost" // } type Viper struct { + // Delimiter that separates a list of keys + // used to access a nested value in one go + keyDelim string + // A set of paths to look for the config file in configPaths []string @@ -134,6 +138,7 @@ type Viper struct { // Returns an initialized Viper instance. func New() *Viper { v := new(Viper) + v.keyDelim = "." v.configName = "config" v.config = make(map[string]interface{}) v.override = make(map[string]interface{}) @@ -307,6 +312,26 @@ func (v *Viper) providerPathExists(p *remoteProvider) bool { return false } +func (v *Viper) searchMap(source map[string]interface{}, path []string) interface{} { + + if len(path) == 0 { + return source + } + + if next, ok := source[path[0]]; ok { + switch next.(type) { + 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 + return v.searchMap(next.(map[string]interface{}), path[1:]) + default: + return next + } + } else { + return nil + } +} + // Viper is essentially repository for configurations // Get can retrieve any value given the key to use // Get has the behavior of returning the value associated with the first @@ -316,11 +341,19 @@ func (v *Viper) providerPathExists(p *remoteProvider) bool { // Get returns an interface. For a specific value use one of the Get____ methods. func Get(key string) interface{} { return v.Get(key) } func (v *Viper) Get(key string) interface{} { - key = strings.ToLower(key) - val := v.find(key) + path := strings.Split(key, v.keyDelim) + + val := v.find(strings.ToLower(key)) if val == nil { - return nil + source := v.find(path[0]) + if source == nil { + return nil + } + + if reflect.TypeOf(source).Kind() == reflect.Map { + val = v.searchMap(cast.ToStringMap(source), path[1:]) + } } switch val.(type) { From 73f97d107c0b377c64c9cbb541740d8b4c22f4af Mon Sep 17 00:00:00 2001 From: Kiril Zvezdarov Date: Fri, 1 May 2015 15:14:41 -0400 Subject: [PATCH 2/4] Reordered the debug dump of configs to print them in order of precedence --- viper.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/viper.go b/viper.go index a148209..73bc6f3 100644 --- a/viper.go +++ b/viper.go @@ -914,18 +914,18 @@ func (v *Viper) findConfigFile() (string, error) { // purposes. func Debug() { v.Debug() } func (v *Viper) Debug() { - fmt.Println("Config:") - pretty.Println(v.config) - fmt.Println("Key/Value Store:") - pretty.Println(v.kvstore) - fmt.Println("Env:") - pretty.Println(v.env) - fmt.Println("Defaults:") - pretty.Println(v.defaults) - fmt.Println("Override:") - pretty.Println(v.override) fmt.Println("Aliases:") pretty.Println(v.aliases) + fmt.Println("Override:") + pretty.Println(v.override) fmt.Println("PFlags") pretty.Println(v.pflags) + fmt.Println("Env:") + pretty.Println(v.env) + fmt.Println("Key/Value Store:") + pretty.Println(v.kvstore) + fmt.Println("Config:") + pretty.Println(v.config) + fmt.Println("Defaults:") + pretty.Println(v.defaults) } From c0c19ab6517fc97764e2595736472c396e2cb7cb Mon Sep 17 00:00:00 2001 From: Kiril Zvezdarov Date: Fri, 1 May 2015 15:15:24 -0400 Subject: [PATCH 3/4] Added test coverage for searching for deeply nested values --- viper_test.go | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/viper_test.go b/viper_test.go index 334773d..76ec0d2 100644 --- a/viper_test.go +++ b/viper_test.go @@ -453,3 +453,87 @@ func TestSizeInBytes(t *testing.T) { assert.Equal(t, expected, parseSizeInBytes(str), str) } } + +func TestFindsNestedKeys(t *testing.T) { + initConfigs() + dob, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z") + + Set("super", map[string]interface{}{ + "deep": map[string]interface{}{ + "nested": "value", + }, + }) + + expected := map[string]interface{}{ + "super": map[string]interface{}{ + "deep": map[string]interface{}{ + "nested": "value", + }, + }, + "super.deep": map[string]interface{}{ + "nested": "value", + }, + "super.deep.nested": "value", + "owner.organization": "MongoDB", + "batters.batter": []interface{}{ + map[string]interface{}{ + "type": "Regular", + }, + map[string]interface{}{ + "type": "Chocolate", + }, + map[string]interface{}{ + "type": "Blueberry", + }, + map[string]interface{}{ + "type": "Devil's Food", + }, + }, + "hobbies": []interface{}{ + "skateboarding", "snowboarding", "go", + }, + "title": "TOML Example", + "newkey": "remote", + "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", + }, + }, + }, + "eyes": "brown", + "age": 35, + "owner": map[string]interface{}{ + "organization": "MongoDB", + "Bio": "MongoDB Chief Developer Advocate & Hacker at Large", + "dob": dob, + }, + "owner.Bio": "MongoDB Chief Developer Advocate & Hacker at Large", + "type": "donut", + "id": "0001", + "name": "Cake", + "hacker": true, + "ppu": 0.55, + "clothing": map[interface{}]interface{}{ + "jacket": "leather", + "trousers": "denim", + }, + "clothing.jacket": "leather", + "clothing.trousers": "denim", + "owner.dob": dob, + "beard": true, + } + + for key, expectedValue := range expected { + + assert.Equal(t, expectedValue, v.Get(key)) + } + +} From 1fbfce02f5a157be98aeb7bb1eaddf589d153577 Mon Sep 17 00:00:00 2001 From: Kiril Zvezdarov Date: Fri, 1 May 2015 16:18:03 -0400 Subject: [PATCH 4/4] Recursively insensitivize the configuration structures --- util.go | 5 +++++ viper_test.go | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/util.go b/util.go index 7c5d21a..6fef1a4 100644 --- a/util.go +++ b/util.go @@ -17,6 +17,7 @@ import ( "io" "os" "path/filepath" + "reflect" "runtime" "strings" "unicode" @@ -35,6 +36,10 @@ func insensitiviseMap(m map[string]interface{}) { delete(m, key) m[lower] = val } + + if val != nil && reflect.TypeOf(val).Kind() == reflect.Map { + insensitiviseMap(cast.ToStringMap(val)) + } } } diff --git a/viper_test.go b/viper_test.go index 76ec0d2..1039039 100644 --- a/viper_test.go +++ b/viper_test.go @@ -304,7 +304,7 @@ func TestAllKeys(t *testing.T) { 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"} 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"}, "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"} + 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"}, "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"} var allkeys sort.StringSlice allkeys = AllKeys() @@ -512,10 +512,10 @@ func TestFindsNestedKeys(t *testing.T) { "age": 35, "owner": map[string]interface{}{ "organization": "MongoDB", - "Bio": "MongoDB Chief Developer Advocate & Hacker at Large", + "bio": "MongoDB Chief Developer Advocate & Hacker at Large", "dob": dob, }, - "owner.Bio": "MongoDB Chief Developer Advocate & Hacker at Large", + "owner.bio": "MongoDB Chief Developer Advocate & Hacker at Large", "type": "donut", "id": "0001", "name": "Cake",