Materialized path key traversal.

Allows deep access of config settings via a dot-delimited materialized path.
Environment variables may also be set to corresponding deep keys using double
underscores in place of the usual period delimiter. E.g:

  clothing.jacket => CLOTHING__JACKET
This commit is contained in:
jacobstr 2014-10-22 02:27:34 -06:00
parent 83fd92627c
commit df68c0ac38
2 changed files with 85 additions and 18 deletions

View file

@ -25,6 +25,7 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"reflect"
"runtime" "runtime"
"strings" "strings"
"time" "time"
@ -50,6 +51,7 @@ var configFile string
var configType string var configType string
var config map[string]interface{} = make(map[string]interface{}) var config map[string]interface{} = make(map[string]interface{})
var config_index map[string]interface{} = make(map[string]interface{})
var override map[string]interface{} = make(map[string]interface{}) var override map[string]interface{} = make(map[string]interface{})
var env map[string]string = make(map[string]string) var env map[string]string = make(map[string]string)
var defaults map[string]interface{} = make(map[string]interface{}) var defaults map[string]interface{} = make(map[string]interface{})
@ -133,7 +135,7 @@ func Marshal(rawVal interface{}) error {
return err return err
} }
insensativiseMaps() indexMaps()
return nil return nil
} }
@ -172,7 +174,7 @@ func BindEnv(input ...string) (err error) {
key = input[0] key = input[0]
if len(input) == 1 { if len(input) == 1 {
envkey = strings.ToUpper(key) envkey = strings.Replace(strings.ToUpper(key), ".", "__", -1)
} else { } else {
envkey = input[1] envkey = input[1]
} }
@ -204,6 +206,8 @@ func find(key string) interface{} {
return val return val
} }
// Periods are not supported. Allow the usage of double underscores to specify
// nested configuration options.
envkey, exists := env[key] envkey, exists := env[key]
if exists { if exists {
jww.TRACE.Println(key, "registered as env var", envkey) jww.TRACE.Println(key, "registered as env var", envkey)
@ -215,7 +219,7 @@ func find(key string) interface{} {
} }
} }
val, exists = config[key] val, exists = config_index[key]
if exists { if exists {
jww.TRACE.Println(key, "found in config:", val) jww.TRACE.Println(key, "found in config:", val)
return val return val
@ -265,7 +269,7 @@ func IsSet(key string) bool {
// Have viper check ENV variables for all // Have viper check ENV variables for all
// keys set in config, default & flags // keys set in config, default & flags
func AutomaticEnv() { func AutomaticEnv() {
for _, x := range AllKeys() { for _, x := range AllDeepKeys() {
BindEnv(x) BindEnv(x)
} }
} }
@ -286,8 +290,8 @@ func registerAlias(alias string, key string) {
// name, we'll never be able to get that value using the original // name, we'll never be able to get that value using the original
// name, so move the config value to the new realkey. // name, so move the config value to the new realkey.
if val, ok := config[alias]; ok { if val, ok := config[alias]; ok {
delete(config, alias) delete(config_index, alias)
config[key] = val config_index[key] = val
} }
if val, ok := defaults[alias]; ok { if val, ok := defaults[alias]; ok {
delete(defaults, alias) delete(defaults, alias)
@ -327,7 +331,7 @@ func InConfig(key string) bool {
func SetDefault(key string, value interface{}) { func SetDefault(key string, value interface{}) {
// If alias passed in, then set the proper default // If alias passed in, then set the proper default
key = realKey(strings.ToLower(key)) key = realKey(strings.ToLower(key))
defaults[key] = value defaults[strings.ToLower(key)] = value
} }
// The user provided value (via flag) // The user provided value (via flag)
@ -382,25 +386,70 @@ func MarshallReader(in io.Reader) {
} }
} }
insensativiseMap(config) indexMap(config, "", config_index)
} }
func insensativiseMaps() { func indexMaps() {
insensativiseMap(config) indexMap(config, "", config_index)
insensativiseMap(defaults)
insensativiseMap(override)
} }
func insensativiseMap(m map[string]interface{}) { // Creates a flat index consisting of materialized paths into the potentially
// nested config structure we've loaded from various sources. The index allows us
// to access the original map contents in a case insensitive manner without
// having to modify the original keys - which may lead to unexpected results
// depending on how a user intends to use the config variable (e.g. passing the
// entire map to function that may not be case-insensitive) or accessing nested
// keys via materialized paths.
func indexMap(m map[string]interface{}, path string, index map[string]interface{}) {
for key, val := range m { for key, val := range m {
lower := strings.ToLower(key) lower := strings.ToLower(key)
if key != lower { var joined_key string
delete(m, key) if len(path) > 0 {
m[lower] = val joined_key = path + "." + lower
} else {
joined_key = lower
}
index[joined_key] = val
if reflect.TypeOf(val).Kind() == reflect.Map {
// YAML maps may be map[interface{}]interface{}
indexMap(cast.ToStringMap(val), joined_key, index)
} }
} }
} }
// AllDeepKeys returns all keys, including deep materialized paths.
func AllDeepKeys() []string {
all := map[string]struct{}{}
var traverse func(m map[string]interface{}, path string)
traverse = func(m map[string]interface{}, path string) {
for key, val := range m {
lower := strings.ToLower(key)
var joined_key string
if len(path) > 0 {
joined_key = path + "." + lower
} else {
joined_key = lower
}
all[joined_key] = struct{}{}
if reflect.TypeOf(val).Kind() == reflect.Map {
traverse(cast.ToStringMap(val), joined_key)
}
}
}
traverse(defaults, "")
traverse(config, "")
traverse(override, "")
a := []string{}
for x, _ := range all {
a = append(a, x)
}
return a
}
func AllKeys() []string { func AllKeys() []string {
m := map[string]struct{}{} m := map[string]struct{}{}
@ -418,7 +467,8 @@ func AllKeys() []string {
a := []string{} a := []string{}
for x, _ := range m { for x, _ := range m {
a = append(a, x) // LowerCase the key for backwards-compatibility.
a = append(a, strings.ToLower(x))
} }
return a return a
@ -614,6 +664,7 @@ func Reset() {
configType = "" configType = ""
config = make(map[string]interface{}) config = make(map[string]interface{})
config_index = make(map[string]interface{})
override = make(map[string]interface{}) override = make(map[string]interface{})
env = make(map[string]string) env = make(map[string]string)
defaults = make(map[string]interface{}) defaults = make(map[string]interface{})

View file

@ -149,7 +149,7 @@ func TestEnv(t *testing.T) {
func TestAllKeys(t *testing.T) { func TestAllKeys(t *testing.T) {
ks := sort.StringSlice{"title", "owner", "name", "beard", "ppu", "batters", "hobbies", "clothing", "age", "hacker", "id", "type"} ks := sort.StringSlice{"title", "owner", "name", "beard", "ppu", "batters", "hobbies", "clothing", "age", "hacker", "id", "type"}
dob, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z") dob, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z")
all := map[string]interface{}{"hacker": true, "beard": true, "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"}}}, "hobbies": []interface{}{"skateboarding", "snowboarding", "go"}, "ppu": 0.55, "clothing": map[interface{}]interface{}{"jacket": "leather", "trousers": "denim"}, "name": "crunk", "owner": map[string]interface{}{"organization": "MongoDB", "Bio": "MongoDB Chief Developer Advocate & Hacker at Large", "dob": dob}, "id": "13", "title": "TOML Example", "age": 35, "type": "donut"} all := map[string]interface{}{"beard": true, "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, "hobbies": []interface{}{"skateboarding", "snowboarding", "go"}, "id": "13", "type": "donut", "owner": map[string]interface{}{"organization": "MongoDB", "Bio": "MongoDB Chief Developer Advocate & Hacker at Large", "dob": dob}, "ppu": 0.55, "age": 35, "title": "TOML Example", "clothing": map[interface{}]interface{}{"jacket": "leather", "trousers": "denim"}, "name": "crunk"}
var allkeys sort.StringSlice var allkeys sort.StringSlice
allkeys = AllKeys() allkeys = AllKeys()
@ -202,3 +202,19 @@ func TestMarshal(t *testing.T) {
} }
assert.Equal(t, &C, &config{Name: "Steve", Port: 1234}) assert.Equal(t, &C, &config{Name: "Steve", Port: 1234})
} }
func TestDeepAccess(t *testing.T) {
assert.Equal(t, "leather", Get("clothing.jacket"))
}
func TestDeepBindEnv(t *testing.T) {
BindEnv("clothing.jacket")
os.Setenv("CLOTHING__JACKET", "peacoat")
assert.Equal(t, "peacoat", Get("clothing.jacket"))
}
func TestDeepAutomaticEnv(t *testing.T) {
AutomaticEnv()
os.Setenv("CLOTHING__JACKET", "jean")
assert.Equal(t, "jean", Get("clothing.jacket"))
}