diff --git a/.gitignore b/.gitignore index 8365624..31ec60d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ # Folders _obj _test +.idea # Architecture specific extensions/prefixes *.[568vq] diff --git a/README.md b/README.md index 76e1071..bcf2764 100644 --- a/README.md +++ b/README.md @@ -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. ``` { diff --git a/viper.go b/viper.go index db0bd26..ad711e1 100644 --- a/viper.go +++ b/viper.go @@ -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 diff --git a/viper_test.go b/viper_test.go index de276f5..9fafb59 100644 --- a/viper_test.go +++ b/viper_test.go @@ -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 +}