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 7cab6cf
Show file tree
Hide file tree
Showing 3 changed files with 218 additions and 41 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)
}
}
110 changes: 69 additions & 41 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,34 +165,24 @@ 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 {
switch {
case (!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())
}
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())
}
case config.AppendSlice && srcSlice.Type() != dstSlice.Type():
return fmt.Errorf("cannot append two slices with different type (%s, %s)", srcSlice.Type(), dstSlice.Type())
case config.AppendSlice:
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
}
case sliceDeepCopy:
if err = doSliceDeepCopy(dstSlice, srcSlice, visited, depth, config); err != nil {
return
}
case sliceDeepMerge:
if err = doSliceDeepMerge(dstSlice, srcSlice, visited, depth, config); err != nil {
return
}

}
dst.SetMapIndex(key, dstSlice)
}
Expand Down Expand Up @@ -226,27 +218,23 @@ 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 typeCheck && src.Type() != dst.Type() {
return fmt.Errorf("cannot override two slices with different type (%s, %s)", src.Type(), dst.Type())
}

switch {
case (!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() {
return fmt.Errorf("cannot append two slice with different type (%s, %s)", src.Type(), dst.Type())
}
case config.AppendSlice:
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 sliceDeepCopy:
if err = doSliceDeepCopy(dst, src, visited, depth, config); err != nil {
return
}
case sliceDeepMerge:
if err = doSliceDeepMerge(dst, src, visited, depth, config); err != nil {
return
}
}
case reflect.Ptr:
Expand Down Expand Up @@ -311,6 +299,40 @@ func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int, co
return
}

func doSliceDeepCopy(dst, src reflect.Value, visited map[uintptr]*visit, depth int, config *Config) error {
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 err
}
}

return nil
}

func doSliceDeepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int, config *Config) error {
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 err
}
}
// Append to dst items beyond its original length
if src.Len() > dst.Len() {
dst.Set(reflect.AppendSlice(dst, src.Slice(dst.Len(), src.Len())))
}
return nil
}

// Merge will fill any empty for value type attributes on the dst struct using corresponding
// src attributes if they themselves are not empty. dst and src must be valid same-type structs
// and dst must be a pointer to struct.
Expand Down Expand Up @@ -371,6 +393,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 7cab6cf

Please sign in to comment.