diff --git a/.gitignore b/.gitignore index 8365624..352a34a 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ _testmain.go *.exe *.test +*.bench \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 9e6d621..e793edb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,27 @@ +go_import_path: github.com/spf13/viper + language: go go: - - 1.3 - - release + - 1.5.4 + - 1.6.3 + - 1.7 - tip +os: + - linux + - osx + +matrix: + allow_failures: + - go: tip + fast_finish: true + script: + - go install ./... - go test -v ./... + +after_success: + - go get -u -d github.com/spf13/hugo + - cd $GOPATH/src/github.com/spf13/hugo && make && ./hugo -s docs && cd - + sudo: false diff --git a/README.md b/README.md index 8fdcd37..25181df 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,17 @@ Go configuration with fangs! - [![Build Status](https://travis-ci.org/spf13/viper.svg)](https://travis-ci.org/spf13/viper) [![Join the chat at https://gitter.im/spf13/viper](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/spf13/viper?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +Many Go projects are built using Viper including: + +* [Hugo](http://gohugo.io) +* [EMC RexRay](http://rexray.readthedocs.org/en/stable/) +* [Imgur's Incus](https://github.com/Imgur/incus) +* [Nanobox](https://github.com/nanobox-io/nanobox)/[Nanopack](https://github.com/nanopack) +* [Docker Notary](https://github.com/docker/Notary) +* [BloomApi](https://www.bloomapi.com/) +* [doctl](https://github.com/digitalocean/doctl) + +[![Build Status](https://travis-ci.org/spf13/viper.svg)](https://travis-ci.org/spf13/viper) [![Join the chat at https://gitter.im/spf13/viper](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/spf13/viper?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![GoDoc](https://godoc.org/github.com/spf13/viper?status.svg)](https://godoc.org/github.com/spf13/viper) ## What is Viper? @@ -12,7 +22,7 @@ to work within an application, and can handle all types of configuration needs and formats. It supports: * setting defaults -* reading from JSON, TOML, YAML and HCL config files +* reading from JSON, TOML, YAML, HCL, and Java properties config files * live watching and re-reading of config files (optional) * reading from environment variables * reading from remote config systems (etcd or Consul), and watching changes @@ -31,7 +41,7 @@ Viper is here to help with that. Viper does the following for you: -1. Find, load, and unmarshal a configuration file in JSON, TOML, YAML or HCL. +1. Find, load, and unmarshal a configuration file in JSON, TOML, YAML, HCL, or Java properties formats. 2. Provide a mechanism to set default values for your different configuration options. 3. Provide a mechanism to set override values for options specified through @@ -72,7 +82,7 @@ viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "cat ### Reading Config Files Viper requires minimal configuration so it knows where to look for config files. -Viper supports JSON, TOML, YAML and HCL files. Viper can search multiple paths, but +Viper supports JSON, TOML, YAML, HCL, and Java Properties files. Viper can search multiple paths, but currently a single Viper instance only supports a single configuration file. Viper does not default to any configuration search paths leaving defaults decision to an application. @@ -100,7 +110,7 @@ Gone are the days of needing to restart a server to have a config take effect, viper powered applications can read an update to a config file while running and not miss a beat. -Simply tell the viper instance to watchConfig. +Simply tell the viper instance to watchConfig. Optionally you can provide a function for Viper to run each time a change occurs. **Make sure you add all of the configPaths prior to calling `WatchConfig()`** @@ -267,10 +277,10 @@ Viper provides two Go interfaces to bind other flag systems if you don't use `Pf ```go type myFlag struct {} -func (f myFlag) IsChanged() { return false } -func (f myFlag) Name() { return "my-flag-name" } -func (f myFlag) ValueString() { return "my-flag-value" } -func (f myFlag) ValueType() { return "string" } +func (f myFlag) HasChanged() bool { return false } +func (f myFlag) Name() string { return "my-flag-name" } +func (f myFlag) ValueString() string { return "my-flag-value" } +func (f myFlag) ValueType() string { return "string" } ``` Once your flag implements this interface, you can simply tell Viper to bind it: @@ -288,7 +298,7 @@ type myFlagSet struct { func (f myFlagSet) VisitAll(fn func(FlagValue)) { for _, flag := range flags { - fn(flag) + fn(flag) } } ``` @@ -343,7 +353,7 @@ how to use Consul. ```go viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json") -viper.SetConfigType("json") // because there is no file extension in a stream of bytes +viper.SetConfigType("json") // because there is no file extension in a stream of bytes, supported extensions are "json", "toml", "yaml", "yml", "properties", "props", "prop" err := viper.ReadRemoteConfig() ``` @@ -351,7 +361,7 @@ err := viper.ReadRemoteConfig() ```go viper.AddSecureRemoteProvider("etcd","http://127.0.0.1:4001","/config/hugo.json","/etc/secrets/mykeyring.gpg") -viper.SetConfigType("json") // because there is no file extension in a stream of bytes +viper.SetConfigType("json") // because there is no file extension in a stream of bytes, supported extensions are "json", "toml", "yaml", "yml", "properties", "props", "prop" err := viper.ReadRemoteConfig() ``` @@ -362,7 +372,7 @@ err := viper.ReadRemoteConfig() var runtime_viper = viper.New() runtime_viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001", "/config/hugo.yml") -runtime_viper.SetConfigType("yaml") // because there is no file extension in a stream of bytes +runtime_viper.SetConfigType("yaml") // because there is no file extension in a stream of bytes, supported extensions are "json", "toml", "yaml", "yml", "properties", "props", "prop" // read from remote config the first time. err := runtime_viper.ReadRemoteConfig() @@ -448,16 +458,17 @@ Viper can access a nested field by passing a `.` delimited path of keys: GetString("datastore.metric.host") // (returns "127.0.0.1") ``` -This obeys the precedence rules established above; the search for the root key -(in this example, `datastore`) will cascade through the remaining configuration -registries until found. The search for the sub-keys (`metric` and `host`), -however, will not. +This obeys the precedence rules established above; the search for the path +will cascade through the remaining configuration registries until found. -For example, if the `metric` key was not defined in the configuration loaded -from file, but was defined in the defaults, Viper would return the zero value. +For example, given this configuration file, both `datastore.metric.host` and +`datastore.metric.port` are already defined (and may be overridden). If in addition +`datastore.metric.protocol` was defined in the defaults, Viper would also find it. -On the other hand, if the primary key was not defined, Viper would go through -the remaining registries looking for it. +However, if `datastore.metric` was overridden (by a flag, an environment variable, +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. Lastly, if there exists a key that matches the delimited key path, its value will be returned instead. E.g. @@ -481,7 +492,7 @@ will be returned instead. E.g. } } -GetString("datastore.metric.host") //returns "0.0.0.0" +GetString("datastore.metric.host") // returns "0.0.0.0" ``` ### Extract sub-tree diff --git a/flags_test.go b/flags_test.go index 5489278..5bffca3 100644 --- a/flags_test.go +++ b/flags_test.go @@ -22,7 +22,7 @@ func TestBindFlagValueSet(t *testing.T) { "endpoint": "/public", } - for name, _ := range testValues { + for name := range testValues { testValues[name] = flagSet.String(name, "", "test") } diff --git a/nohup.out b/nohup.out new file mode 100644 index 0000000..8973bf2 --- /dev/null +++ b/nohup.out @@ -0,0 +1 @@ +QProcess::start: Process is already running diff --git a/overrides_test.go b/overrides_test.go new file mode 100644 index 0000000..dd2aa9b --- /dev/null +++ b/overrides_test.go @@ -0,0 +1,173 @@ +package viper + +import ( + "fmt" + "strings" + "testing" + + "github.com/spf13/cast" + "github.com/stretchr/testify/assert" +) + +type layer int + +const ( + defaultLayer layer = iota + 1 + overrideLayer +) + +func TestNestedOverrides(t *testing.T) { + assert := assert.New(t) + var v *Viper + + // Case 0: value overridden by a value + overrideDefault(assert, "tom", 10, "tom", 20) // "tom" is first given 10 as default value, then overridden by 20 + override(assert, "tom", 10, "tom", 20) // "tom" is first given value 10, then overridden by 20 + overrideDefault(assert, "tom.age", 10, "tom.age", 20) + override(assert, "tom.age", 10, "tom.age", 20) + overrideDefault(assert, "sawyer.tom.age", 10, "sawyer.tom.age", 20) + override(assert, "sawyer.tom.age", 10, "sawyer.tom.age", 20) + + // Case 1: key:value overridden by a value + v = overrideDefault(assert, "tom.age", 10, "tom", "boy") // "tom.age" is first given 10 as default value, then "tom" is overridden by "boy" + assert.Nil(v.Get("tom.age")) // "tom.age" should not exist anymore + v = override(assert, "tom.age", 10, "tom", "boy") + assert.Nil(v.Get("tom.age")) + + // Case 2: value overridden by a key:value + overrideDefault(assert, "tom", "boy", "tom.age", 10) // "tom" is first given "boy" as default value, then "tom" is overridden by map{"age":10} + override(assert, "tom.age", 10, "tom", "boy") + + // Case 3: key:value overridden by a key:value + v = overrideDefault(assert, "tom.size", 4, "tom.age", 10) + assert.Equal(4, v.Get("tom.size")) // value should still be reachable + v = override(assert, "tom.size", 4, "tom.age", 10) + assert.Equal(4, v.Get("tom.size")) + deepCheckValue(assert, v, overrideLayer, []string{"tom", "size"}, 4) + + // Case 4: key:value overridden by a map + v = overrideDefault(assert, "tom.size", 4, "tom", map[string]interface{}{"age": 10}) // "tom.size" is first given "4" as default value, then "tom" is overridden by map{"age":10} + assert.Equal(4, v.Get("tom.size")) // "tom.size" should still be reachable + assert.Equal(10, v.Get("tom.age")) // new value should be there + deepCheckValue(assert, v, overrideLayer, []string{"tom", "age"}, 10) // new value should be there + v = override(assert, "tom.size", 4, "tom", map[string]interface{}{"age": 10}) + assert.Nil(v.Get("tom.size")) + assert.Equal(10, v.Get("tom.age")) + deepCheckValue(assert, v, overrideLayer, []string{"tom", "age"}, 10) + + // Case 5: array overridden by a value + overrideDefault(assert, "tom", []int{10, 20}, "tom", 30) + override(assert, "tom", []int{10, 20}, "tom", 30) + overrideDefault(assert, "tom.age", []int{10, 20}, "tom.age", 30) + override(assert, "tom.age", []int{10, 20}, "tom.age", 30) + + // Case 6: array overridden by an array + overrideDefault(assert, "tom", []int{10, 20}, "tom", []int{30, 40}) + override(assert, "tom", []int{10, 20}, "tom", []int{30, 40}) + overrideDefault(assert, "tom.age", []int{10, 20}, "tom.age", []int{30, 40}) + v = override(assert, "tom.age", []int{10, 20}, "tom.age", []int{30, 40}) + // explicit array merge: + s, ok := v.Get("tom.age").([]int) + if assert.True(ok, "tom[\"age\"] is not a slice") { + v.Set("tom.age", append(s, []int{50, 60}...)) + assert.Equal([]int{30, 40, 50, 60}, v.Get("tom.age")) + deepCheckValue(assert, v, overrideLayer, []string{"tom", "age"}, []int{30, 40, 50, 60}) + } +} + +func overrideDefault(assert *assert.Assertions, firstPath string, firstValue interface{}, secondPath string, secondValue interface{}) *Viper { + return overrideFromLayer(defaultLayer, assert, firstPath, firstValue, secondPath, secondValue) +} +func override(assert *assert.Assertions, firstPath string, firstValue interface{}, secondPath string, secondValue interface{}) *Viper { + return overrideFromLayer(overrideLayer, assert, firstPath, firstValue, secondPath, secondValue) +} + +// overrideFromLayer performs the sequential override and low-level checks. +// +// First assignment is made on layer l for path firstPath with value firstValue, +// the second one on the override layer (i.e., with the Set() function) +// for path secondPath with value secondValue. +// +// firstPath and secondPath can include an arbitrary number of dots to indicate +// a nested element. +// +// After each assignment, the value is checked, retrieved both by its full path +// and by its key sequence (successive maps). +func overrideFromLayer(l layer, assert *assert.Assertions, firstPath string, firstValue interface{}, secondPath string, secondValue interface{}) *Viper { + v := New() + firstKeys := strings.Split(firstPath, v.keyDelim) + if assert == nil || + len(firstKeys) == 0 || len(firstKeys[0]) == 0 { + return v + } + + // Set and check first value + switch l { + case defaultLayer: + v.SetDefault(firstPath, firstValue) + case overrideLayer: + v.Set(firstPath, firstValue) + default: + return v + } + assert.Equal(firstValue, v.Get(firstPath)) + deepCheckValue(assert, v, l, firstKeys, firstValue) + + // Override and check new value + secondKeys := strings.Split(secondPath, v.keyDelim) + if len(secondKeys) == 0 || len(secondKeys[0]) == 0 { + return v + } + v.Set(secondPath, secondValue) + assert.Equal(secondValue, v.Get(secondPath)) + deepCheckValue(assert, v, overrideLayer, secondKeys, secondValue) + + return v +} + +// deepCheckValue checks that all given keys correspond to a valid path in the +// configuration map of the given layer, and that the final value equals the one given +func deepCheckValue(assert *assert.Assertions, v *Viper, l layer, keys []string, value interface{}) { + if assert == nil || v == nil || + len(keys) == 0 || len(keys[0]) == 0 { + return + } + + // init + var val interface{} + var ms string + switch l { + case defaultLayer: + val = v.defaults + ms = "v.defaults" + case overrideLayer: + val = v.override + ms = "v.override" + } + + // loop through map + var m map[string]interface{} + err := false + for _, k := range keys { + if val == nil { + assert.Fail(fmt.Sprintf("%s is not a map[string]interface{}", ms)) + return + } + + // deep scan of the map to get the final value + switch val.(type) { + case map[interface{}]interface{}: + m = cast.ToStringMap(val) + case map[string]interface{}: + m = val.(map[string]interface{}) + default: + assert.Fail(fmt.Sprintf("%s is not a map[string]interface{}", ms)) + return + } + ms = ms + "[\"" + k + "\"]" + val = m[k] + } + if !err { + assert.Equal(value, val) + } +} diff --git a/util.go b/util.go index 0cc4553..b0903fb 100644 --- a/util.go +++ b/util.go @@ -21,20 +21,20 @@ import ( "strings" "unicode" - "github.com/BurntSushi/toml" "github.com/hashicorp/hcl" "github.com/magiconair/properties" + toml "github.com/pelletier/go-toml" "github.com/spf13/cast" jww "github.com/spf13/jwalterweatherman" "gopkg.in/yaml.v2" ) -// Denotes failing to parse configuration file. +// ConfigParseError denotes failing to parse configuration file. type ConfigParseError struct { err error } -// Returns the formatted configuration error. +// Error returns the formatted configuration error. func (pe ConfigParseError) Error() string { return fmt.Sprintf("While parsing config: %s", pe.err.Error()) } @@ -45,6 +45,10 @@ func insensitiviseMap(m map[string]interface{}) { if key != lower { delete(m, key) m[lower] = val + if m2, ok := val.(map[string]interface{}); ok { + // nested map: recursively insensitivise + insensitiviseMap(m2) + } } } } @@ -68,16 +72,16 @@ func absPathify(inPath string) string { p, err := filepath.Abs(inPath) if err == nil { return filepath.Clean(p) - } else { - jww.ERROR.Println("Couldn't discover absolute path") - jww.ERROR.Println(err) } + + jww.ERROR.Println("Couldn't discover absolute path") + jww.ERROR.Println(err) return "" } // Check if File / Directory Exists func exists(path string) (bool, error) { - _, err := os.Stat(path) + _, err := v.fs.Stat(path) if err == nil { return true, nil } @@ -107,29 +111,6 @@ func userHomeDir() string { return os.Getenv("HOME") } -func findCWD() (string, error) { - serverFile, err := filepath.Abs(os.Args[0]) - - if err != nil { - return "", fmt.Errorf("Can't get absolute path for executable: %v", err) - } - - path := filepath.Dir(serverFile) - realFile, err := filepath.EvalSymlinks(serverFile) - - if err != nil { - if _, err = os.Stat(serverFile + ".exe"); err == nil { - realFile = filepath.Clean(serverFile + ".exe") - } - } - - if err == nil && realFile != serverFile { - path = filepath.Dir(realFile) - } - - return path, nil -} - func unmarshallConfigReader(in io.Reader, c map[string]interface{}, configType string) error { buf := new(bytes.Buffer) buf.ReadFrom(in) @@ -155,9 +136,14 @@ func unmarshallConfigReader(in io.Reader, c map[string]interface{}, configType s } case "toml": - if _, err := toml.Decode(buf.String(), &c); err != nil { + tree, err := toml.LoadReader(buf) + if err != nil { return ConfigParseError{err} } + tmap := tree.ToMap() + for k, v := range tmap { + c[k] = v + } case "properties", "props", "prop": var p *properties.Properties @@ -167,7 +153,12 @@ func unmarshallConfigReader(in io.Reader, c map[string]interface{}, configType s } for _, key := range p.Keys() { value, _ := p.Get(key) - c[key] = value + // recursively build nested maps + path := strings.Split(key, ".") + lastKey := strings.ToLower(path[len(path)-1]) + deepestMap := deepSearch(c, path[0:len(path)-1]) + // set innermost value + deepestMap[lastKey] = value } } @@ -217,3 +208,34 @@ func parseSizeInBytes(sizeStr string) uint { return safeMul(uint(size), multiplier) } + +// deepSearch scans deep maps, following the key indexes listed in the +// sequence "path". +// The last value is expected to be another map, and is returned. +// +// In case intermediate keys do not exist, or map to a non-map value, +// a new map is created and inserted, and the search continues from there: +// the initial map "m" may be modified! +func deepSearch(m map[string]interface{}, path []string) map[string]interface{} { + for _, k := range path { + m2, ok := m[k] + if !ok { + // intermediate key does not exist + // => create it and continue from there + m3 := make(map[string]interface{}) + m[k] = m3 + m = m3 + continue + } + m3, ok := m2.(map[string]interface{}) + if !ok { + // intermediate key is a value + // => replace with a new map + m3 = make(map[string]interface{}) + m[k] = m3 + } + // continue search from here + m = m3 + } + return m +} diff --git a/viper.go b/viper.go index cdf6e9e..152e125 100644 --- a/viper.go +++ b/viper.go @@ -23,7 +23,6 @@ import ( "bytes" "fmt" "io" - "io/ioutil" "log" "os" "path/filepath" @@ -31,12 +30,12 @@ import ( "strings" "time" - "github.com/kr/pretty" + "github.com/fsnotify/fsnotify" "github.com/mitchellh/mapstructure" + "github.com/spf13/afero" "github.com/spf13/cast" jww "github.com/spf13/jwalterweatherman" "github.com/spf13/pflag" - "gopkg.in/fsnotify.v1" ) var v *Viper @@ -53,40 +52,40 @@ type remoteConfigFactory interface { // RemoteConfig is optional, see the remote package var RemoteConfig remoteConfigFactory -// Denotes encountering an unsupported +// UnsupportedConfigError denotes encountering an unsupported // configuration filetype. type UnsupportedConfigError string -// Returns the formatted configuration error. +// Error returns the formatted configuration error. func (str UnsupportedConfigError) Error() string { return fmt.Sprintf("Unsupported Config Type %q", string(str)) } -// Denotes encountering an unsupported remote +// UnsupportedRemoteProviderError denotes encountering an unsupported remote // provider. Currently only etcd and Consul are // supported. type UnsupportedRemoteProviderError string -// Returns the formatted remote provider error. +// Error returns the formatted remote provider error. func (str UnsupportedRemoteProviderError) Error() string { return fmt.Sprintf("Unsupported Remote Provider Type %q", string(str)) } -// Denotes encountering an error while trying to +// RemoteConfigError denotes encountering an error while trying to // pull the configuration from the remote provider. type RemoteConfigError string -// Returns the formatted remote provider error +// Error returns the formatted remote provider error func (rce RemoteConfigError) Error() string { return fmt.Sprintf("Remote Configurations Error: %s", string(rce)) } -// Denotes failing to find configuration file. +// ConfigFileNotFoundError denotes failing to find configuration file. type ConfigFileNotFoundError struct { name, locations string } -// Returns the formatted configuration error. +// Error returns the formatted configuration error. func (fnfe ConfigFileNotFoundError) Error() string { return fmt.Sprintf("Config File %q Not Found in %q", fnfe.name, fnfe.locations) } @@ -108,11 +107,11 @@ func (fnfe ConfigFileNotFoundError) Error() string { // Defaults : { // "secret": "", // "user": "default", -// "endpoint": "https://localhost" +// "endpoint": "https://localhost" // } // Config : { // "user": "root" -// "secret": "defaultsecret" +// "secret": "defaultsecret" // } // Env : { // "secret": "somesecretkey" @@ -133,6 +132,9 @@ type Viper struct { // A set of paths to look for the config file in configPaths []string + // The filesystem to read config from. + fs afero.Fs + // A set of remote providers to search for the configuration remoteProviders []*defaultRemoteProvider @@ -157,11 +159,12 @@ type Viper struct { onConfigChange func(fsnotify.Event) } -// Returns an initialized Viper instance. +// New returns an initialized Viper instance. func New() *Viper { v := new(Viper) v.keyDelim = "." v.configName = "config" + v.fs = afero.NewOsFs() v.config = make(map[string]interface{}) v.override = make(map[string]interface{}) v.defaults = make(map[string]interface{}) @@ -217,11 +220,11 @@ type RemoteProvider interface { SecretKeyring() string } -// Universally supported extensions. -var SupportedExts []string = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl"} +// SupportedExts are universally supported extensions. +var SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl"} -// Universally supported remote providers. -var SupportedRemoteProviders []string = []string{"etcd", "consul"} +// SupportedRemoteProviders are universally supported remote providers. +var SupportedRemoteProviders = []string{"etcd", "consul"} func OnConfigChange(run func(in fsnotify.Event)) { v.OnConfigChange(run) } func (v *Viper) OnConfigChange(run func(in fsnotify.Event)) { @@ -273,7 +276,7 @@ func (v *Viper) WatchConfig() { }() } -// Explicitly define the path, name and extension of the config file +// SetConfigFile explicitly defines the path, name and extension of the config file // Viper will use this and not check any of the config paths func SetConfigFile(in string) { v.SetConfigFile(in) } func (v *Viper) SetConfigFile(in string) { @@ -282,7 +285,7 @@ func (v *Viper) SetConfigFile(in string) { } } -// Define a prefix that ENVIRONMENT variables will use. +// SetEnvPrefix defines a prefix that ENVIRONMENT variables will use. // E.g. if your prefix is "spf", the env registry // will look for env. variables that start with "SPF_" func SetEnvPrefix(in string) { v.SetEnvPrefix(in) } @@ -304,7 +307,7 @@ func (v *Viper) mergeWithEnvPrefix(in string) string { // rewriting keys many things, Ex: Get('someKey') -> some_key // (cammel case to snake case for JSON keys perhaps) -// getEnv s a wrapper around os.Getenv which replaces characters in the original +// getEnv is a wrapper around os.Getenv which replaces characters in the original // key. This allows env vars which have different keys then the config object // keys func (v *Viper) getEnv(key string) string { @@ -314,11 +317,11 @@ func (v *Viper) getEnv(key string) string { return os.Getenv(key) } -// Return the file used to populate the config registry +// ConfigFileUsed returns the file used to populate the config registry func ConfigFileUsed() string { return v.ConfigFileUsed() } func (v *Viper) ConfigFileUsed() string { return v.configFile } -// Add a path for Viper to search for the config file in. +// AddConfigPath adds a path for Viper to search for the config file in. // Can be called multiple times to define multiple search paths. func AddConfigPath(in string) { v.AddConfigPath(in) } func (v *Viper) AddConfigPath(in string) { @@ -402,16 +405,42 @@ func (v *Viper) providerPathExists(p *defaultRemoteProvider) bool { return false } -func (v *Viper) searchMap(source map[string]interface{}, path []string) interface{} { +// searchMapForKey may end up traversing the map if the key references a nested +// item (foo.bar), but will use a fast path for the common case. +// Note: This assumes that the key given is already lowercase. +func (v *Viper) searchMapForKey(source map[string]interface{}, lcaseKey string) interface{} { + if !strings.Contains(lcaseKey, v.keyDelim) { + v, ok := source[lcaseKey] + if ok { + return v + } + return nil + } + path := strings.Split(lcaseKey, v.keyDelim) + return v.searchMap(source, path) +} + +// searchMap recursively searches for a value for path in source map. +// Returns nil if not found. +// Note: This assumes that the path entries are lower cased. +func (v *Viper) searchMap(source map[string]interface{}, path []string) interface{} { if len(path) == 0 { return source } + // Fast path + if len(path) == 1 { + if v, ok := source[path[0]]; ok { + return v + } + return nil + } + var ok bool var next interface{} for k, v := range source { - if strings.ToLower(k) == strings.ToLower(path[0]) { + if k == path[0] { ok = true next = v break @@ -427,11 +456,133 @@ func (v *Viper) searchMap(source map[string]interface{}, path []string) interfac // 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 + if len(path) == 1 { + return next + } + // got a value but nested key expected, return "nil" for not found + return nil } - } else { - return nil } + return nil +} + +// searchMapWithPathPrefixes recursively searches for a value for path in source map. +// +// While searchMap() considers each path element as a single map key, 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"]. +// +// This should be useful only at config level (other maps may not contain dots +// in their keys). +func (v *Viper) searchMapWithPathPrefixes(source map[string]interface{}, path []string) interface{} { + if len(path) == 0 { + return source + } + + // search for path prefixes, starting from the longest one + for i := len(path); i > 0; i-- { + prefixKey := strings.ToLower(strings.Join(path[0:i], v.keyDelim)) + + var ok bool + var next interface{} + for k, v := range source { + if strings.ToLower(k) == prefixKey { + ok = true + next = v + break + } + } + + if ok { + 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: + if len(path) == i { + val = next + } + // got a value but nested key expected, do nothing and look for next prefix + } + if val != nil { + return val + } + } + } + + // 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” +// "foo.bar.baz" in a lower-priority map +func (v *Viper) isPathShadowedInDeepMap(path []string, m map[string]interface{}) string { + var parentVal interface{} + for i := 1; i < len(path); i++ { + parentVal = v.searchMap(m, path[0:i]) + if parentVal == nil { + // not found, no need to add more path elements + return "" + } + switch parentVal.(type) { + case map[interface{}]interface{}: + continue + case map[string]interface{}: + continue + default: + // parentVal is a regular value which shadows "path" + return strings.Join(path[0:i], v.keyDelim) + } + } + return "" +} + +// isPathShadowedInFlatMap makes sure the given path is not shadowed somewhere +// in a sub-path of the map. +// e.g., if "foo.bar" has a value in the given map, it “shadows” +// "foo.bar.baz" in a lower-priority map +func (v *Viper) isPathShadowedInFlatMap(path []string, mi interface{}) string { + // unify input map + var m map[string]interface{} + switch mi.(type) { + case map[string]string, map[string]FlagValue: + m = cast.ToStringMap(mi) + default: + return "" + } + + // scan paths + var parentKey string + for i := 1; i < len(path); i++ { + parentKey = strings.Join(path[0:i], v.keyDelim) + if _, ok := m[parentKey]; ok { + return parentKey + } + } + return "" +} + +// isPathShadowedInAutoEnv makes sure the given path is not shadowed somewhere +// in the environment, when automatic env is on. +// e.g., if "foo.bar" has a value in the environment, it “shadows” +// "foo.bar.baz" in a lower-priority map +func (v *Viper) isPathShadowedInAutoEnv(path []string) string { + var parentKey string + var val string + for i := 1; i < len(path); i++ { + parentKey = strings.Join(path[0:i], v.keyDelim) + if val = v.getEnv(v.mergeWithEnvPrefix(parentKey)); val != "" { + return parentKey + } + } + return "" } // SetTypeByDefaultValue enables or disables the inference of a key value's @@ -453,8 +604,12 @@ func (v *Viper) SetTypeByDefaultValue(enable bool) { v.typeByDefValue = enable } -// Viper is essentially repository for configurations -// Get can retrieve any value given the key to use +// GetViper gets the global Viper instance. +func GetViper() *Viper { + return v +} + +// Get can retrieve any value given the key to use. // Get has the behavior of returning the value associated with the first // place from where it is set. Viper will check in the following order: // override, flag, env, config file, key/value store, default @@ -462,49 +617,18 @@ func (v *Viper) SetTypeByDefaultValue(enable 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{} { - path := strings.Split(key, v.keyDelim) - lcaseKey := strings.ToLower(key) val := v.find(lcaseKey) - - if val == nil { - source := v.find(strings.ToLower(path[0])) - if source != nil { - if reflect.TypeOf(source).Kind() == reflect.Map { - val = v.searchMap(cast.ToStringMap(source), path[1:]) - } - } - } - - // if no other value is returned and a flag does exist for the value, - // get the flag's value even if the flag's value has not changed - if val == nil { - if flag, exists := v.pflags[lcaseKey]; exists { - jww.TRACE.Println(key, "get pflag default", val) - switch flag.ValueType() { - case "int", "int8", "int16", "int32", "int64": - val = cast.ToInt(flag.ValueString()) - case "bool": - val = cast.ToBool(flag.ValueString()) - default: - val = flag.ValueString() - } - } - } - if val == nil { return nil } - var valType interface{} - if !v.typeByDefValue { - valType = val - } else { - defVal, defExists := v.defaults[lcaseKey] - if defExists { + valType := val + if v.typeByDefValue { + // TODO(bep) this branch isn't covered by a single test. + defVal := v.searchMapForKey(v.defaults, lcaseKey) + if defVal != nil { valType = defVal - } else { - valType = val } } @@ -527,80 +651,89 @@ func (v *Viper) Get(key string) interface{} { return val } -// Returns new Viper instance representing a sub tree of this instance +// Sub returns new Viper instance representing a sub tree of this instance. func Sub(key string) *Viper { return v.Sub(key) } func (v *Viper) Sub(key string) *Viper { subv := New() data := v.Get(key) + if data == nil { + return nil + } + if reflect.TypeOf(data).Kind() == reflect.Map { subv.config = cast.ToStringMap(data) return subv - } else { - return nil } + return nil } -// Returns the value associated with the key as a string +// GetString returns the value associated with the key as a string. func GetString(key string) string { return v.GetString(key) } func (v *Viper) GetString(key string) string { return cast.ToString(v.Get(key)) } -// Returns the value associated with the key asa boolean +// GetBool returns the value associated with the key as a boolean. func GetBool(key string) bool { return v.GetBool(key) } func (v *Viper) GetBool(key string) bool { return cast.ToBool(v.Get(key)) } -// Returns the value associated with the key as an integer +// GetInt returns the value associated with the key as an integer. func GetInt(key string) int { return v.GetInt(key) } func (v *Viper) GetInt(key string) int { return cast.ToInt(v.Get(key)) } -// Returns the value associated with the key as a float64 +// GetInt64 returns the value associated with the key as an integer. +func GetInt64(key string) int64 { return v.GetInt64(key) } +func (v *Viper) GetInt64(key string) int64 { + return cast.ToInt64(v.Get(key)) +} + +// GetFloat64 returns the value associated with the key as a float64. func GetFloat64(key string) float64 { return v.GetFloat64(key) } func (v *Viper) GetFloat64(key string) float64 { return cast.ToFloat64(v.Get(key)) } -// Returns the value associated with the key as time +// GetTime returns the value associated with the key as time. func GetTime(key string) time.Time { return v.GetTime(key) } func (v *Viper) GetTime(key string) time.Time { return cast.ToTime(v.Get(key)) } -// Returns the value associated with the key as a duration +// GetDuration returns the value associated with the key as a duration. func GetDuration(key string) time.Duration { return v.GetDuration(key) } func (v *Viper) GetDuration(key string) time.Duration { return cast.ToDuration(v.Get(key)) } -// Returns the value associated with the key as a slice of strings +// GetStringSlice returns the value associated with the key as a slice of strings. func GetStringSlice(key string) []string { return v.GetStringSlice(key) } func (v *Viper) GetStringSlice(key string) []string { return cast.ToStringSlice(v.Get(key)) } -// Returns the value associated with the key as a map of interfaces +// GetStringMap returns the value associated with the key as a map of interfaces. func GetStringMap(key string) map[string]interface{} { return v.GetStringMap(key) } func (v *Viper) GetStringMap(key string) map[string]interface{} { return cast.ToStringMap(v.Get(key)) } -// Returns the value associated with the key as a map of strings +// GetStringMapString returns the value associated with the key as a map of strings. func GetStringMapString(key string) map[string]string { return v.GetStringMapString(key) } func (v *Viper) GetStringMapString(key string) map[string]string { return cast.ToStringMapString(v.Get(key)) } -// Returns the value associated with the key as a map to a slice of strings. +// GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings. func GetStringMapStringSlice(key string) map[string][]string { return v.GetStringMapStringSlice(key) } func (v *Viper) GetStringMapStringSlice(key string) map[string][]string { return cast.ToStringMapStringSlice(v.Get(key)) } -// Returns the size of the value associated with the given key +// GetSizeInBytes returns the size of the value associated with the given key // in bytes. func GetSizeInBytes(key string) uint { return v.GetSizeInBytes(key) } func (v *Viper) GetSizeInBytes(key string) uint { @@ -608,17 +741,17 @@ func (v *Viper) GetSizeInBytes(key string) uint { return parseSizeInBytes(sizeStr) } -// Takes a single key and unmarshals it into a Struct +// UnmarshalKey takes a single key and unmarshals it into a Struct. func UnmarshalKey(key string, rawVal interface{}) error { return v.UnmarshalKey(key, rawVal) } func (v *Viper) UnmarshalKey(key string, rawVal interface{}) error { return mapstructure.Decode(v.Get(key), rawVal) } -// Unmarshals the config into a Struct. Make sure that the tags +// Unmarshal unmarshals the config into a Struct. Make sure that the tags // on the fields of the structure are properly set. func Unmarshal(rawVal interface{}) error { return v.Unmarshal(rawVal) } func (v *Viper) Unmarshal(rawVal interface{}) error { - err := mapstructure.WeakDecode(v.AllSettings(), rawVal) + err := decode(v.AllSettings(), defaultDecoderConfig(rawVal)) if err != nil { return err @@ -629,27 +762,64 @@ func (v *Viper) Unmarshal(rawVal interface{}) error { return nil } -// Bind a full flag set to the configuration, using each flag's long +// defaultDecoderConfig returns default mapsstructure.DecoderConfig with suppot +// of time.Duration values +func defaultDecoderConfig(output interface{}) *mapstructure.DecoderConfig { + return &mapstructure.DecoderConfig{ + Metadata: nil, + Result: output, + WeaklyTypedInput: true, + DecodeHook: mapstructure.StringToTimeDurationHookFunc(), + } +} + +// A wrapper around mapstructure.Decode that mimics the WeakDecode functionality +func decode(input interface{}, config *mapstructure.DecoderConfig) error { + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return err + } + return decoder.Decode(input) +} + +// UnmarshalExact unmarshals the config into a Struct, erroring if a field is nonexistent +// in the destination struct. +func (v *Viper) UnmarshalExact(rawVal interface{}) error { + config := defaultDecoderConfig(rawVal) + config.ErrorUnused = true + + err := decode(v.AllSettings(), config) + + if err != nil { + return err + } + + v.insensitiviseMaps() + + return nil +} + +// BindPFlags binds a full flag set to the configuration, using each flag's long // name as the config key. -func BindPFlags(flags *pflag.FlagSet) (err error) { return v.BindPFlags(flags) } -func (v *Viper) BindPFlags(flags *pflag.FlagSet) (err error) { +func BindPFlags(flags *pflag.FlagSet) error { return v.BindPFlags(flags) } +func (v *Viper) BindPFlags(flags *pflag.FlagSet) error { return v.BindFlagValues(pflagValueSet{flags}) } -// Bind a specific key to a pflag (as used by cobra) -// Example(where serverCmd is a Cobra instance): +// BindPFlag binds a specific key to a pflag (as used by cobra). +// Example (where serverCmd is a Cobra instance): // // serverCmd.Flags().Int("port", 1138, "Port to run Application server on") // Viper.BindPFlag("port", serverCmd.Flags().Lookup("port")) // -func BindPFlag(key string, flag *pflag.Flag) (err error) { return v.BindPFlag(key, flag) } -func (v *Viper) BindPFlag(key string, flag *pflag.Flag) (err error) { +func BindPFlag(key string, flag *pflag.Flag) error { return v.BindPFlag(key, flag) } +func (v *Viper) BindPFlag(key string, flag *pflag.Flag) error { return v.BindFlagValue(key, pflagValue{flag}) } -// Bind a full FlagValue set to the configuration, using each flag's long +// BindFlagValues binds a full FlagValue set to the configuration, using each flag's long // name as the config key. -func BindFlagValues(flags FlagValueSet) (err error) { return v.BindFlagValues(flags) } +func BindFlagValues(flags FlagValueSet) error { return v.BindFlagValues(flags) } func (v *Viper) BindFlagValues(flags FlagValueSet) (err error) { flags.VisitAll(func(flag FlagValue) { if err = v.BindFlagValue(flag.Name(), flag); err != nil { @@ -659,14 +829,14 @@ func (v *Viper) BindFlagValues(flags FlagValueSet) (err error) { return nil } -// Bind a specific key to a FlagValue. +// BindFlagValue binds a specific key to a FlagValue. // Example(where serverCmd is a Cobra instance): // // serverCmd.Flags().Int("port", 1138, "Port to run Application server on") // Viper.BindFlagValue("port", serverCmd.Flags().Lookup("port")) // -func BindFlagValue(key string, flag FlagValue) (err error) { return v.BindFlagValue(key, flag) } -func (v *Viper) BindFlagValue(key string, flag FlagValue) (err error) { +func BindFlagValue(key string, flag FlagValue) error { return v.BindFlagValue(key, flag) } +func (v *Viper) BindFlagValue(key string, flag FlagValue) error { if flag == nil { return fmt.Errorf("flag for %q is nil", key) } @@ -674,12 +844,12 @@ func (v *Viper) BindFlagValue(key string, flag FlagValue) (err error) { return nil } -// Binds a Viper key to a ENV variable -// ENV variables are case sensitive +// BindEnv binds a Viper key to a ENV variable. +// ENV variables are case sensitive. // If only a key is provided, it will use the env key matching the key, uppercased. // EnvPrefix will be used when set when env name is not provided. -func BindEnv(input ...string) (err error) { return v.BindEnv(input...) } -func (v *Viper) BindEnv(input ...string) (err error) { +func BindEnv(input ...string) error { return v.BindEnv(input...) } +func (v *Viper) BindEnv(input ...string) error { var key, envkey string if len(input) == 0 { return fmt.Errorf("BindEnv missing key to bind to") @@ -698,113 +868,138 @@ func (v *Viper) BindEnv(input ...string) (err error) { return nil } -// Given a key, find the value +// Given a key, find the value. // Viper will check in the following order: -// flag, env, config file, key/value store, default -// Viper will check to see if an alias exists first -func (v *Viper) find(key string) interface{} { - var val interface{} - var exists bool +// flag, env, config file, key/value store, default. +// Viper will check to see if an alias exists first. +// Note: this assumes a lower-cased key given. +func (v *Viper) find(lcaseKey string) interface{} { + + var ( + val interface{} + exists bool + path = strings.Split(lcaseKey, v.keyDelim) + nested = len(path) > 1 + ) + + // compute the path through the nested maps to the nested value + if nested && v.isPathShadowedInDeepMap(path, castMapStringToMapInterface(v.aliases)) != "" { + return nil + } // if the requested key is an alias, then return the proper key - key = v.realKey(key) + lcaseKey = v.realKey(lcaseKey) - // PFlag Override first - flag, exists := v.pflags[key] + // Set() override first + val = v.searchMapForKey(v.override, lcaseKey) + if val != nil { + return val + } + + path = strings.Split(lcaseKey, v.keyDelim) + nested = len(path) > 1 + + if nested && v.isPathShadowedInDeepMap(path, v.override) != "" { + return nil + } + + // PFlag override next + flag, exists := v.pflags[lcaseKey] if exists && flag.HasChanged() { - jww.TRACE.Println(key, "found in override (via pflag):", flag.ValueString()) switch flag.ValueType() { case "int", "int8", "int16", "int32", "int64": return cast.ToInt(flag.ValueString()) case "bool": return cast.ToBool(flag.ValueString()) + case "stringSlice": + s := strings.TrimPrefix(flag.ValueString(), "[") + return strings.TrimSuffix(s, "]") default: return flag.ValueString() } } - val, exists = v.override[key] - if exists { - jww.TRACE.Println(key, "found in override:", val) - return val + if nested && v.isPathShadowedInFlatMap(path, v.pflags) != "" { + return nil } + // Env override next if v.automaticEnvApplied { // even if it hasn't been registered, if automaticEnv is used, // check any Get request - if val = v.getEnv(v.mergeWithEnvPrefix(key)); val != "" { - jww.TRACE.Println(key, "found in environment with val:", val) + if val = v.getEnv(v.mergeWithEnvPrefix(lcaseKey)); val != "" { return val } + if nested && v.isPathShadowedInAutoEnv(path) != "" { + return nil + } } - - envkey, exists := v.env[key] + envkey, exists := v.env[lcaseKey] if exists { - jww.TRACE.Println(key, "registered as env var", envkey) if val = v.getEnv(envkey); val != "" { - jww.TRACE.Println(envkey, "found in environment with val:", val) return val - } else { - jww.TRACE.Println(envkey, "env value unset:") } } - - val, exists = v.config[key] - if exists { - jww.TRACE.Println(key, "found in config:", val) - return val + if shadow := v.isPathShadowedInFlatMap(path, v.env); shadow != "" { + return nil } - // Test for nested config parameter - if strings.Contains(key, v.keyDelim) { - path := strings.Split(key, v.keyDelim) + // Config file next + val = v.searchMapWithPathPrefixes(v.config, path) + if val != nil { + return val + } + if shadow := v.isPathShadowedInDeepMap(path, v.config); shadow != "" { + return nil + } - source := v.find(path[0]) - if source != nil { - if reflect.TypeOf(source).Kind() == reflect.Map { - val := v.searchMap(cast.ToStringMap(source), path[1:]) - jww.TRACE.Println(key, "found in nested config:", val) - return val - } + // K/V store next + val = v.searchMap(v.kvstore, path) + if val != nil { + return val + } + if shadow := v.isPathShadowedInDeepMap(path, v.kvstore); shadow != "" { + return nil + } + + // Default next + val = v.searchMap(v.defaults, path) + if val != nil { + return val + } + if shadow := v.isPathShadowedInDeepMap(path, v.defaults); shadow != "" { + return nil + } + + // last chance: if no other value is returned and a flag does exist for the value, + // get the flag's value even if the flag's value has not changed + if flag, exists := v.pflags[lcaseKey]; exists { + switch flag.ValueType() { + case "int", "int8", "int16", "int32", "int64": + return cast.ToInt(flag.ValueString()) + case "bool": + return cast.ToBool(flag.ValueString()) + case "stringSlice": + s := strings.TrimPrefix(flag.ValueString(), "[") + return strings.TrimSuffix(s, "]") + default: + return flag.ValueString() } } - - val, exists = v.kvstore[key] - if exists { - jww.TRACE.Println(key, "found in key/value store:", val) - return val - } - - val, exists = v.defaults[key] - if exists { - jww.TRACE.Println(key, "found in defaults:", val) - return val - } + // last item, no need to check shadowing return nil } -// Check to see if the key has been set in any of the data locations +// IsSet checks 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 { - path := strings.Split(key, v.keyDelim) - lcaseKey := strings.ToLower(key) val := v.find(lcaseKey) - - if val == nil { - source := v.find(strings.ToLower(path[0])) - if source != nil { - if reflect.TypeOf(source).Kind() == reflect.Map { - val = v.searchMap(cast.ToStringMap(source), path[1:]) - } - } - } - return val != nil } -// Have Viper check ENV variables for all +// AutomaticEnv has Viper check ENV variables for all. // keys set in config, default & flags func AutomaticEnv() { v.AutomaticEnv() } func (v *Viper) AutomaticEnv() { @@ -863,12 +1058,11 @@ func (v *Viper) realKey(key string) string { if exists { jww.DEBUG.Println("Alias", key, "to", newkey) return v.realKey(newkey) - } else { - return key } + return key } -// Check to see if the given key (or an alias) is in the config file +// InConfig checks to see if the given key (or an alias) is in the config file. func InConfig(key string) bool { return v.InConfig(key) } func (v *Viper) InConfig(key string) bool { // if the requested key is an alias, then return the proper key @@ -878,26 +1072,38 @@ func (v *Viper) InConfig(key string) bool { return exists } -// Set the default value for this key. +// SetDefault sets the default value for this key. // Default only used when no value is provided by the user via flag, config or ENV. func SetDefault(key string, value interface{}) { v.SetDefault(key, value) } func (v *Viper) SetDefault(key string, value interface{}) { // If alias passed in, then set the proper default key = v.realKey(strings.ToLower(key)) - v.defaults[key] = value + + path := strings.Split(key, v.keyDelim) + lastKey := strings.ToLower(path[len(path)-1]) + deepestMap := deepSearch(v.defaults, path[0:len(path)-1]) + + // set innermost value + deepestMap[lastKey] = value } -// Sets the value for the key in the override regiser. +// Set sets the value for the key in the override regiser. // Will be used instead of values obtained via -// flags, config file, ENV, default, or key/value store +// flags, config file, ENV, default, or key/value store. func Set(key string, value interface{}) { v.Set(key, value) } func (v *Viper) Set(key string, value interface{}) { // If alias passed in, then set the proper override key = v.realKey(strings.ToLower(key)) - v.override[key] = value + + path := strings.Split(key, v.keyDelim) + lastKey := strings.ToLower(path[len(path)-1]) + deepestMap := deepSearch(v.override, path[0:len(path)-1]) + + // set innermost value + deepestMap[lastKey] = value } -// Viper will discover and load the configuration file from disk +// ReadInConfig will discover and load the configuration file from disk // and key/value stores, searching in one of the defined paths. func ReadInConfig() error { return v.ReadInConfig() } func (v *Viper) ReadInConfig() error { @@ -911,7 +1117,7 @@ func (v *Viper) ReadInConfig() error { return UnsupportedConfigError(v.getConfigType()) } - file, err := ioutil.ReadFile(filename) + file, err := afero.ReadFile(v.fs, filename) if err != nil { return err } @@ -934,7 +1140,7 @@ func (v *Viper) MergeInConfig() error { return err } - file, err := ioutil.ReadFile(filename) + file, err := afero.ReadFile(v.fs, filename) if err != nil { return err } @@ -942,7 +1148,7 @@ func (v *Viper) MergeInConfig() error { return v.MergeConfig(bytes.NewReader(file)) } -// Viper will read a configuration file, setting existing keys to nil if the +// ReadConfig will read a configuration file, setting existing keys to nil if the // key does not exist in the file. func ReadConfig(in io.Reader) error { return v.ReadConfig(in) } func (v *Viper) ReadConfig(in io.Reader) error { @@ -984,6 +1190,14 @@ func castToMapStringInterface( return tgt } +func castMapStringToMapInterface(src map[string]string) map[string]interface{} { + tgt := map[string]interface{}{} + for k, v := range src { + tgt[k] = v + } + return tgt +} + // mergeMaps merges two maps. The `itgt` parameter is for handling go-yaml's // insistence on parsing nested structures as `map[interface{}]interface{}` // instead of using a `string` as the key for nest structures beyond one level @@ -1044,34 +1258,20 @@ func mergeMaps( } } -// func ReadBufConfig(buf *bytes.Buffer) error { return v.ReadBufConfig(buf) } -// func (v *Viper) ReadBufConfig(buf *bytes.Buffer) error { -// v.config = make(map[string]interface{}) -// return v.unmarshalReader(buf, v.config) -// } - -// Attempts to get configuration from a remote source +// ReadRemoteConfig attempts to get configuration from a remote source // and read it in the remote configuration registry. func ReadRemoteConfig() error { return v.ReadRemoteConfig() } func (v *Viper) ReadRemoteConfig() error { - err := v.getKeyValueConfig() - if err != nil { - return err - } - return nil + return v.getKeyValueConfig() } func WatchRemoteConfig() error { return v.WatchRemoteConfig() } func (v *Viper) WatchRemoteConfig() error { - err := v.watchKeyValueConfig() - if err != nil { - return err - } - return nil + return v.watchKeyValueConfig() } -// Unmarshall a Reader into a map -// Should probably be an unexported function +// Unmarshall a Reader into a map. +// Should probably be an unexported function. func unmarshalReader(in io.Reader, c map[string]interface{}) error { return v.unmarshalReader(in, c) } @@ -1087,7 +1287,7 @@ func (v *Viper) insensitiviseMaps() { insensitiviseMap(v.kvstore) } -// retrieve the first found remote configuration +// Retrieve the first found remote configuration. func (v *Viper) getKeyValueConfig() error { if RemoteConfig == nil { return RemoteConfigError("Enable the remote features by doing a blank import of the viper/remote package: '_ github.com/spf13/viper/remote'") @@ -1104,8 +1304,7 @@ func (v *Viper) getKeyValueConfig() error { return RemoteConfigError("No Files Found") } -func (v *Viper) getRemoteConfig(provider *defaultRemoteProvider) (map[string]interface{}, error) { - +func (v *Viper) getRemoteConfig(provider RemoteProvider) (map[string]interface{}, error) { reader, err := RemoteConfig.Get(provider) if err != nil { return nil, err @@ -1114,7 +1313,7 @@ func (v *Viper) getRemoteConfig(provider *defaultRemoteProvider) (map[string]int return v.kvstore, err } -// retrieve the first found remote configuration +// Retrieve the first found remote configuration. func (v *Viper) watchKeyValueConfig() error { for _, rp := range v.remoteProviders { val, err := v.watchRemoteConfig(rp) @@ -1127,7 +1326,7 @@ func (v *Viper) watchKeyValueConfig() error { return RemoteConfigError("No Files Found") } -func (v *Viper) watchRemoteConfig(provider *defaultRemoteProvider) (map[string]interface{}, error) { +func (v *Viper) watchRemoteConfig(provider RemoteProvider) (map[string]interface{}, error) { reader, err := RemoteConfig.Watch(provider) if err != nil { return nil, err @@ -1136,64 +1335,134 @@ func (v *Viper) watchRemoteConfig(provider *defaultRemoteProvider) (map[string]i return v.kvstore, err } -// Return all keys regardless where they are set +// AllKeys returns all keys holding a value, regardless of where they are set. +// Nested keys are returned with a v.keyDelim (= ".") separator func AllKeys() []string { return v.AllKeys() } func (v *Viper) AllKeys() []string { - m := map[string]struct{}{} - - for key, _ := range v.defaults { - m[strings.ToLower(key)] = struct{}{} - } - - for key, _ := range v.pflags { - m[strings.ToLower(key)] = struct{}{} - } - - for key, _ := range v.env { - m[strings.ToLower(key)] = struct{}{} - } - - for key, _ := range v.config { - m[strings.ToLower(key)] = struct{}{} - } - - for key, _ := range v.kvstore { - m[strings.ToLower(key)] = struct{}{} - } - - for key, _ := range v.override { - m[strings.ToLower(key)] = struct{}{} - } + m := map[string]bool{} + // add all paths, by order of descending priority to ensure correct shadowing + m = v.flattenAndMergeMap(m, castMapStringToMapInterface(v.aliases), "") + m = v.flattenAndMergeMap(m, v.override, "") + m = v.mergeFlatMap(m, v.pflags) + m = v.mergeFlatMap(m, v.env) + m = v.flattenAndMergeMap(m, v.config, "") + m = v.flattenAndMergeMap(m, v.kvstore, "") + m = v.flattenAndMergeMap(m, v.defaults, "") + // convert set of paths to list a := []string{} - for x, _ := range m { + for x := range m { a = append(a, x) } - return a } -// Return all settings as a map[string]interface{} +// flattenAndMergeMap recursively flattens the given map into a map[string]bool +// of key paths (used as a set, easier to manipulate than a []string): +// - each path is merged into a single key string, delimited with v.keyDelim (= ".") +// - if a path is shadowed by an earlier value in the initial shadow map, +// it is skipped. +// The resulting set of paths is merged to the given shadow set at the same time. +func (v *Viper) flattenAndMergeMap(shadow map[string]bool, m map[string]interface{}, prefix string) map[string]bool { + if shadow != nil && prefix != "" && shadow[prefix] { + // prefix is shadowed => nothing more to flatten + return shadow + } + if shadow == nil { + shadow = make(map[string]bool) + } + + var m2 map[string]interface{} + if prefix != "" { + prefix += v.keyDelim + } + for k, val := range m { + fullKey := prefix + k + switch val.(type) { + case map[string]interface{}: + m2 = val.(map[string]interface{}) + case map[interface{}]interface{}: + m2 = cast.ToStringMap(val) + default: + // immediate value + shadow[strings.ToLower(fullKey)] = true + continue + } + // recursively merge to shadow map + shadow = v.flattenAndMergeMap(shadow, m2, fullKey) + } + return shadow +} + +// mergeFlatMap merges the given maps, excluding values of the second map +// shadowed by values from the first map. +func (v *Viper) mergeFlatMap(shadow map[string]bool, mi interface{}) map[string]bool { + // unify input map + var m map[string]interface{} + switch mi.(type) { + case map[string]string, map[string]FlagValue: + m = cast.ToStringMap(mi) + default: + return shadow + } + + // scan keys +outer: + for k, _ := range m { + path := strings.Split(k, v.keyDelim) + // scan intermediate paths + var parentKey string + for i := 1; i < len(path); i++ { + parentKey = strings.Join(path[0:i], v.keyDelim) + if shadow[parentKey] { + // path is shadowed, continue + continue outer + } + } + // add key + shadow[strings.ToLower(k)] = true + } + return shadow +} + +// AllSettings merges all settings and returns them 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) + // start from the list of keys, and construct the map one value at a time + for _, k := range v.AllKeys() { + value := v.Get(k) + if value == nil { + // should not happen, since AllKeys() returns only keys holding a value, + // check just in case anything changes + continue + } + path := strings.Split(k, v.keyDelim) + lastKey := strings.ToLower(path[len(path)-1]) + deepestMap := deepSearch(m, path[0:len(path)-1]) + // set innermost value + deepestMap[lastKey] = value } - return m } -// Name for the config file. +// SetFs sets the filesystem to use to read configuration. +func SetFs(fs afero.Fs) { v.SetFs(fs) } +func (v *Viper) SetFs(fs afero.Fs) { + v.fs = fs +} + +// SetConfigName sets name for the config file. // Does not include extension. func SetConfigName(in string) { v.SetConfigName(in) } func (v *Viper) SetConfigName(in string) { if in != "" { v.configName = in + v.configFile = "" } } -// Sets the type of the configuration returned by the +// SetConfigType sets the type of the configuration returned by the // remote source, e.g. "json". func SetConfigType(in string) { v.SetConfigType(in) } func (v *Viper) SetConfigType(in string) { @@ -1216,9 +1485,9 @@ func (v *Viper) getConfigType() string { if len(ext) > 1 { return ext[1:] - } else { - return "" } + + return "" } func (v *Viper) getConfigFile() (string, error) { @@ -1249,8 +1518,8 @@ func (v *Viper) searchInPath(in string) (filename string) { return "" } -// search all configPaths for any config file. -// Returns the first path that exists (and is a config file) +// Search all configPaths for any config file. +// Returns the first path that exists (and is a config file). func (v *Viper) findConfigFile() (string, error) { jww.INFO.Println("Searching for config in ", v.configPaths) @@ -1264,22 +1533,15 @@ func (v *Viper) findConfigFile() (string, error) { return "", ConfigFileNotFoundError{v.configName, fmt.Sprintf("%s", v.configPaths)} } -// Prints all configuration registries for debugging +// Debug prints all configuration registries for debugging // purposes. func Debug() { v.Debug() } func (v *Viper) Debug() { - 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) + fmt.Printf("Aliases:\n%#v\n", v.aliases) + fmt.Printf("Override:\n%#v\n", v.override) + fmt.Printf("PFlags:\n%#v\n", v.pflags) + fmt.Printf("Env:\n%#v\n", v.env) + fmt.Printf("Key/Value Store:\n%#v\n", v.kvstore) + fmt.Printf("Config:\n%#v\n", v.config) + fmt.Printf("Defaults:\n%#v\n", v.defaults) } diff --git a/viper_test.go b/viper_test.go index 328ea21..f34421c 100644 --- a/viper_test.go +++ b/viper_test.go @@ -8,6 +8,7 @@ package viper import ( "bytes" "fmt" + "io" "io/ioutil" "os" "path" @@ -17,6 +18,8 @@ import ( "testing" "time" + "github.com/spf13/cast" + "github.com/spf13/pflag" "github.com/stretchr/testify/assert" ) @@ -37,6 +40,14 @@ eyes : brown beard: true `) +var yamlExampleWithExtras = []byte(`Existing: true +Bogus: true +`) + +type testUnmarshalExtra struct { + Existing bool +} + var tomlExample = []byte(` title = "TOML Example" @@ -96,8 +107,9 @@ var remoteExample = []byte(`{ func initConfigs() { Reset() + var r io.Reader SetConfigType("yaml") - r := bytes.NewReader(yamlExample) + r = bytes.NewReader(yamlExample) unmarshalReader(r, v.config) SetConfigType("json") @@ -121,12 +133,18 @@ func initConfigs() { unmarshalReader(remote, v.kvstore) } -func initYAML() { +func initConfig(typ, config string) { Reset() - SetConfigType("yaml") - r := bytes.NewReader(yamlExample) + SetConfigType(typ) + r := strings.NewReader(config) - unmarshalReader(r, v.config) + if err := unmarshalReader(r, v.config); err != nil { + panic(err) + } +} + +func initYAML() { + initConfig("yaml", string(yamlExample)) } func initJSON() { @@ -253,10 +271,22 @@ func TestUnmarshalling(t *testing.T) { assert.False(t, InConfig("state")) assert.Equal(t, "steve", Get("name")) assert.Equal(t, []interface{}{"skateboarding", "snowboarding", "go"}, Get("hobbies")) - assert.Equal(t, map[interface{}]interface{}{"jacket": "leather", "trousers": "denim", "pants": map[interface{}]interface{}{"size": "large"}}, Get("clothing")) + assert.Equal(t, map[string]interface{}{"jacket": "leather", "trousers": "denim", "pants": map[interface{}]interface{}{"size": "large"}}, Get("clothing")) assert.Equal(t, 35, Get("age")) } +func TestUnmarshalExact(t *testing.T) { + vip := New() + target := &testUnmarshalExtra{} + vip.SetConfigType("yaml") + r := bytes.NewReader(yamlExampleWithExtras) + vip.ReadConfig(r) + err := vip.UnmarshalExact(target) + if err == nil { + t.Fatal("UnmarshalExact should error when populating a struct from a conf that contains unused fields") + } +} + func TestOverrides(t *testing.T) { Set("age", 40) assert.Equal(t, 40, Get("age")) @@ -402,9 +432,9 @@ func TestSetEnvReplacer(t *testing.T) { func TestAllKeys(t *testing.T) { initConfigs() - 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", "foos"} + ks := sort.StringSlice{"title", "newkey", "owner.organization", "owner.dob", "owner.bio", "name", "beard", "ppu", "batters.batter", "hobbies", "clothing.jacket", "clothing.trousers", "clothing.pants.size", "age", "hacker", "id", "type", "eyes", "p_id", "p_ppu", "p_batters.batter.type", "p_type", "p_name", "foos"} 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", "pants": map[interface{}]interface{}{"size": "large"}}, "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", "foos": []map[string]interface{}{map[string]interface{}{"foo": []map[string]interface{}{map[string]interface{}{"key": 1}, map[string]interface{}{"key": 2}, map[string]interface{}{"key": 3}, map[string]interface{}{"key": 4}}}}} + 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[string]interface{}{"trousers": "denim", "jacket": "leather", "pants": map[string]interface{}{"size": "large"}}, "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": map[string]interface{}{"batter": map[string]interface{}{"type": "Regular"}}, "p_type": "donut", "foos": []map[string]interface{}{map[string]interface{}{"foo": []map[string]interface{}{map[string]interface{}{"key": 1}, map[string]interface{}{"key": 2}, map[string]interface{}{"key": 3}, map[string]interface{}{"key": 4}}}}} var allkeys sort.StringSlice allkeys = AllKeys() @@ -415,13 +445,8 @@ func TestAllKeys(t *testing.T) { assert.Equal(t, all, AllSettings()) } -func TestCaseInSensitive(t *testing.T) { - assert.Equal(t, true, Get("hacker")) - Set("Title", "Checking Case") - assert.Equal(t, "Checking Case", Get("tItle")) -} - func TestAliasesOfAliases(t *testing.T) { + Set("Title", "Checking Case") RegisterAlias("Foo", "Bar") RegisterAlias("Bar", "Title") assert.Equal(t, "Checking Case", Get("FOO")) @@ -435,10 +460,12 @@ func TestRecursiveAliases(t *testing.T) { func TestUnmarshal(t *testing.T) { SetDefault("port", 1313) Set("name", "Steve") + Set("duration", "1s1ms") type config struct { - Port int - Name string + Port int + Name string + Duration time.Duration } var C config @@ -448,17 +475,18 @@ func TestUnmarshal(t *testing.T) { t.Fatalf("unable to decode into struct, %v", err) } - assert.Equal(t, &C, &config{Name: "Steve", Port: 1313}) + assert.Equal(t, &config{Name: "Steve", Port: 1313, Duration: time.Second + time.Millisecond}, &C) Set("port", 1234) err = Unmarshal(&C) if err != nil { t.Fatalf("unable to decode into struct, %v", err) } - assert.Equal(t, &C, &config{Name: "Steve", Port: 1234}) + assert.Equal(t, &config{Name: "Steve", Port: 1234, Duration: time.Second + time.Millisecond}, &C) } func TestBindPFlags(t *testing.T) { + v := New() // create independent Viper object flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) var testValues = map[string]*string{ @@ -473,11 +501,11 @@ func TestBindPFlags(t *testing.T) { "endpoint": "/public", } - for name, _ := range testValues { + for name := range testValues { testValues[name] = flagSet.String(name, "", "test") } - err := BindPFlags(flagSet) + err := v.BindPFlags(flagSet) if err != nil { t.Fatalf("error binding flag set, %v", err) } @@ -488,7 +516,7 @@ func TestBindPFlags(t *testing.T) { }) for name, expected := range mutatedTestValues { - assert.Equal(t, Get(name), expected) + assert.Equal(t, expected, v.Get(name)) } } @@ -515,7 +543,6 @@ func TestBindPFlag(t *testing.T) { } func TestBoundCaseSensitivity(t *testing.T) { - assert.Equal(t, "brown", Get("eyes")) BindEnv("eYEs", "TURTLE_EYES") @@ -621,7 +648,7 @@ func TestFindsNestedKeys(t *testing.T) { "name": "Cake", "hacker": true, "ppu": 0.55, - "clothing": map[interface{}]interface{}{ + "clothing": map[string]interface{}{ "jacket": "leather", "trousers": "denim", "pants": map[interface{}]interface{}{ @@ -670,7 +697,7 @@ func TestReadBufConfig(t *testing.T) { assert.False(t, v.InConfig("state")) assert.Equal(t, "steve", v.Get("name")) assert.Equal(t, []interface{}{"skateboarding", "snowboarding", "go"}, v.Get("hobbies")) - assert.Equal(t, map[interface{}]interface{}{"jacket": "leather", "trousers": "denim", "pants": map[interface{}]interface{}{"size": "large"}}, v.Get("clothing")) + assert.Equal(t, map[string]interface{}{"jacket": "leather", "trousers": "denim", "pants": map[interface{}]interface{}{"size": "large"}}, v.Get("clothing")) assert.Equal(t, 35, v.Get("age")) } @@ -739,12 +766,16 @@ func TestSub(t *testing.T) { assert.Equal(t, v.Get("clothing.pants.size"), subv.Get("size")) subv = v.Sub("clothing.pants.size") - assert.Equal(t, subv, (*Viper)(nil)) + assert.Equal(t, (*Viper)(nil), subv) + + subv = v.Sub("missing.key") + assert.Equal(t, (*Viper)(nil), subv) } var yamlMergeExampleTgt = []byte(` hello: pop: 37890 + lagrenum: 765432101234567 world: - us - uk @@ -755,6 +786,7 @@ hello: var yamlMergeExampleSrc = []byte(` hello: pop: 45000 + lagrenum: 7654321001234567 universe: - mw - ad @@ -772,6 +804,14 @@ func TestMergeConfig(t *testing.T) { t.Fatalf("pop != 37890, = %d", pop) } + if pop := v.GetInt("hello.lagrenum"); pop != 765432101234567 { + t.Fatalf("lagrenum != 765432101234567, = %d", pop) + } + + if pop := v.GetInt64("hello.lagrenum"); pop != int64(765432101234567) { + t.Fatalf("int64 lagrenum != 765432101234567, = %d", pop) + } + if world := v.GetStringSlice("hello.world"); len(world) != 4 { t.Fatalf("len(world) != 4, = %d", len(world)) } @@ -788,6 +828,14 @@ func TestMergeConfig(t *testing.T) { t.Fatalf("pop != 45000, = %d", pop) } + if pop := v.GetInt("hello.lagrenum"); pop != 7654321001234567 { + t.Fatalf("lagrenum != 7654321001234567, = %d", pop) + } + + if pop := v.GetInt64("hello.lagrenum"); pop != int64(7654321001234567) { + t.Fatalf("int64 lagrenum != 7654321001234567, = %d", pop) + } + if world := v.GetStringSlice("hello.world"); len(world) != 4 { t.Fatalf("len(world) != 4, = %d", len(world)) } @@ -840,3 +888,171 @@ func TestMergeConfigNoMerge(t *testing.T) { t.Fatalf("fu != \"bar\", = %s", fu) } } + +func TestUnmarshalingWithAliases(t *testing.T) { + v := New() + v.SetDefault("ID", 1) + v.Set("name", "Steve") + v.Set("lastname", "Owen") + + v.RegisterAlias("UserID", "ID") + v.RegisterAlias("Firstname", "name") + v.RegisterAlias("Surname", "lastname") + + type config struct { + ID int + FirstName string + Surname string + } + + var C config + err := v.Unmarshal(&C) + if err != nil { + t.Fatalf("unable to decode into struct, %v", err) + } + + assert.Equal(t, &config{ID: 1, FirstName: "Steve", Surname: "Owen"}, &C) +} + +func TestSetConfigNameClearsFileCache(t *testing.T) { + SetConfigFile("/tmp/config.yaml") + SetConfigName("default") + f, err := v.getConfigFile() + if err == nil { + t.Fatalf("config file cache should have been cleared") + } + assert.Empty(t, f) +} + +func TestShadowedNestedValue(t *testing.T) { + + config := `name: steve +clothing: + jacket: leather + trousers: denim + pants: + size: large +` + initConfig("yaml", config) + + assert.Equal(t, "steve", GetString("name")) + + polyester := "polyester" + SetDefault("clothing.shirt", polyester) + SetDefault("clothing.jacket.price", 100) + + assert.Equal(t, "leather", GetString("clothing.jacket")) + assert.Nil(t, Get("clothing.jacket.price")) + assert.Equal(t, polyester, GetString("clothing.shirt")) + + clothingSettings := AllSettings()["clothing"].(map[string]interface{}) + assert.Equal(t, "leather", clothingSettings["jacket"]) + assert.Equal(t, polyester, clothingSettings["shirt"]) +} + +func TestDotParameter(t *testing.T) { + initJSON() + // shoud take precedence over batters defined in jsonExample + r := bytes.NewReader([]byte(`{ "batters.batter": [ { "type": "Small" } ] }`)) + unmarshalReader(r, v.config) + + actual := Get("batters.batter") + expected := []interface{}{map[string]interface{}{"type": "Small"}} + assert.Equal(t, expected, actual) +} + +func TestCaseInSensitive(t *testing.T) { + for _, config := range []struct { + typ string + content string + }{ + {"yaml", ` +aBcD: 1 +eF: + gH: 2 + iJk: 3 + Lm: + nO: 4 + P: + Q: 5 + R: 6 +`}, + {"json", `{ + "aBcD": 1, + "eF": { + "iJk": 3, + "Lm": { + "P": { + "Q": 5, + "R": 6 + }, + "nO": 4 + }, + "gH": 2 + } +}`}, + {"toml", `aBcD = 1 +[eF] +gH = 2 +iJk = 3 +[eF.Lm] +nO = 4 +[eF.Lm.P] +Q = 5 +R = 6 +`}, + } { + doTestCaseInSensitive(t, config.typ, config.content) + } +} + +func doTestCaseInSensitive(t *testing.T, typ, config string) { + initConfig(typ, config) + Set("RfD", true) + assert.Equal(t, true, Get("rfd")) + assert.Equal(t, true, Get("rFD")) + assert.Equal(t, 1, cast.ToInt(Get("abcd"))) + assert.Equal(t, 1, cast.ToInt(Get("Abcd"))) + assert.Equal(t, 2, cast.ToInt(Get("ef.gh"))) + assert.Equal(t, 3, cast.ToInt(Get("ef.ijk"))) + assert.Equal(t, 4, cast.ToInt(Get("ef.lm.no"))) + assert.Equal(t, 5, cast.ToInt(Get("ef.lm.p.q"))) + +} + +func BenchmarkGetBool(b *testing.B) { + key := "BenchmarkGetBool" + v = New() + v.Set(key, true) + + for i := 0; i < b.N; i++ { + if !v.GetBool(key) { + b.Fatal("GetBool returned false") + } + } +} + +func BenchmarkGet(b *testing.B) { + key := "BenchmarkGet" + v = New() + v.Set(key, true) + + for i := 0; i < b.N; i++ { + if !v.Get(key).(bool) { + b.Fatal("Get returned false") + } + } +} + +// This is the "perfect result" for the above. +func BenchmarkGetBoolFromMap(b *testing.B) { + m := make(map[string]bool) + key := "BenchmarkGetBool" + m[key] = true + + for i := 0; i < b.N; i++ { + if !m[key] { + b.Fatal("Map value was false") + } + } +}