diff --git a/README.md b/README.md index 885b6d8..2e695b3 100644 --- a/README.md +++ b/README.md @@ -237,6 +237,12 @@ Example: fmt.Println("verbose enabled") } +You can get deeply nested values by providing the expected key path. + +Example: + + viper.Get("log.verbose") // case-insensitive path .....etc + ### Marshaling You also have the option of Marshaling all or a specific value to a struct, map, etc. diff --git a/util.go b/util.go index 7c5d21a..09bd5a5 100644 --- a/util.go +++ b/util.go @@ -17,6 +17,7 @@ import ( "io" "os" "path/filepath" + "reflect" "runtime" "strings" "unicode" @@ -196,3 +197,23 @@ func parseSizeInBytes(sizeStr string) uint { return safeMul(uint(size), multiplier) } + +// Recursively walks through the source map, populating the index with keys representing +// the nesting of the value and the value itself. +func indexMap(source map[string]interface{}, prefix string, index map[string]interface{}) { + if len(prefix) > 0 { + prefix = prefix + INDEX_DELIM + } + + for key, val := range source { + + indexPath := strings.ToLower(prefix + key) + + v.index[indexPath] = val + + if reflect.TypeOf(val).Kind() == reflect.Map { + indexMap(cast.ToStringMap(val), indexPath, index) + } + } + +} diff --git a/viper.go b/viper.go index e099839..d2c5cf5 100644 --- a/viper.go +++ b/viper.go @@ -38,6 +38,9 @@ import ( crypt "github.com/xordataexchange/crypt/config" ) +const INDEX_DELIM = "." +const ENV_INDEX_DELIM = "__" + var v *Viper func init() { @@ -121,7 +124,10 @@ type Viper struct { automaticEnvApplied bool envKeyReplacer *strings.Replacer + envToIndex *strings.Replacer + indexToEnv *strings.Replacer + index map[string]interface{} config map[string]interface{} override map[string]interface{} defaults map[string]interface{} @@ -135,6 +141,7 @@ type Viper struct { func New() *Viper { v := new(Viper) v.configName = "config" + v.index = make(map[string]interface{}) v.config = make(map[string]interface{}) v.override = make(map[string]interface{}) v.defaults = make(map[string]interface{}) @@ -149,6 +156,8 @@ func New() *Viper { } else { v.AddConfigPath(wd) } + v.indexToEnv = strings.NewReplacer(ENV_INDEX_DELIM, INDEX_DELIM) + v.envToIndex = strings.NewReplacer(INDEX_DELIM, ENV_INDEX_DELIM) return v } @@ -214,6 +223,8 @@ func (v *Viper) mergeWithEnvPrefix(in string) string { // key. This allows env vars which have different keys then the config object // keys func (v *Viper) getEnv(key string) string { + key = v.indexToEnv.Replace(key) + if v.envKeyReplacer != nil { key = v.envKeyReplacer.Replace(key) } @@ -317,12 +328,23 @@ func (v *Viper) providerPathExists(p *remoteProvider) bool { func Get(key string) interface{} { return v.Get(key) } func (v *Viper) Get(key string) interface{} { key = strings.ToLower(key) - val := v.find(key) + var val interface{} + + if val = v.find(key); val == nil { + v.buildIndex() + val = v.findIndex(key) + } if val == nil { return nil } + val = castVal(val) + + return val +} + +func castVal(val interface{}) interface{} { switch val.(type) { case bool: return cast.ToBool(val) @@ -339,6 +361,7 @@ func (v *Viper) Get(key string) interface{} { case []string: return val } + return val } @@ -483,7 +506,7 @@ func (v *Viper) BindEnv(input ...string) (err error) { return fmt.Errorf("BindEnv missing key to bind to") } - key = strings.ToLower(input[0]) + key = v.envToIndex.Replace(strings.ToLower(input[0])) if len(input) == 1 { envkey = v.mergeWithEnvPrefix(key) @@ -496,6 +519,18 @@ func (v *Viper) BindEnv(input ...string) (err error) { return nil } +func (v *Viper) findIndex(key string) interface{} { + // Check index - it is safe ot check it first + // as the paths there should be materialized + // according to their priority + val, exists := v.index[key] + if exists { + jww.TRACE.Println(key, "found in index:", val) + return val + } + return nil +} + // Given a key, find the value // Viper will check in the following order: // flag, env, config file, key/value store, default @@ -507,6 +542,12 @@ func (v *Viper) find(key string) interface{} { // if the requested key is an alias, then return the proper key key = v.realKey(key) + val, exists = v.override[key] + if exists { + jww.TRACE.Println(key, "found in override:", val) + return val + } + // PFlag Override first flag, exists := v.pflags[key] if exists { @@ -516,12 +557,6 @@ func (v *Viper) find(key string) interface{} { } } - val, exists = v.override[key] - if exists { - jww.TRACE.Println(key, "found in override:", val) - return val - } - if v.automaticEnvApplied { // even if it hasn't been registered, if automaticEnv is used, // check any Get request @@ -563,6 +598,15 @@ func (v *Viper) find(key string) interface{} { return nil } +// Recursively walks through the structure returned by +// AllSettings, indexing deeply nested values. +// It uses AllSettings in order to get a properly +// prioritized config structure. +func (v *Viper) buildIndex() { + v.index = make(map[string]interface{}) + indexMap(v.AllSettings(), "", v.index) +} + // Check to see if the key has been set in any of the data locations func IsSet(key string) bool { return v.IsSet(key) } func (v *Viper) IsSet(key string) bool { @@ -784,12 +828,26 @@ func (v *Viper) AllKeys() []string { return a } +// Return all keys found in the index, including the deeply +// nested keys. +func AllIndexes() []string { return v.AllIndexes() } +func (v *Viper) AllIndexes() []string { + v.buildIndex() + + var indexes []string + for key, _ := range v.index { + indexes = append(indexes, key) + } + + return indexes +} + // Return all settings as a map[string]interface{} func AllSettings() map[string]interface{} { return v.AllSettings() } func (v *Viper) AllSettings() map[string]interface{} { m := map[string]interface{}{} for _, x := range v.AllKeys() { - m[x] = v.Get(x) + m[x] = castVal(v.find(x)) } return m @@ -881,6 +939,8 @@ func (v *Viper) findConfigFile() (string, error) { // purposes. func Debug() { v.Debug() } func (v *Viper) Debug() { + fmt.Println("Index:") + pretty.Print(v.index) fmt.Println("Config:") pretty.Println(v.config) fmt.Println("Key/Value Store:") diff --git a/viper_test.go b/viper_test.go index 334773d..ef62df3 100644 --- a/viper_test.go +++ b/viper_test.go @@ -315,6 +315,184 @@ func TestAllKeys(t *testing.T) { assert.Equal(t, all, AllSettings()) } +func TestAllIndexes(t *testing.T) { + initConfigs() + expected := sort.StringSlice{"owner.organization", "name", "ppu", "clothing.trousers", "type", "batters.batter", "clothing.jacket", "hacker", "beard", "newkey", "owner.bio", "batters", "clothing", "owner", "eyes", "hobbies", "owner.dob", "title", "age", "id"} + expected.Sort() + + var actual sort.StringSlice + actual = AllIndexes() + actual.Sort() + + assert.Equal(t, expected, actual) +} + +func TestBuildsIndex(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, + } + + v.buildIndex() + + assert.Equal(t, expected, v.index) +} + +func TestRetrievesIndex(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, + } + + v.buildIndex() + + for key, value := range expected { + assert.Equal(t, value, v.Get(key)) + } +} + func TestCaseInSensitive(t *testing.T) { assert.Equal(t, true, Get("hacker")) Set("Title", "Checking Case") @@ -359,6 +537,7 @@ func TestMarshal(t *testing.T) { } func TestBindPFlags(t *testing.T) { + Reset() flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) var testValues = map[string]*string{ @@ -415,6 +594,7 @@ func TestBindPFlag(t *testing.T) { } func TestBoundCaseSensitivity(t *testing.T) { + initConfigs() assert.Equal(t, "brown", Get("eyes"))