From 991d18afb2404beca5bc475995bf76f38729cc8f Mon Sep 17 00:00:00 2001 From: akutz Date: Thu, 12 Nov 2015 14:20:40 -0600 Subject: [PATCH] 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`. --- viper.go | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++ viper_test.go | 98 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 210 insertions(+) diff --git a/viper.go b/viper.go index b253c75..7c98c6f 100644 --- a/viper.go +++ b/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{}) diff --git a/viper_test.go b/viper_test.go index 0c71549..e9bb3f4 100644 --- a/viper_test.go +++ b/viper_test.go @@ -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) + } +}