mirror of
https://github.com/spf13/viper
synced 2024-12-23 03:57:01 +00:00
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
e37b56e207
commit
a9f31dbe0c
2 changed files with 211 additions and 0 deletions
112
viper.go
112
viper.go
|
@ -858,12 +858,124 @@ func (v *Viper) ReadInConfig() error {
|
||||||
return v.unmarshalReader(bytes.NewReader(file), v.config)
|
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 ReadConfig(in io.Reader) error { return v.ReadConfig(in) }
|
||||||
func (v *Viper) ReadConfig(in io.Reader) error {
|
func (v *Viper) ReadConfig(in io.Reader) error {
|
||||||
v.config = make(map[string]interface{})
|
v.config = make(map[string]interface{})
|
||||||
return v.unmarshalReader(in, v.config)
|
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 ReadBufConfig(buf *bytes.Buffer) error { return v.ReadBufConfig(buf) }
|
||||||
// func (v *Viper) ReadBufConfig(buf *bytes.Buffer) error {
|
// func (v *Viper) ReadBufConfig(buf *bytes.Buffer) error {
|
||||||
// v.config = make(map[string]interface{})
|
// v.config = make(map[string]interface{})
|
||||||
|
|
|
@ -652,3 +652,102 @@ func TestWrongDirsSearchNotFound(t *testing.T) {
|
||||||
// been ignored by the client, the default still loads
|
// been ignored by the client, the default still loads
|
||||||
assert.Equal(t, `default`, v.GetString(`key`))
|
assert.Equal(t, `default`, v.GetString(`key`))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 a new issue