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
@ -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.

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
}