diff --git a/README.md b/README.md index db4ed13..d1cefa8 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,11 @@ ### Latest release -[Release v0.3.3](https://github.com/imdario/mergo/releases/tag/v0.3.3). +[Release v0.3.4](https://github.com/imdario/mergo/releases/tag/v0.3.4). ### Important note -Please keep in mind that in [v0.3.2](//github.com/imdario/mergo/releases/tag/v0.3.2) Mergo changed `Merge()`and `Map()` signatures to support [transformers](#transformers). An optional/variadic argument has been added, so it won't break existing code. +Please keep in mind that in [0.3.2](//github.com/imdario/mergo/releases/tag/0.3.2) Mergo changed `Merge()`and `Map()` signatures to support [transformers](#transformers). An optional/variadic argument has been added, so it won't break existing code. If you were using Mergo **before** April 6th 2015, please check your project works as intended after updating your local copy with ```go get -u github.com/imdario/mergo```. I apologize for any issue caused by its previous behavior and any future bug that Mergo could cause (I hope it won't!) in existing projects after the change (release 0.2.0). diff --git a/issue66_test.go b/issue66_test.go index 23fa5e2..9e4bcce 100644 --- a/issue66_test.go +++ b/issue66_test.go @@ -20,6 +20,25 @@ if err := Merge(&p1, p2); err != nil { t.Fatalf("Error during the merge: %v", err) } + if len(p1.PublicStrings) != 3 { + t.Error("5 elements should be in 'PublicStrings' field") + } + if len(p1.privateStrings) != 2 { + t.Error("2 elements should be in 'privateStrings' field") + } +} + +func TestPrivateSliceWithAppendSlice(t *testing.T) { + p1 := PrivateSliceTest66{ + PublicStrings: []string{"one", "two", "three"}, + privateStrings: []string{"four", "five"}, + } + p2 := PrivateSliceTest66{ + PublicStrings: []string{"six", "seven"}, + } + if err := Merge(&p1, p2, WithAppendSlice); err != nil { + t.Fatalf("Error during the merge: %v", err) + } if len(p1.PublicStrings) != 5 { t.Error("5 elements should be in 'PublicStrings' field") } diff --git a/merge.go b/merge.go index 2a308b4..706b220 100644 --- a/merge.go +++ b/merge.go @@ -26,6 +26,7 @@ type Config struct { Overwrite bool + AppendSlice bool Transformers Transformers } @@ -102,7 +103,15 @@ case reflect.Ptr: fallthrough case reflect.Map: - if err = deepMerge(dstElement, srcElement, visited, depth+1, config); err != nil { + srcMapElm := srcElement + dstMapElm := dstElement + if srcMapElm.CanInterface() { + srcMapElm = reflect.ValueOf(srcMapElm.Interface()) + if dstMapElm.IsValid() { + dstMapElm = reflect.ValueOf(dstMapElm.Interface()) + } + } + if err = deepMerge(dstMapElm, srcMapElm, visited, depth+1, config); err != nil { return } case reflect.Slice: @@ -115,7 +124,11 @@ dstSlice = reflect.ValueOf(dstElement.Interface()) } - dstSlice = reflect.AppendSlice(dstSlice, srcSlice) + if !isEmptyValue(src) && (overwrite || isEmptyValue(dst)) && !config.AppendSlice { + dstSlice = srcSlice + } else if config.AppendSlice { + dstSlice = reflect.AppendSlice(dstSlice, srcSlice) + } dst.SetMapIndex(key, dstSlice) } } @@ -123,7 +136,7 @@ continue } - if srcElement.IsValid() && (overwrite || (!dstElement.IsValid() || isEmptyValue(dst))) { + if srcElement.IsValid() && (overwrite || (!dstElement.IsValid() || isEmptyValue(dstElement))) { if dst.IsNil() { dst.Set(reflect.MakeMap(dst.Type())) } @@ -134,9 +147,9 @@ if !dst.CanSet() { break } - if !isEmptyValue(src) && (overwrite || isEmptyValue(dst)) { + if !isEmptyValue(src) && (overwrite || isEmptyValue(dst)) && !config.AppendSlice { dst.Set(src) - } else { + } else if config.AppendSlice { dst.Set(reflect.AppendSlice(dst, src)) } case reflect.Ptr: @@ -205,6 +218,11 @@ config.Overwrite = true } +// WithAppendSlice will make merge append slices instead of overwriting it +func WithAppendSlice(config *Config) { + config.AppendSlice = true +} + func merge(dst, src interface{}, opts ...func(*Config)) error { var ( vDst, vSrc reflect.Value diff --git a/merge_appendslice_test.go b/merge_appendslice_test.go new file mode 100644 index 0000000..a780f34 --- /dev/null +++ b/merge_appendslice_test.go @@ -0,0 +1,33 @@ +package mergo + +import ( + "testing" +) + +var testDataS = []struct { + S1 Student + S2 Student + ExpectedSlice []string +}{ + {Student{"Jack", []string{"a", "B"}}, Student{"Tom", []string{"1"}}, []string{"1", "a", "B"}}, + {Student{"Jack", []string{"a", "B"}}, Student{"Tom", []string{}}, []string{"a", "B"}}, + {Student{"Jack", []string{}}, Student{"Tom", []string{"1"}}, []string{"1"}}, + {Student{"Jack", []string{}}, Student{"Tom", []string{}}, []string{}}, +} + +func TestMergeSliceWithOverrideWithAppendSlice(t *testing.T) { + for _, data := range testDataS { + err := Merge(&data.S2, data.S1, WithOverride, WithAppendSlice) + if err != nil { + t.Errorf("Error while merging %s", err) + } + if len(data.S2.Books) != len(data.ExpectedSlice) { + t.Fatalf("Got %d elements in slice, but expected %d", len(data.S2.Books), len(data.ExpectedSlice)) + } + for i, val := range data.S2.Books { + if val != data.ExpectedSlice[i] { + t.Fatalf("Expected %s, but got %s while merging slice with override", data.ExpectedSlice[i], val) + } + } + } +} diff --git a/mergo.go b/mergo.go index 785618c..a82fea2 100644 --- a/mergo.go +++ b/mergo.go @@ -45,7 +45,12 @@ return v.Uint() == 0 case reflect.Float32, reflect.Float64: return v.Float() == 0 - case reflect.Interface, reflect.Ptr, reflect.Func: + case reflect.Interface, reflect.Ptr: + if v.IsNil() { + return true + } + return isEmptyValue(v.Elem()) + case reflect.Func: return v.IsNil() case reflect.Invalid: return true diff --git a/mergo_test.go b/mergo_test.go index 8545138..d777538 100644 --- a/mergo_test.go +++ b/mergo_test.go @@ -6,11 +6,12 @@ package mergo import ( - "gopkg.in/yaml.v2" "io/ioutil" "reflect" "testing" "time" + + "gopkg.in/yaml.v2" ) type simpleTest struct { @@ -225,13 +226,13 @@ } } -func testSlice(t *testing.T, a []int, b []int) { +func testSlice(t *testing.T, a []int, b []int, e []int, opts ...func(*Config)) { + t.Helper() bc := b - e := append(a, b...) sa := sliceTest{a} sb := sliceTest{b} - if err := Merge(&sa, sb); err != nil { + if err := Merge(&sa, sb, opts...); err != nil { t.FailNow() } if !reflect.DeepEqual(sb.S, bc) { @@ -243,14 +244,14 @@ ma := map[string][]int{"S": a} mb := map[string][]int{"S": b} - if err := Merge(&ma, mb); err != nil { + if err := Merge(&ma, mb, opts...); err != nil { t.FailNow() } if !reflect.DeepEqual(mb["S"], bc) { - t.Fatalf("Source slice was modified %d != %d", mb["S"], bc) + t.Fatalf("map value: Source slice was modified %d != %d", mb["S"], bc) } if !reflect.DeepEqual(ma["S"], e) { - t.Fatalf("b not merged in a proper way %d != %d", ma["S"], e) + t.Fatalf("map value: b not merged in a proper way %d != %d", ma["S"], e) } if a == nil { @@ -261,10 +262,10 @@ t.FailNow() } if !reflect.DeepEqual(mb["S"], bc) { - t.Fatalf("Source slice was modified %d != %d", mb["S"], bc) + t.Fatalf("missing dst key: Source slice was modified %d != %d", mb["S"], bc) } if !reflect.DeepEqual(ma["S"], e) { - t.Fatalf("b not merged in a proper way %d != %d", ma["S"], e) + t.Fatalf("missing dst key: b not merged in a proper way %d != %d", ma["S"], e) } } @@ -276,20 +277,25 @@ t.FailNow() } if !reflect.DeepEqual(mb["S"], bc) { - t.Fatalf("Source slice was modified %d != %d", mb["S"], bc) + t.Fatalf("missing src key: Source slice was modified %d != %d", mb["S"], bc) } if !reflect.DeepEqual(ma["S"], e) { - t.Fatalf("b not merged in a proper way %d != %d", ma["S"], e) + t.Fatalf("missing src key: b not merged in a proper way %d != %d", ma["S"], e) } } } func TestSlice(t *testing.T) { - testSlice(t, nil, []int{1, 2, 3}) - testSlice(t, []int{}, []int{1, 2, 3}) - testSlice(t, []int{1}, []int{2, 3}) - testSlice(t, []int{1}, []int{}) - testSlice(t, []int{1}, nil) + testSlice(t, nil, []int{1, 2, 3}, []int{1, 2, 3}) + testSlice(t, []int{}, []int{1, 2, 3}, []int{1, 2, 3}) + testSlice(t, []int{1}, []int{2, 3}, []int{1}) + testSlice(t, []int{1}, []int{}, []int{1}) + testSlice(t, []int{1}, nil, []int{1}) + testSlice(t, nil, []int{1, 2, 3}, []int{1, 2, 3}, WithAppendSlice) + testSlice(t, []int{}, []int{1, 2, 3}, []int{1, 2, 3}, WithAppendSlice) + testSlice(t, []int{1}, []int{2, 3}, []int{1, 2, 3}, WithAppendSlice) + testSlice(t, []int{1}, []int{}, []int{1}, WithAppendSlice) + testSlice(t, []int{1}, nil, []int{1}, WithAppendSlice) } func TestEmptyMaps(t *testing.T) { diff --git a/pr80_test.go b/pr80_test.go new file mode 100644 index 0000000..0b3220f --- /dev/null +++ b/pr80_test.go @@ -0,0 +1,18 @@ +package mergo + +import ( + "testing" +) + +type mapInterface map[string]interface{} + +func TestMergeMapsEmptyString(t *testing.T) { + a := mapInterface{"s": ""} + b := mapInterface{"s": "foo"} + if err := Merge(&a, b); err != nil { + t.Fatal(err) + } + if a["s"] != "foo" { + t.Fatalf("b not merged in properly: a.s.Value(%s) != expected(%s)", a["s"], "foo") + } +} diff --git a/pr81_test.go b/pr81_test.go new file mode 100644 index 0000000..e90e923 --- /dev/null +++ b/pr81_test.go @@ -0,0 +1,42 @@ +package mergo + +import ( + "testing" +) + +func TestMapInterfaceWithMultipleLayer(t *testing.T) { + m1 := map[string]interface{}{ + "k1": map[string]interface{}{ + "k1.1": "v1", + }, + } + + m2 := map[string]interface{}{ + "k1": map[string]interface{}{ + "k1.1": "v2", + "k1.2": "v3", + }, + } + + if err := Map(&m1, m2, WithOverride); err != nil { + t.Fatalf("Error merging: %v", err) + } + + // Check overwrite of sub map works + expected := "v2" + actual := m1["k1"].(map[string]interface{})["k1.1"].(string) + if actual != expected { + t.Fatalf("Expected %v but got %v", + expected, + actual) + } + + // Check new key is merged + expected = "v3" + actual = m1["k1"].(map[string]interface{})["k1.2"].(string) + if actual != expected { + t.Fatalf("Expected %v but got %v", + expected, + actual) + } +}