Skip to content

Commit

Permalink
Squashed commit of the following:
Browse files Browse the repository at this point in the history
commit 64219f269280df8ad7c8abf48c3a33697154e70a
Author: Dario Castañé <d@rio.hn>
Date:   Sat Jul 18 00:11:30 2020 +0200

    Issue #123 reverted

commit 1c0f4c5e64026215964a7365cc2da1750bf44f31
Author: János Pásztor <janoszen@users.noreply.github.com>
Date:   Mon Jun 8 08:42:20 2020 +0200

    Added janoszen/containerssh as  a mergo user

commit 36e7eb818ccb22432f4c0824f820c5c832b70538
Author: Dario Castañé <d@rio.hn>
Date:   Sat Jul 18 00:09:18 2020 +0200

    Fatal* replaced by Error* functions

commit a3badb1749efbc887cf881cf78eba881c0864773
Author: Dario Castañé <i@dario.im>
Date:   Sun May 17 16:59:56 2020 +0200

    Improved semantics of tests for issue #129

commit e7166a5d1ccf985ad9077a4ce23edc0fae0d7378
Author: Dario Castañé <i@dario.im>
Date:   Sun May 17 16:52:27 2020 +0200

    Issue #129 closed

commit 4c6b27f7c7eacc5575f1eb454419258e8c92c2d1
Author: Dario Castañé <i@dario.im>
Date:   Sun May 17 16:40:14 2020 +0200

    Issue #89 closed

commit 293f485af2dce978b4fd27b65789b5e4a92df4fd
Author: Dario Castañé <i@dario.im>
Date:   Sun May 17 16:36:55 2020 +0200

    Improved name for the final condition to set

commit a4db553223c3a1ba8de4035e1110133ec3c5ae48
Author: Dario Castañé <i@dario.im>
Date:   Sun May 17 16:06:36 2020 +0200

    Issue #138 fixed

commit 8583b70567e0f59382ca76f0bbe999470e8982df
Author: Dario Castañé <i@dario.im>
Date:   Sun May 17 14:58:37 2020 +0200

    Issue #136 resolved

commit f34173f37dbe977477feb95cf3cba1cfb3919630
Author: Dario Castañé <d@rio.hn>
Date:   Fri Jul 17 23:50:34 2020 +0200

    Issue #131 fixed again

commit 1fdd4e8dbae7c1b8c88d550aab142f32bc3d0c86
Author: Dario Castañé <i@dario.im>
Date:   Sun May 17 14:47:55 2020 +0200

    Issue 131 fixed

commit 2fbb87d2f9fbdb47fdf6d2d4274ec8a05ca698c2
Author: Tariq Ibrahim <tariq181290@gmail.com>
Date:   Thu Apr 23 12:49:33 2020 -0700

    add release badge to track latest release of mergo

commit 815b47be4da2a8dcb87ab36544466b89c1c0e40d
Author: Eyal <eyalsoha@google.com>
Date:   Tue Apr 14 08:57:57 2020 -0400

    Fix typo transfomer -> transformer

commit 6ed75f818a997495ab9eb9d474f4bc13bad3e5bc
Author: Dario Castañé <dcastane@loyal.guru>
Date:   Fri Mar 27 08:34:25 2020 +0100

    README updated to last release v0.3..9

commit 9b0f1c7e5c418675c7f9d1ab588b8bf6b29ac1ff
Author: Dario Castañé <d@rio.hn>
Date:   Fri Jul 17 23:28:50 2020 +0200

    Improved semantics for mergeable fields

commit cf80c4cbd6102982dd91eb0ded30558692a3f43d
Author: Dario Castañé <d@rio.hn>
Date:   Fri Jul 17 23:18:59 2020 +0200

    Faulty test removed

commit 5a49eb5583bd3ebdae0afe011a66ae5365196918
Author: Dario Castañé <d@rio.hn>
Date:   Fri Jul 17 23:10:01 2020 +0200

    Working on CI fixes and v0.3.9 bugs

commit b75bbeef3b546d3fb916271538adc25bbde20f8d
Author: Dario Castañé <dcastane@loyal.guru>
Date:   Thu Mar 26 22:43:59 2020 +0100

    Dead code removed

commit c08f60a65adf317d234f1f5eb51572c30c9b87d0
Author: Dario Castañé <dcastane@loyal.guru>
Date:   Thu Mar 26 22:43:50 2020 +0100

    Destination pointer type check added to map

commit e5e7507289bba3e635e6a9df7671d58915ac0487
Author: Stefan Bourlon <s.bourlon@gmail.com>
Date:   Mon Feb 3 11:51:09 2020 -0800

    merge: test dst is a pointer

    Return an error instead of panicking if merge receives dst != pointer

commit b8a2f9bd2feb2110ee3cb550372cc4105ebcaa99
Author: Umair Idris <umair.m.idris@gmail.com>
Date:   Mon Jan 20 05:21:09 2020 -0500

    Fix lint (#135)

commit 3cebbeca61cb9226249551e9c8afc24bf64fe559
Author: komalsinha-g <59949151+komalsinha-g@users.noreply.github.com>
Date:   Fri Jan 17 14:36:55 2020 +0530

    Make "OverwriteWithEmptyValue" in config struct to be set by any method. (#133)

    Make "OverwriteWithEmptyValue" in config struct to be set by any method.

    this make the merge more customizable. In cases, where request toggles a boolean value and the the request message can change a "true" value to false we can pass the flag to merge accordingly, which is not possible otherwise.

commit f444b9dcd2f5e66f8572b474b465564ba4f54240
Author: Dario Castañé <i@dario.im>
Date:   Wed Jan 1 22:27:24 2020 +0100

    DeepSource support

commit a6318969156f71a80c41afd42104c53f239ff23a
Author: Dario Castañé <i@dario.im>
Date:   Wed Jan 1 22:25:43 2020 +0100

    Assert removed

commit 6c2f66f933a1741f448e437dea3cb491a7c979d8
Author: Peer Xu <pppeerxu@gmail.com>
Date:   Sat Oct 12 10:35:47 2019 +0800

    Fixed typo

commit 8993363a0c7c25aec36e0da04e82d4988d6cc96b
Author: Peer Xu <pppeerxu@gmail.com>
Date:   Sat Oct 12 10:26:25 2019 +0800

    added WithSliceDeepCopy config flag
  • Loading branch information
darccio committed Jul 17, 2020
1 parent a0c19e4 commit 72f2f56
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 129 deletions.
90 changes: 9 additions & 81 deletions issue121_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,102 +4,30 @@ import (
"testing"
)

func TestIssue121WithSliceDeepMerge(t *testing.T) {
dst := map[string]interface{}{
"a": "1",
"b": []map[string]interface{}{
{"c": "2"},
},
}
src := map[string]interface{}{
"b": []map[string]interface{}{
{"c": "3", "d": "1"},
{"e": "1", "f": "1", "g": []string{"1", "2"}},
},
}
if err := Merge(&dst, src, WithDeepMergeSlice); err != nil {
t.Fatalf("Error during the merge: %v", err)
}
if dst["a"].(string) != "1" {
t.Error("a should equal '1'")
}
if dst["b"].([]map[string]interface{})[0]["c"] != "2" {
t.Error("b.[0].c should equal '2'")
}
if dst["b"].([]map[string]interface{})[0]["d"] != "1" {
t.Error("b.[0].d should equal '2'")
}
if dst["b"].([]map[string]interface{})[1]["e"] != "1" {
t.Error("b.[1].e should equal '1'")
}
if dst["b"].([]map[string]interface{})[1]["g"].([]string)[0] != "1" {
t.Error("b.[1].g[0] should equal '1'")
}
}

func TestIssue121WithSliceDeepMergeFromPR126(t *testing.T) {
func TestIssue121WithSliceDeepCopy(t *testing.T) {
dst := map[string]interface{}{
"inter": map[string]interface{}{
"a": "1",
"b": "2",
},
}

src := map[string]interface{}{
"inter": map[string]interface{}{
"a": "3",
"c": "4",
},
}
if err := Merge(&dst, src, WithDeepMergeSlice); err != nil {
t.Fatalf("Error during the merge: %v", err)
}
if dst["inter"].(map[string]interface{})["a"].(string) != "1" {
t.Error("inter.a should equal '1'")

if err := Merge(&dst, src, WithSliceDeepCopy); err != nil {
t.Errorf("Error during the merge: %v", err)
}
if dst["inter"].(map[string]interface{})["b"].(string) != "2" {
t.Error("inter.a should equal '2'")

if dst["inter"].(map[string]interface{})["a"].(string) != "3" {
t.Error("inter.a should equal '3'")
}

if dst["inter"].(map[string]interface{})["c"].(string) != "4" {
t.Error("inter.c should equal '4'")
}
}

type order struct {
A string
B int64
Details []detail
}

type detail struct {
A string
B string
}

func TestIssue121(t *testing.T) {
src := order{
A: "one",
B: 2,
Details: []detail{
{
B: "B",
},
},
}
dst := order{
A: "two",
Details: []detail{
{
A: "one",
},
},
}
if err := Merge(&dst, src, WithDeepMergeSlice); err != nil {
t.Fatalf("Error during the merge: %v", err)
}
if len(dst.Details) != 1 {
t.Fatalf("B was not properly merged: %+v", dst.Details)
}
if dst.Details[0].B != "B" {
t.Fatalf("B was not properly merged: %+v", dst.Details[0].B)
}
}
4 changes: 3 additions & 1 deletion issue131_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ func TestIssue131MergeWithOverwriteWithEmptyValue(t *testing.T) {
A: func(v bool) *bool { return &v }(true),
B: "dest",
}
Merge(&dest, src, WithOverwriteWithEmptyValue)
if err := Merge(&dest, src, WithOverwriteWithEmptyValue); err != nil {
t.Error(err)
}
if *src.A != *dest.A {
t.Errorf("dest.A not merged in properly: %v != %v", *src.A, *dest.A)
}
Expand Down
3 changes: 1 addition & 2 deletions map.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,8 @@ func MapWithOverwrite(dst, src interface{}, opts ...func(*Config)) error {

func _map(dst, src interface{}, opts ...func(*Config)) error {
if dst != nil && reflect.ValueOf(dst).Kind() != reflect.Ptr {
return ErrNonPointerArgument
return ErrNonPointerAgument
}

var (
vDst, vSrc reflect.Value
err error
Expand Down
124 changes: 86 additions & 38 deletions merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ import (
"unsafe"
)

func hasExportedField(dst reflect.Value) bool {
func isMergeableField(dst reflect.Value) (exported bool) {
for i, n := 0, dst.NumField(); i < n; i++ {
field := dst.Type().Field(i)
if isExportedComponent(&field) {
return true
if field.Anonymous && dst.Field(i).Kind() == reflect.Struct {
exported = exported || isMergeableField(dst.Field(i))
} else if isExportedComponent(&field) {
exported = exported || len(field.PkgPath) == 0
}
}

Expand All @@ -30,24 +32,20 @@ func isExportedComponent(field *reflect.StructField) bool {
if len(pkgPath) > 0 {
return false
}

c := field.Name[0]
if 'a' <= c && c <= 'z' || c == '_' {
return false
}

return true
}

// Config allows to customize Mergo's behaviour.
type Config struct {
Overwrite bool
AppendSlice bool
TypeCheck bool
overwriteWithEmptyValue bool
overwriteSliceWithEmptyValue bool
deepMergeSlice bool
Transformers Transformers
sliceDeepCopy bool
}

// Transformers allow to merge specific types differently than in the default behavior.
Expand All @@ -65,6 +63,7 @@ func deepMerge(dstIn, src reflect.Value, visited map[uintptr]*visit, depth int,
typeCheck := config.TypeCheck
overwriteWithEmptySrc := config.overwriteWithEmptyValue
overwriteSliceWithEmptySrc := config.overwriteSliceWithEmptyValue
sliceDeepCopy := config.sliceDeepCopy

if !src.IsValid() {
return
Expand Down Expand Up @@ -100,9 +99,7 @@ func deepMerge(dstIn, src reflect.Value, visited map[uintptr]*visit, depth int,

switch dst.Kind() {
case reflect.Struct:
if hasExportedField(dst) {
dstCp := reflect.New(dst.Type()).Elem()

if isMergeableField(dst) {
for i, n := 0, dst.NumField(); i < n; i++ {
dstField := dst.Field(i)
structField := dst.Type().Field(i)
Expand All @@ -129,11 +126,9 @@ func deepMerge(dstIn, src reflect.Value, visited map[uintptr]*visit, depth int,

dstCp.Field(i).Set(dstField)
}

if dst.CanSet() {
dst.Set(dstCp)
} else {
dst = dstCp
} else {
if (isReflectNil(dst) || overwrite) && (!isEmptyValue(src) || overwriteWithEmptySrc) {
dst.Set(src)
}

return
Expand Down Expand Up @@ -204,15 +199,35 @@ func deepMerge(dstIn, src reflect.Value, visited map[uintptr]*visit, depth int,
dst.SetMapIndex(key, newSlice)
dstElement = newSlice
}
}
} else {
if overwrite || overwriteSliceWithEmptySrc || overwriteWithEmptySrc || dst.IsNil() {
dst.SetMapIndex(key, srcElement)
dstElement = srcElement
} else {
if typeCheck {
err = fmt.Errorf("cannot append two different types (%s, %s)", srcElement, dstElement)
return

if (!isEmptyValue(src) || overwriteWithEmptySrc || overwriteSliceWithEmptySrc) && (overwrite || isEmptyValue(dst)) && !config.AppendSlice && !sliceDeepCopy {
if typeCheck && srcSlice.Type() != dstSlice.Type() {
return fmt.Errorf("cannot override two slices with different type (%s, %s)", srcSlice.Type(), dstSlice.Type())
}
dstSlice = srcSlice
} else if config.AppendSlice {
if srcSlice.Type() != dstSlice.Type() {
return fmt.Errorf("cannot append two slices with different type (%s, %s)", srcSlice.Type(), dstSlice.Type())
}
dstSlice = reflect.AppendSlice(dstSlice, srcSlice)
} else if sliceDeepCopy {
i := 0
for ; i < srcSlice.Len() && i < dstSlice.Len(); i++ {
srcElement := srcSlice.Index(i)
dstElement := dstSlice.Index(i)

if srcElement.CanInterface() {
srcElement = reflect.ValueOf(srcElement.Interface())
}
if dstElement.CanInterface() {
dstElement = reflect.ValueOf(dstElement.Interface())
}

if err = deepMerge(dstElement, srcElement, visited, depth+1, config); err != nil {
return
}
}

}
}
}
Expand All @@ -225,15 +240,11 @@ func deepMerge(dstIn, src reflect.Value, visited map[uintptr]*visit, depth int,
dst.SetMapIndex(key, dstElement)
}
case reflect.Slice:
newSlice := dst

if (!isEmptyValue(src) || overwriteWithEmptySrc || overwriteSliceWithEmptySrc) && (overwrite || isEmptyValue(dst)) && !config.AppendSlice {
if typeCheck && src.Type() != dst.Type() {
err = fmt.Errorf("cannot override two slices with different type (%s, %s)", src.Type(), dst.Type())
return
}

newSlice = src
if !dst.CanSet() {
break
}
if (!isEmptyValue(src) || overwriteWithEmptySrc || overwriteSliceWithEmptySrc) && (overwrite || isEmptyValue(dst)) && !config.AppendSlice && !sliceDeepCopy {
dst.Set(src)
} else if config.AppendSlice {
if typeCheck && src.Type() != dst.Type() {
err = fmt.Errorf("cannot append two slice with different type (%s, %s)", src.Type(), dst.Type())
Expand All @@ -245,6 +256,29 @@ func deepMerge(dstIn, src reflect.Value, visited map[uintptr]*visit, depth int,
err = fmt.Errorf("cannot deep merge two slice with different type (%s, %s)", src.Type(), dst.Type())
return
}
dst.Set(reflect.AppendSlice(dst, src))
} else if sliceDeepCopy {
for i := 0; i < src.Len() && i < dst.Len(); i++ {
srcElement := src.Index(i)
dstElement := dst.Index(i)
if srcElement.CanInterface() {
srcElement = reflect.ValueOf(srcElement.Interface())
}
if dstElement.CanInterface() {
dstElement = reflect.ValueOf(dstElement.Interface())
}

if err = deepMerge(dstElement, srcElement, visited, depth+1, config); err != nil {
return
}
}
}
case reflect.Ptr:
fallthrough
case reflect.Interface:
if src.IsNil() {
break
}

if src.Len() > dst.Len() {
newSlice = reflect.MakeSlice(dst.Type(), src.Len(), src.Len())
Expand Down Expand Up @@ -407,14 +441,15 @@ func WithTypeCheck(config *Config) {
config.TypeCheck = true
}

// WithDeepMergeSlice will make merge deep merge slice elements pairwise (resizing dst slice if needed)
func WithDeepMergeSlice(config *Config) {
config.deepMergeSlice = true
// WithSliceDeepCopy will merge slice element one by one with Overwrite flag.
func WithSliceDeepCopy(config *Config) {
config.sliceDeepCopy = true
config.Overwrite = true
}

func merge(dst, src interface{}, opts ...func(*Config)) error {
if dst != nil && reflect.ValueOf(dst).Kind() != reflect.Ptr {
return ErrNonPointerArgument
return ErrNonPointerAgument
}
var (
vDst, vSrc reflect.Value
Expand Down Expand Up @@ -452,3 +487,16 @@ func isReflectNil(v reflect.Value) bool {
return false
}
}

// IsReflectNil is the reflect value provided nil
func isReflectNil(v reflect.Value) bool {
k := v.Kind()
switch k {
case reflect.Interface, reflect.Slice, reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr:
// Both interface and slice are nil if first word is 0.
// Both are always bigger than a word; assume flagIndir.
return v.IsNil()
default:
return false
}
}
4 changes: 2 additions & 2 deletions merge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func TestMergeNonPointer(t *testing.T) {
"a": "1",
},
}
want := ErrNonPointerArgument
want := ErrNonPointerAgument

if got := merge(dst, src); got != want {
t.Errorf("want: %s, got: %s", want, got)
Expand All @@ -79,7 +79,7 @@ func TestMapNonPointer(t *testing.T) {
},
},
}
want := ErrNonPointerArgument
want := ErrNonPointerAgument
if got := merge(dst, src); got != want {
t.Errorf("want: %s, got: %s", want, got)
}
Expand Down
2 changes: 1 addition & 1 deletion mergo.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ var (
ErrNotSupported = errors.New("only structs and maps are supported")
ErrExpectedMapAsDestination = errors.New("dst was expected to be a map")
ErrExpectedStructAsDestination = errors.New("dst was expected to be a struct")
ErrNonPointerArgument = errors.New("dst must be a pointer")
ErrNonPointerAgument = errors.New("dst must be a pointer")
)

// During deepMerge, must keep track of checks that are
Expand Down
8 changes: 4 additions & 4 deletions mergo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,12 +365,12 @@ func TestMapsWithOverwrite(t *testing.T) {
"e": {14},
}

if err := MergeWithOverwrite(&dst, src); err != nil {
if err := MergeWithOverwrite(&m, n); err != nil {
t.Errorf(err.Error())
}

if !reflect.DeepEqual(dst, expect) {
t.Errorf("Test failed:\ngot :\n%#v\n\nwant :\n%#v\n\n", dst, expect)
if !reflect.DeepEqual(m, expect) {
t.Errorf("Test failed:\ngot :\n%#v\n\nwant :\n%#v\n\n", m, expect)
}
}

Expand Down Expand Up @@ -552,7 +552,7 @@ func TestMaps(t *testing.T) {
if !reflect.DeepEqual(m, expect) {
t.Errorf("Test failed:\ngot :\n%#v\n\nwant :\n%#v\n\n", m, expect)
}
if m["a"].Value != 16 {
if m["a"].Value != 0 {
t.Errorf(`n merged in m because I solved non-addressable map values TODO: m["a"].Value(%d) != n["a"].Value(%d)`, m["a"].Value, n["a"].Value)
}
if m["b"].Value != 42 {
Expand Down

0 comments on commit 72f2f56

Please sign in to comment.