Merge remote-tracking branch 'wrobbins/master'

This commit is contained in:
Dana NicCaluim 2015-07-20 18:20:28 -07:00
commit eac5b31a90
4 changed files with 220 additions and 17 deletions

1
.gitignore vendored
View file

@ -6,6 +6,7 @@
# Folders # Folders
_obj _obj
_test _test
.idea
# Architecture specific extensions/prefixes # Architecture specific extensions/prefixes
*.[568vq] *.[568vq]

View file

@ -72,7 +72,7 @@ Examples:
If you want to support a config file, Viper requires a minimal If you want to support a config file, Viper requires a minimal
configuration so it knows where to look for the config file. Viper configuration so it knows where to look for the config file. Viper
supports json, toml and yaml files. Viper can search multiple paths, but supports json, toml and yaml files. Viper can search multiple paths, but
currently a single viper only supports a single config file. currently a single viper only supports a single config file, unless cascading is enabled.
viper.SetConfigName("config") // name of config file (without extension) viper.SetConfigName("config") // name of config file (without extension)
viper.AddConfigPath("/etc/appname/") // path to look for the config file in viper.AddConfigPath("/etc/appname/") // path to look for the config file in
@ -84,14 +84,14 @@ currently a single viper only supports a single config file.
### Reading Config from io.Reader ### Reading Config from io.Reader
Viper predefined many configuration sources, such as files, environment variables, flags and Viper predefined many configuration sources, such as files, environment variables, flags and
remote K/V store. But you are not bound to them. You can also implement your own way to remote K/V store. But you are not bound to them. You can also implement your own way to
require configuration and feed it to viper. require configuration and feed it to viper.
````go ````go
viper.SetConfigType("yaml") // or viper.SetConfigType("YAML") viper.SetConfigType("yaml") // or viper.SetConfigType("YAML")
// any approach to require this configuration into your program. // any approach to require this configuration into your program.
var yamlExample = []byte(` var yamlExample = []byte(`
Hacker: true Hacker: true
name: steve name: steve
@ -112,6 +112,22 @@ viper.ReadConfig(bytes.NewBuffer(yamlExample))
viper.Get("name") // this would be "steve" viper.Get("name") // this would be "steve"
```` ````
#### Enabling Cascading
By default Viper stops reading configuration once it encounters the first available configuration file.
That means each configuration file must contain all configuration values you need.
By enabling cascading you can create sparse configuration files. Configuration will cascade down in
the order that files are added by AddConfigPath. For more see viper_test's cascading tests.
Consider:
* \etc\myapp\myapp.json
* ($GOPATH)\src\myapp\myapp.json
You can check in a default myapp.json for development and only override certain kvps in production
viper.EnableCascading(true)
### Setting Overrides ### Setting Overrides
These could be from a command line flag, or from your own application logic. These could be from a command line flag, or from your own application logic.
@ -270,7 +286,7 @@ to use Consul.
continue continue
} }
// marshal new config into our runtime config struct. you can also use channel // marshal new config into our runtime config struct. you can also use channel
// to implement a signal to notify the system of the changes // to implement a signal to notify the system of the changes
runtime_viper.Marshal(&runtime_conf) runtime_viper.Marshal(&runtime_conf)
} }
@ -307,7 +323,7 @@ Example:
### Accessing nested keys ### Accessing nested keys
The accessor methods also accept formatted paths to deeply nested keys. The accessor methods also accept formatted paths to deeply nested keys.
For example, if the following JSON file is loaded: For example, if the following JSON file is loaded:
``` ```
@ -346,7 +362,7 @@ On the other hand, if the primary key was not defined, Viper would go through th
remaining registries looking for it. remaining registries looking for it.
Lastly, if there exists a key that matches the delimited key path, its value will Lastly, if there exists a key that matches the delimited key path, its value will
be returned instead. E.g. be returned instead. E.g.
``` ```
{ {

103
viper.go
View file

@ -133,13 +133,16 @@ type Viper struct {
automaticEnvApplied bool automaticEnvApplied bool
envKeyReplacer *strings.Replacer envKeyReplacer *strings.Replacer
config map[string]interface{} cascadeConfigurations bool
override map[string]interface{}
defaults map[string]interface{} config map[string]interface{}
kvstore map[string]interface{} override map[string]interface{}
pflags map[string]*pflag.Flag defaults map[string]interface{}
env map[string]string kvstore map[string]interface{}
aliases map[string]string cascadingConfigs map[string]map[string]interface{}
pflags map[string]*pflag.Flag
env map[string]string
aliases map[string]string
} }
// Returns an initialized Viper instance. // Returns an initialized Viper instance.
@ -154,6 +157,7 @@ func New() *Viper {
v.pflags = make(map[string]*pflag.Flag) v.pflags = make(map[string]*pflag.Flag)
v.env = make(map[string]string) v.env = make(map[string]string)
v.aliases = make(map[string]string) v.aliases = make(map[string]string)
v.cascadeConfigurations = false
return v return v
} }
@ -226,6 +230,13 @@ func (v *Viper) SetEnvPrefix(in string) {
} }
} }
// Enable cascading configuration values for files. Will traverse down
// ConfigPaths in an attempt to find keys
func EnableCascading(enable bool) { v.EnableCascading(enable) }
func (v *Viper) EnableCascading(enable bool) {
v.cascadeConfigurations = enable
}
func (v *Viper) mergeWithEnvPrefix(in string) string { func (v *Viper) mergeWithEnvPrefix(in string) string {
if v.envPrefix != "" { if v.envPrefix != "" {
return strings.ToUpper(v.envPrefix + "_" + in) return strings.ToUpper(v.envPrefix + "_" + in)
@ -474,7 +485,6 @@ func (v *Viper) MarshalKey(key string, rawVal interface{}) error {
func Marshal(rawVal interface{}) error { return v.Marshal(rawVal) } func Marshal(rawVal interface{}) error { return v.Marshal(rawVal) }
func (v *Viper) Marshal(rawVal interface{}) error { func (v *Viper) Marshal(rawVal interface{}) error {
err := mapstructure.WeakDecode(v.AllSettings(), rawVal) err := mapstructure.WeakDecode(v.AllSettings(), rawVal)
if err != nil { if err != nil {
return err return err
} }
@ -562,6 +572,7 @@ func (v *Viper) BindEnv(input ...string) (err error) {
func (v *Viper) find(key string) interface{} { func (v *Viper) find(key string) interface{} {
var val interface{} var val interface{}
var exists bool var exists bool
var file string
// if the requested key is an alias, then return the proper key // if the requested key is an alias, then return the proper key
key = v.realKey(key) key = v.realKey(key)
@ -607,6 +618,15 @@ func (v *Viper) find(key string) interface{} {
return val return val
} }
if v.cascadeConfigurations {
//cascade down the rest of the files
val, exists, file = v.findCascading(key)
if exists {
jww.TRACE.Printf("%s found in config: %s (%s)", key, val, file)
return val
}
}
val, exists = v.kvstore[key] val, exists = v.kvstore[key]
if exists { if exists {
jww.TRACE.Println(key, "found in key/value store:", val) jww.TRACE.Println(key, "found in key/value store:", val)
@ -622,6 +642,53 @@ func (v *Viper) find(key string) interface{} {
return nil return nil
} }
func (v *Viper) findCascading(key string) (interface{}, bool, string) {
configFiles := v.findAllConfigFiles()
if v.cascadingConfigs == nil {
v.cascadingConfigs = make(map[string]map[string]interface{})
}
for _, configFile := range configFiles {
config := v.cascadingConfigs[configFile]
if config == nil {
var err error
config, err = readConfigFile(configFile)
if err != nil {
jww.ERROR.Print(err)
continue
}
v.cascadingConfigs[configFile] = config
}
jww.TRACE.Printf("Looking in %s for key %s", configFile, key)
result := config[key]
if result != nil {
return result, true, configFile
}
}
return "", false, ""
}
func readConfigFile(configFile string) (map[string]interface{}, error) {
jww.TRACE.Printf("marshalling %s ", configFile)
file, err := ioutil.ReadFile(configFile)
if err != nil {
return nil, err
}
var config = make(map[string]interface{})
marshallConfigReader(bytes.NewReader(file), config, filepath.Ext(configFile)[1:])
return config, nil
}
// Check to see if the key has been set in any of the data locations // Check to see if the key has been set in any of the data locations
func IsSet(key string) bool { return v.IsSet(key) } func IsSet(key string) bool { return v.IsSet(key) }
func (v *Viper) IsSet(key string) bool { func (v *Viper) IsSet(key string) bool {
@ -986,10 +1053,23 @@ func (v *Viper) searchInPath(in string) (filename string) {
func (v *Viper) findConfigFile() (string, error) { func (v *Viper) findConfigFile() (string, error) {
jww.INFO.Println("Searching for config in ", v.configPaths) jww.INFO.Println("Searching for config in ", v.configPaths)
var validFiles = v.findAllConfigFiles()
if len(validFiles) == 0 {
return "", fmt.Errorf("config file not found in: %s", v.configPaths)
}
return validFiles[0], nil
}
func (v *Viper) findAllConfigFiles() []string {
var validFiles []string
for _, cp := range v.configPaths { for _, cp := range v.configPaths {
file := v.searchInPath(cp) file := v.searchInPath(cp)
if file != "" { if file != "" {
return file, nil jww.TRACE.Println("Found config file in: %s", file)
validFiles = append(validFiles, file)
} }
} }
@ -997,9 +1077,10 @@ func (v *Viper) findConfigFile() (string, error) {
wd, _ := os.Getwd() wd, _ := os.Getwd()
file := v.searchInPath(wd) file := v.searchInPath(wd)
if file != "" { if file != "" {
return file, nil validFiles = append(validFiles, file)
} }
return "", fmt.Errorf("config file not found in: %s", v.configPaths)
return validFiles
} }
// Prints all configuration registries for debugging // Prints all configuration registries for debugging

View file

@ -10,6 +10,8 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec"
"path"
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
@ -587,3 +589,106 @@ func TestReadDir(t *testing.T) {
assert.Equal(t, "https://donuts/api/1/", v.Get("app.service.url")) assert.Equal(t, "https://donuts/api/1/", v.Get("app.service.url"))
assert.Equal(t, "0123456789abcdef", v.Get("app.service.key")) assert.Equal(t, "0123456789abcdef", v.Get("app.service.key"))
} }
func TestCanCascadeConfigurationValues(t *testing.T) {
v2 := New()
generateCascadingTests(v2, "cascading")
v2.ReadInConfig()
v2.EnableCascading(true)
assert.Equal(t, "high", v2.GetString("0"), "Key 0 should be high")
assert.Equal(t, "med", v2.GetString("1"), "Key 1 should be med")
assert.Equal(t, "low", v2.GetString("2"), "key 2 should be low")
v2.EnableCascading(false)
assert.Nil(t, v2.Get("1"), "With enable cascading disabled, no value for 1 should exist")
assert.Nil(t, v2.Get("2"), "With enable cascading disabled, no value for 2 should exist")
}
func TestFindAllConfigPaths(t *testing.T) {
v2 := New()
file := "viper_test"
var expected = generateCascadingTests(v2, file)
found := v2.findAllConfigFiles()
for _, fp := range expected {
command := exec.Command("rm", fp)
command.Run()
}
assert.Equal(t, expected, removeDuplicates(found), "All files should exist")
}
func generateCascadingTests(v2 *Viper, file_name string) []string {
v2.SetConfigName(file_name)
tmp := os.Getenv("TMPDIR")
if tmp == "" {
tmp, _ = filepath.Abs(filepath.Dir("./"))
}
// $TMPDIR/a > $TMPDIR/b > %TMPDIR
paths := []string{path.Join(tmp, "a"), path.Join(tmp, "b"), tmp}
v2.SetConfigName(file_name)
var expected []string
for idx, fp := range paths {
v2.AddConfigPath(fp)
exec.Command("mkdir", "-m", "777", fp).Run()
full_path := path.Join(fp, file_name+".json")
var val string
switch idx {
case 0:
val = "high"
break
case 1:
val = "med"
break
case 2:
val = "low"
}
config := "{"
for i := 0; i <= idx; i++ {
config += fmt.Sprintf("\"%d\": \"%s\"", i, val)
if i == idx {
config += "\n"
} else {
config += ",\n"
}
}
config += "}"
ioutil.WriteFile(full_path, []byte(config), 0777)
expected = append(expected, full_path)
}
return expected
}
func removeDuplicates(a []string) []string {
result := []string{}
seen := map[string]string{}
for _, val := range a {
if _, ok := seen[val]; !ok {
result = append(result, val)
seen[val] = val
}
}
return result
}