Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add WithSliceDeepMerge option #234

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
}
111 changes: 69 additions & 42 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,23 @@ 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:
err = doSliceDeepCopy(dstSlice, srcSlice, visited, depth, config)
case sliceDeepMerge:
err = doSliceDeepMerge(dstSlice, srcSlice, visited, depth, config)
}
if err != nil {
return
}
dst.SetMapIndex(key, dstSlice)
}
Expand Down Expand Up @@ -226,27 +217,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 +298,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 +392,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