Skip to content

Commit

Permalink
Add WithSliceDeepMerge option
Browse files Browse the repository at this point in the history
  • Loading branch information
sunsingerus committed May 7, 2023
1 parent 14fe2b1 commit 1094edd
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 3 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ Session.vim
*~
# Auto-generated tag files
tags

# IDE
.idea/
146 changes: 146 additions & 0 deletions issue233_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package mergo_test

import (
"reflect"
"testing"

"github.com/imdario/mergo"
)

// SimpleStructTest233 is a simple struct with fields of a base type
type SimpleStructTest233 struct {
Field1 string
Field2 string
Field3 string
}

// StructWithSliceOfSimpleStructsTest233 has to have slice of structs with fields
type StructWithSliceOfSimpleStructsTest233 struct {
SliceOfSimpleStructs []SimpleStructTest233
}

// makeSrcDst makes source and destination structs for test
func makeSrcDst() (src StructWithSliceOfSimpleStructsTest233, dst StructWithSliceOfSimpleStructsTest233) {
src = StructWithSliceOfSimpleStructsTest233{
SliceOfSimpleStructs: []SimpleStructTest233{
{
Field1: "src:Slice[0].Field1",
Field2: "src:Slice[0].Field2",
Field3: "",
},
{
Field1: "src:Slice[1].Field1",
Field2: "src:Slice[1].Field2",
Field3: "",
},
},
}
dst = StructWithSliceOfSimpleStructsTest233{
SliceOfSimpleStructs: []SimpleStructTest233{
{
Field1: "dst:Slice[0].Field1",
Field2: "",
Field3: "dst:Slice[0].Field3",
},
},
}
return
}

// TestNestedStructsFieldsAreMergedWithDeepMerge test base mergo.WithSliceDeepMerge usage
func TestNestedStructsFieldsAreMergedWithDeepMerge(t *testing.T) {
src, dst := makeSrcDst()
expected := StructWithSliceOfSimpleStructsTest233{
SliceOfSimpleStructs: []SimpleStructTest233{
{
// Original dst field is expected not to be overwritten by value
Field1: "dst:Slice[0].Field1",
// Empty dst field is expected to be filled with src value
Field2: "src:Slice[0].Field2",
// Original dst field is expected not to be overwritten by empty value
Field3: "dst:Slice[0].Field3",
},
// Expected dst being appended
{
Field1: "src:Slice[1].Field1",
Field2: "src:Slice[1].Field2",
Field3: "",
},
},
}

err := mergo.Merge(&dst, src, mergo.WithSliceDeepMerge)
if err != nil {
t.Errorf("Error while merging %s", err)
}

if !reflect.DeepEqual(dst, expected) {
t.Errorf("expected: %#v\ngot: %#v", expected, dst)
}
}

// TestNestedStructsFieldsAreMergedWithDeepMergeWithOverride test combination of
// mergo.WithSliceDeepMerge and mergo.WithOverride
func TestNestedStructsFieldsAreMergedWithDeepMergeWithOverride(t *testing.T) {
src, dst := makeSrcDst()
expected := StructWithSliceOfSimpleStructsTest233{
SliceOfSimpleStructs: []SimpleStructTest233{
{
// Original dst field is expected to be overwritten by value
Field1: "src:Slice[0].Field1",
// Empty dst field is expected to be filled with src value
Field2: "src:Slice[0].Field2",
// Original dst field is expected not to be overwritten by empty value
Field3: "dst:Slice[0].Field3",
},
// Expected dst being appended
{
Field1: "src:Slice[1].Field1",
Field2: "src:Slice[1].Field2",
Field3: "",
},
},
}

err := mergo.Merge(&dst, src, mergo.WithSliceDeepMerge, mergo.WithOverride)
if err != nil {
t.Errorf("Error while merging %s", err)
}

if !reflect.DeepEqual(dst, expected) {
t.Errorf("expected: %#v\ngot: %#v", expected, dst)
}
}

// TestNestedStructsFieldsAreMergedWithDeepMergeWithOverwriteWithEmptyValue test combination of
// mergo.WithSliceDeepMerge and mergo.WithOverwriteWithEmptyValue
func TestNestedStructsFieldsAreMergedWithDeepMergeWithOverwriteWithEmptyValue(t *testing.T) {
src, dst := makeSrcDst()
expected := StructWithSliceOfSimpleStructsTest233{
SliceOfSimpleStructs: []SimpleStructTest233{
{
// Original dst field is expected to be overwritten by value
Field1: "src:Slice[0].Field1",
// Empty dst field is expected to be filled with src value
Field2: "src:Slice[0].Field2",
// Original dst field is expected to be overwritten by empty value
Field3: "",
},
// Expected dst being appended
{
Field1: "src:Slice[1].Field1",
Field2: "src:Slice[1].Field2",
Field3: "",
},
},
}

err := mergo.Merge(&dst, src, mergo.WithSliceDeepMerge, mergo.WithOverwriteWithEmptyValue)
if err != nil {
t.Errorf("Error while merging %s", err)
}

if !reflect.DeepEqual(dst, expected) {
t.Errorf("expected: %#v\ngot: %#v", expected, dst)
}
}
37 changes: 34 additions & 3 deletions merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type Config struct {
overwriteWithEmptyValue bool
overwriteSliceWithEmptyValue bool
sliceDeepCopy bool
sliceDeepMerge bool
debug bool
}

Expand All @@ -62,6 +63,7 @@ func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int, co
overwriteWithEmptySrc := config.overwriteWithEmptyValue
overwriteSliceWithEmptySrc := config.overwriteSliceWithEmptyValue
sliceDeepCopy := config.sliceDeepCopy
sliceDeepMerge := config.sliceDeepMerge

if !src.IsValid() {
return
Expand Down Expand Up @@ -163,7 +165,7 @@ func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int, co
dstSlice = reflect.ValueOf(dstElement.Interface())
}

if (!isEmptyValue(src, !config.ShouldNotDereference) || overwriteWithEmptySrc || overwriteSliceWithEmptySrc) && (overwrite || isEmptyValue(dst, !config.ShouldNotDereference)) && !config.AppendSlice && !sliceDeepCopy {
if (!isEmptyValue(src, !config.ShouldNotDereference) || overwriteWithEmptySrc || overwriteSliceWithEmptySrc) && (overwrite || isEmptyValue(dst, !config.ShouldNotDereference)) && !config.AppendSlice && !sliceDeepCopy && !sliceDeepMerge {
if typeCheck && srcSlice.Type() != dstSlice.Type() {
return fmt.Errorf("cannot override two slices with different type (%s, %s)", srcSlice.Type(), dstSlice.Type())
}
Expand All @@ -190,7 +192,18 @@ func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int, co
return
}
}

} else if sliceDeepMerge {
for i := 0; i < src.Len() && i < dst.Len(); i++ {
srcElement := src.Index(i)
dstElement := dst.Index(i)
if err = deepMerge(dstElement, srcElement, visited, depth+1, config); err != nil {
return
}
}
// Append to dst items beyond its original length
if src.Len() > dst.Len() {
dst.Set(reflect.AppendSlice(dst, src.Slice(dst.Len(), src.Len())))
}
}
dst.SetMapIndex(key, dstSlice)
}
Expand Down Expand Up @@ -226,7 +239,7 @@ func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int, co
if !dst.CanSet() {
break
}
if (!isEmptyValue(src, !config.ShouldNotDereference) || overwriteWithEmptySrc || overwriteSliceWithEmptySrc) && (overwrite || isEmptyValue(dst, !config.ShouldNotDereference)) && !config.AppendSlice && !sliceDeepCopy {
if (!isEmptyValue(src, !config.ShouldNotDereference) || overwriteWithEmptySrc || overwriteSliceWithEmptySrc) && (overwrite || isEmptyValue(dst, !config.ShouldNotDereference)) && !config.AppendSlice && !sliceDeepCopy && !sliceDeepMerge {
dst.Set(src)
} else if config.AppendSlice {
if src.Type() != dst.Type() {
Expand All @@ -248,6 +261,18 @@ func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int, co
return
}
}
} else if sliceDeepMerge {
for i := 0; i < src.Len() && i < dst.Len(); i++ {
srcElement := src.Index(i)
dstElement := dst.Index(i)
if err = deepMerge(dstElement, srcElement, visited, depth+1, config); err != nil {
return
}
}
// Append to dst items beyond its original length
if src.Len() > dst.Len() {
dst.Set(reflect.AppendSlice(dst, src.Slice(dst.Len(), src.Len())))
}
}
case reflect.Ptr:
fallthrough
Expand Down Expand Up @@ -371,6 +396,12 @@ func WithSliceDeepCopy(config *Config) {
config.Overwrite = true
}

// WithSliceDeepMerge will merge slice elements one by one and append 'extra' items
// from src into dst in case src.Len() > dst.Len()
func WithSliceDeepMerge(config *Config) {
config.sliceDeepMerge = true
}

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

0 comments on commit 1094edd

Please sign in to comment.