mirror of
https://github.com/spf13/viper
synced 2024-11-16 18:07:02 +00:00
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:
parent
83fd92627c
commit
df68c0ac38
2 changed files with 85 additions and 18 deletions
85
viper.go
85
viper.go
|
@ -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{})
|
||||||
|
|
|
@ -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"))
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue