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
_obj
_test
.idea
# Architecture specific extensions/prefixes
*.[568vq]

View file

@ -72,7 +72,7 @@ Examples:
If you want to support a config file, Viper requires a minimal
configuration so it knows where to look for the config file. Viper
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.AddConfigPath("/etc/appname/") // path to look for the config file in
@ -112,6 +112,22 @@ viper.ReadConfig(bytes.NewBuffer(yamlExample))
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
These could be from a command line flag, or from your own application logic.

View file

@ -133,10 +133,13 @@ type Viper struct {
automaticEnvApplied bool
envKeyReplacer *strings.Replacer
cascadeConfigurations bool
config map[string]interface{}
override map[string]interface{}
defaults map[string]interface{}
kvstore map[string]interface{}
cascadingConfigs map[string]map[string]interface{}
pflags map[string]*pflag.Flag
env map[string]string
aliases map[string]string
@ -154,6 +157,7 @@ func New() *Viper {
v.pflags = make(map[string]*pflag.Flag)
v.env = make(map[string]string)
v.aliases = make(map[string]string)
v.cascadeConfigurations = false
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 {
if v.envPrefix != "" {
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 (v *Viper) Marshal(rawVal interface{}) error {
err := mapstructure.WeakDecode(v.AllSettings(), rawVal)
if err != nil {
return err
}
@ -562,6 +572,7 @@ func (v *Viper) BindEnv(input ...string) (err error) {
func (v *Viper) find(key string) interface{} {
var val interface{}
var exists bool
var file string
// if the requested key is an alias, then return the proper key
key = v.realKey(key)
@ -607,6 +618,15 @@ func (v *Viper) find(key string) interface{} {
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]
if exists {
jww.TRACE.Println(key, "found in key/value store:", val)
@ -622,6 +642,53 @@ func (v *Viper) find(key string) interface{} {
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
func IsSet(key string) bool { return v.IsSet(key) }
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) {
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 {
file := v.searchInPath(cp)
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()
file := v.searchInPath(wd)
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

View file

@ -10,6 +10,8 @@ import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"sort"
"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, "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
}