mirror of https://github.com/spf13/viper.git
Adds MergeConfig functionality
This patch adds the `MergeConfig` and `MergeInConfig` functions to enable reading new configuration files via a merge strategy rather than replace. For example, take the following as the base YAML for a configuration: hello: pop: 37890 world: - us - uk - fr - de Now imagine we want to read the following, new configuration data: hello: pop: 45000 universe: - mw - ad fu: bar Using the standard `ReadConfig` function the value returned by the nested key `hello.world` would no longer be present after the second configuration is read. This is because the `ReadConfig` function and its relatives replace nested structures entirely. The new `MergeConfig` function would produce the following config after the second YAML snippet was merged with the first: hello: pop: 45000 world: - us - uk - fr - de universe: - mw - ad fu: bar Examples showing how this works can be found in the two unit tests named `TestMergeConfig` and `TestMergeConfigNoMerge`.
This commit is contained in:
parent
c8c6312ace
commit
991d18afb2
112
viper.go
112
viper.go
|
@ -901,12 +901,124 @@ func (v *Viper) ReadInConfig() error {
|
|||
return v.unmarshalReader(bytes.NewReader(file), v.config)
|
||||
}
|
||||
|
||||
// MergeInConfig merges a new configuration with an existing config.
|
||||
func MergeInConfig() error { return v.MergeInConfig() }
|
||||
func (v *Viper) MergeInConfig() error {
|
||||
jww.INFO.Println("Attempting to merge in config file")
|
||||
if !stringInSlice(v.getConfigType(), SupportedExts) {
|
||||
return UnsupportedConfigError(v.getConfigType())
|
||||
}
|
||||
|
||||
file, err := ioutil.ReadFile(v.getConfigFile())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return v.MergeConfig(bytes.NewReader(file))
|
||||
}
|
||||
|
||||
// Viper will read a configuration file, setting existing keys to nil if the
|
||||
// key does not exist in the file.
|
||||
func ReadConfig(in io.Reader) error { return v.ReadConfig(in) }
|
||||
func (v *Viper) ReadConfig(in io.Reader) error {
|
||||
v.config = make(map[string]interface{})
|
||||
return v.unmarshalReader(in, v.config)
|
||||
}
|
||||
|
||||
// MergeConfig merges a new configuration with an existing config.
|
||||
func MergeConfig(in io.Reader) error { return v.MergeConfig(in) }
|
||||
func (v *Viper) MergeConfig(in io.Reader) error {
|
||||
if v.config == nil {
|
||||
v.config = make(map[string]interface{})
|
||||
}
|
||||
cfg := make(map[string]interface{})
|
||||
if err := v.unmarshalReader(in, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
mergeMaps(cfg, v.config, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func keyExists(k string, m map[string]interface{}) string {
|
||||
lk := strings.ToLower(k)
|
||||
for mk := range m {
|
||||
lmk := strings.ToLower(mk)
|
||||
if lmk == lk {
|
||||
return mk
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func castToMapStringInterface(
|
||||
src map[interface{}]interface{}) map[string]interface{} {
|
||||
tgt := map[string]interface{}{}
|
||||
for k, v := range src {
|
||||
tgt[fmt.Sprintf("%v", k)] = v
|
||||
}
|
||||
return tgt
|
||||
}
|
||||
|
||||
// mergeMaps merges two maps. The `itgt` parameter is for handling go-yaml's
|
||||
// insistence on parsing nested structures as `map[interface{}]interface{}`
|
||||
// instead of using a `string` as the key for nest structures beyond one level
|
||||
// deep. Both map types are supported as there is a go-yaml fork that uses
|
||||
// `map[string]interface{}` instead.
|
||||
func mergeMaps(
|
||||
src, tgt map[string]interface{}, itgt map[interface{}]interface{}) {
|
||||
for sk, sv := range src {
|
||||
tk := keyExists(sk, tgt)
|
||||
if tk == "" {
|
||||
jww.TRACE.Printf("tk=\"\", tgt[%s]=%v", sk, sv)
|
||||
tgt[sk] = sv
|
||||
if itgt != nil {
|
||||
itgt[sk] = sv
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
tv, ok := tgt[tk]
|
||||
if !ok {
|
||||
jww.TRACE.Printf("tgt[%s] != ok, tgt[%s]=%v", tk, sk, sv)
|
||||
tgt[sk] = sv
|
||||
if itgt != nil {
|
||||
itgt[sk] = sv
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
svType := reflect.TypeOf(sv)
|
||||
tvType := reflect.TypeOf(tv)
|
||||
if svType != tvType {
|
||||
jww.ERROR.Printf(
|
||||
"svType != tvType; key=%s, st=%v, tt=%v, sv=%v, tv=%v",
|
||||
sk, svType, tvType, sv, tv)
|
||||
continue
|
||||
}
|
||||
|
||||
jww.TRACE.Printf("processing key=%s, st=%v, tt=%v, sv=%v, tv=%v",
|
||||
sk, svType, tvType, sv, tv)
|
||||
|
||||
switch ttv := tv.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
jww.TRACE.Printf("merging maps (must convert)")
|
||||
tsv := sv.(map[interface{}]interface{})
|
||||
ssv := castToMapStringInterface(tsv)
|
||||
stv := castToMapStringInterface(ttv)
|
||||
mergeMaps(ssv, stv, ttv)
|
||||
case map[string]interface{}:
|
||||
jww.TRACE.Printf("merging maps")
|
||||
mergeMaps(sv.(map[string]interface{}), ttv, nil)
|
||||
default:
|
||||
jww.TRACE.Printf("setting value")
|
||||
tgt[tk] = sv
|
||||
if itgt != nil {
|
||||
itgt[tk] = sv
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// func ReadBufConfig(buf *bytes.Buffer) error { return v.ReadBufConfig(buf) }
|
||||
// func (v *Viper) ReadBufConfig(buf *bytes.Buffer) error {
|
||||
// v.config = make(map[string]interface{})
|
||||
|
|
|
@ -737,3 +737,101 @@ func TestSub(t *testing.T) {
|
|||
assert.Equal(t, subv, (*Viper)(nil))
|
||||
}
|
||||
|
||||
var yamlMergeExampleTgt = []byte(`
|
||||
hello:
|
||||
pop: 37890
|
||||
world:
|
||||
- us
|
||||
- uk
|
||||
- fr
|
||||
- de
|
||||
`)
|
||||
|
||||
var yamlMergeExampleSrc = []byte(`
|
||||
hello:
|
||||
pop: 45000
|
||||
universe:
|
||||
- mw
|
||||
- ad
|
||||
fu: bar
|
||||
`)
|
||||
|
||||
func TestMergeConfig(t *testing.T) {
|
||||
v := New()
|
||||
v.SetConfigType("yml")
|
||||
if err := v.ReadConfig(bytes.NewBuffer(yamlMergeExampleTgt)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if pop := v.GetInt("hello.pop"); pop != 37890 {
|
||||
t.Fatalf("pop != 37890, = %d", pop)
|
||||
}
|
||||
|
||||
if world := v.GetStringSlice("hello.world"); len(world) != 4 {
|
||||
t.Fatalf("len(world) != 4, = %d", len(world))
|
||||
}
|
||||
|
||||
if fu := v.GetString("fu"); fu != "" {
|
||||
t.Fatalf("fu != \"\", = %s", fu)
|
||||
}
|
||||
|
||||
if err := v.MergeConfig(bytes.NewBuffer(yamlMergeExampleSrc)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if pop := v.GetInt("hello.pop"); pop != 45000 {
|
||||
t.Fatalf("pop != 45000, = %d", pop)
|
||||
}
|
||||
|
||||
if world := v.GetStringSlice("hello.world"); len(world) != 4 {
|
||||
t.Fatalf("len(world) != 4, = %d", len(world))
|
||||
}
|
||||
|
||||
if universe := v.GetStringSlice("hello.universe"); len(universe) != 2 {
|
||||
t.Fatalf("len(universe) != 2, = %d", len(universe))
|
||||
}
|
||||
|
||||
if fu := v.GetString("fu"); fu != "bar" {
|
||||
t.Fatalf("fu != \"bar\", = %s", fu)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeConfigNoMerge(t *testing.T) {
|
||||
v := New()
|
||||
v.SetConfigType("yml")
|
||||
if err := v.ReadConfig(bytes.NewBuffer(yamlMergeExampleTgt)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if pop := v.GetInt("hello.pop"); pop != 37890 {
|
||||
t.Fatalf("pop != 37890, = %d", pop)
|
||||
}
|
||||
|
||||
if world := v.GetStringSlice("hello.world"); len(world) != 4 {
|
||||
t.Fatalf("len(world) != 4, = %d", len(world))
|
||||
}
|
||||
|
||||
if fu := v.GetString("fu"); fu != "" {
|
||||
t.Fatalf("fu != \"\", = %s", fu)
|
||||
}
|
||||
|
||||
if err := v.ReadConfig(bytes.NewBuffer(yamlMergeExampleSrc)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if pop := v.GetInt("hello.pop"); pop != 45000 {
|
||||
t.Fatalf("pop != 45000, = %d", pop)
|
||||
}
|
||||
|
||||
if world := v.GetStringSlice("hello.world"); len(world) != 0 {
|
||||
t.Fatalf("len(world) != 0, = %d", len(world))
|
||||
}
|
||||
|
||||
if universe := v.GetStringSlice("hello.universe"); len(universe) != 2 {
|
||||
t.Fatalf("len(universe) != 2, = %d", len(universe))
|
||||
}
|
||||
|
||||
if fu := v.GetString("fu"); fu != "bar" {
|
||||
t.Fatalf("fu != \"bar\", = %s", fu)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue