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
@ -84,14 +84,14 @@ currently a single viper only supports a single config file.
### 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
require configuration and feed it to viper.
````go
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(`
Hacker: true
name: steve
@ -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.
@ -270,7 +286,7 @@ to use Consul.
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
runtime_viper.Marshal(&runtime_conf)
}
@ -307,7 +323,7 @@ Example:
### 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:
```
@ -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.
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
envKeyReplacer *strings.Replacer
config map[string]interface{}
override map[string]interface{}
defaults map[string]interface{}
kvstore map[string]interface{}
pflags map[string]*pflag.Flag
env map[string]string
aliases map[string]string
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
}
// Returns an initialized Viper instance.
@ -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
}