diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..07939d3 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +ko_fi: dariocc +custom: https://beerpay.io/imdario/mergo diff --git a/.travis.yml b/.travis.yml index b13a50e..dad2972 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,4 +4,6 @@ - go get golang.org/x/tools/cmd/cover - go get github.com/mattn/goveralls script: + - go test -race -v ./... +after_script: - $HOME/gopath/bin/goveralls -service=travis-ci -repotoken $COVERALLS_TOKEN diff --git a/README.md b/README.md index d1cefa8..02fc81e 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ [![Build Status][1]][2] [![Coverage Status][7]][8] [![Sourcegraph][9]][10] +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fimdario%2Fmergo.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fimdario%2Fmergo?ref=badge_shield) [1]: https://travis-ci.org/imdario/mergo.png [2]: https://travis-ci.org/imdario/mergo @@ -27,7 +28,7 @@ ### Latest release -[Release v0.3.4](https://github.com/imdario/mergo/releases/tag/v0.3.4). +[Release v0.3.7](https://github.com/imdario/mergo/releases/tag/v0.3.7). ### Important note @@ -217,6 +218,21 @@ Written by [Dario Castañé](http://dario.im). +## Top Contributors + +[![0](https://sourcerer.io/fame/imdario/imdario/mergo/images/0)](https://sourcerer.io/fame/imdario/imdario/mergo/links/0) +[![1](https://sourcerer.io/fame/imdario/imdario/mergo/images/1)](https://sourcerer.io/fame/imdario/imdario/mergo/links/1) +[![2](https://sourcerer.io/fame/imdario/imdario/mergo/images/2)](https://sourcerer.io/fame/imdario/imdario/mergo/links/2) +[![3](https://sourcerer.io/fame/imdario/imdario/mergo/images/3)](https://sourcerer.io/fame/imdario/imdario/mergo/links/3) +[![4](https://sourcerer.io/fame/imdario/imdario/mergo/images/4)](https://sourcerer.io/fame/imdario/imdario/mergo/links/4) +[![5](https://sourcerer.io/fame/imdario/imdario/mergo/images/5)](https://sourcerer.io/fame/imdario/imdario/mergo/links/5) +[![6](https://sourcerer.io/fame/imdario/imdario/mergo/images/6)](https://sourcerer.io/fame/imdario/imdario/mergo/links/6) +[![7](https://sourcerer.io/fame/imdario/imdario/mergo/images/7)](https://sourcerer.io/fame/imdario/imdario/mergo/links/7) + + ## License [BSD 3-Clause](http://opensource.org/licenses/BSD-3-Clause) license, as [Go language](http://golang.org/LICENSE). + + +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fimdario%2Fmergo.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fimdario%2Fmergo?ref=badge_large) diff --git a/issue125_test.go b/issue125_test.go new file mode 100644 index 0000000..69cc894 --- /dev/null +++ b/issue125_test.go @@ -0,0 +1,37 @@ +package mergo + +import ( + "encoding/json" + "testing" +) + +var ( + data = `{"FirstSlice":[], "SecondSlice": null}` +) + +type settings struct { + FirstSlice []string `json:"FirstSlice"` + SecondSlice []string `json:"SecondSlice"` +} + +func TestIssue125MergeWithOverwrite(t *testing.T) { + + defaultSettings := settings{ + FirstSlice: []string{}, + SecondSlice: []string{}, + } + + var something settings + if err := json.Unmarshal([]byte(data), &something); err != nil { + t.Errorf("Error while Unmarshalling maprequest: %s", err) + } + if err := Merge(&something, defaultSettings, WithOverrideEmptySlice); err != nil { + t.Errorf("Error while merging: %s", err) + } + if something.FirstSlice == nil { + t.Error("Invalid merging first slice") + } + if something.SecondSlice == nil { + t.Error("Invalid merging second slice") + } +} diff --git a/issue84_test.go b/issue84_test.go new file mode 100644 index 0000000..aa60526 --- /dev/null +++ b/issue84_test.go @@ -0,0 +1,82 @@ +package mergo + +import ( + "testing" +) + +type DstStructIssue84 struct { + A int + B int + C int +} + +type DstNestedStructIssue84 struct { + A struct { + A int + B int + C int + } + B int + C int +} + +func TestIssue84MergeMapWithNilValueToStructWithOverride(t *testing.T) { + p1 := DstStructIssue84{ + A: 0, B: 1, C: 2, + } + p2 := map[string]interface{}{ + "A": 3, "B": 4, "C": 0, + } + if err := Map(&p1, p2, WithOverride); err != nil { + t.Fatalf("Error during the merge: %v", err) + } + if p1.C != 0 { + t.Error("C field should become '0'") + } +} + +func TestIssue84MergeMapWithoutKeyExistsToStructWithOverride(t *testing.T) { + p1 := DstStructIssue84{ + A: 0, B: 1, C: 2, + } + p2 := map[string]interface{}{ + "A": 3, "B": 4, + } + if err := Map(&p1, p2, WithOverride); err != nil { + t.Fatalf("Error during the merge: %v", err) + } + if p1.C != 2 { + t.Error("C field should be '2'") + } +} + +func TestIssue84MergeNestedMapWithNilValueToStructWithOverride(t *testing.T) { + p1 := DstNestedStructIssue84{ + A: struct { + A int + B int + C int + }{A: 1, B: 2, C: 0}, + B: 0, + C: 2, + } + p2 := map[string]interface{}{ + "A": map[string]interface{}{ + "A": 0, "B": 0, "C": 5, + }, "B": 4, "C": 0, + } + if err := Map(&p1, p2, WithOverride); err != nil { + t.Fatalf("Error during the merge: %v", err) + } + if p1.B != 4 { + t.Error("A.C field should become '4'") + } + + if p1.A.C != 5 { + t.Error("A.C field should become '5'") + } + + if p1.A.B != 0 || p1.A.A != 0 { + t.Error("A.A and A.B field should become '0'") + } +} diff --git a/map.go b/map.go index 6ea38e6..3f5afa8 100644 --- a/map.go +++ b/map.go @@ -72,6 +72,7 @@ case reflect.Struct: srcMap := src.Interface().(map[string]interface{}) for key := range srcMap { + config.overwriteWithEmptyValue = true srcValue := srcMap[key] fieldName := changeInitialCase(key, unicode.ToUpper) dstElement := dst.FieldByName(fieldName) diff --git a/merge.go b/merge.go index 706b220..3fb6c64 100644 --- a/merge.go +++ b/merge.go @@ -9,6 +9,7 @@ package mergo import ( + "fmt" "reflect" ) @@ -25,9 +26,12 @@ } type Config struct { - Overwrite bool - AppendSlice bool - Transformers Transformers + Overwrite bool + AppendSlice bool + TypeCheck bool + Transformers Transformers + overwriteWithEmptyValue bool + overwriteSliceWithEmptyValue bool } type Transformers interface { @@ -39,6 +43,10 @@ // short circuiting on recursive types. func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int, config *Config) (err error) { overwrite := config.Overwrite + typeCheck := config.TypeCheck + overwriteWithEmptySrc := config.overwriteWithEmptyValue + overwriteSliceWithEmptySrc := config.overwriteSliceWithEmptyValue + config.overwriteWithEmptyValue = false if !src.IsValid() { return @@ -73,7 +81,7 @@ } } } else { - if dst.CanSet() && !isEmptyValue(src) && (overwrite || isEmptyValue(dst)) { + if dst.CanSet() && (!isEmptyValue(src) || overwriteWithEmptySrc) && (overwrite || isEmptyValue(dst)) { dst.Set(src) } } @@ -124,19 +132,25 @@ dstSlice = reflect.ValueOf(dstElement.Interface()) } - if !isEmptyValue(src) && (overwrite || isEmptyValue(dst)) && !config.AppendSlice { + if (!isEmptyValue(src) || overwriteWithEmptySrc || overwriteSliceWithEmptySrc) && (overwrite || isEmptyValue(dst)) && !config.AppendSlice { + 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) } dst.SetMapIndex(key, dstSlice) } } - if dstElement.IsValid() && reflect.TypeOf(srcElement.Interface()).Kind() == reflect.Map { + if dstElement.IsValid() && !isEmptyValue(dstElement) && (reflect.TypeOf(srcElement.Interface()).Kind() == reflect.Map || reflect.TypeOf(srcElement.Interface()).Kind() == reflect.Slice) { continue } - if srcElement.IsValid() && (overwrite || (!dstElement.IsValid() || isEmptyValue(dstElement))) { + if srcElement.IsValid() && ((srcElement.Kind() != reflect.Ptr && overwrite) || !dstElement.IsValid() || isEmptyValue(dstElement)) { if dst.IsNil() { dst.Set(reflect.MakeMap(dst.Type())) } @@ -147,9 +161,12 @@ if !dst.CanSet() { break } - if !isEmptyValue(src) && (overwrite || isEmptyValue(dst)) && !config.AppendSlice { + if (!isEmptyValue(src) || overwriteWithEmptySrc || overwriteSliceWithEmptySrc) && (overwrite || isEmptyValue(dst)) && !config.AppendSlice { 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()) + } dst.Set(reflect.AppendSlice(dst, src)) } case reflect.Ptr: @@ -158,11 +175,21 @@ if src.IsNil() { break } - if src.Kind() != reflect.Interface { + + if dst.Kind() != reflect.Ptr && src.Type().AssignableTo(dst.Type()) { if dst.IsNil() || overwrite { if dst.CanSet() && (overwrite || isEmptyValue(dst)) { dst.Set(src) } + } + break + } + + if src.Kind() != reflect.Interface { + if dst.IsNil() || (src.Kind() != reflect.Ptr && overwrite) { + if dst.CanSet() && (overwrite || isEmptyValue(dst)) { + dst.Set(src) + } } else if src.Kind() == reflect.Ptr { if err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1, config); err != nil { return @@ -184,10 +211,11 @@ return } default: - if dst.CanSet() && !isEmptyValue(src) && (overwrite || isEmptyValue(dst)) { + if dst.CanSet() && (!isEmptyValue(src) || overwriteWithEmptySrc) && (overwrite || isEmptyValue(dst)) { dst.Set(src) } } + return } @@ -199,7 +227,7 @@ return merge(dst, src, opts...) } -// MergeWithOverwrite will do the same as Merge except that non-empty dst attributes will be overriden by +// MergeWithOverwrite will do the same as Merge except that non-empty dst attributes will be overridden by // non-empty src attribute values. // Deprecated: use Merge(…) with WithOverride func MergeWithOverwrite(dst, src interface{}, opts ...func(*Config)) error { @@ -218,9 +246,19 @@ config.Overwrite = true } -// WithAppendSlice will make merge append slices instead of overwriting it +// WithOverride will make merge override empty dst slice with empty src slice. +func WithOverrideEmptySlice(config *Config) { + config.overwriteSliceWithEmptyValue = true +} + +// WithAppendSlice will make merge append slices instead of overwriting it. func WithAppendSlice(config *Config) { config.AppendSlice = true +} + +// WithTypeCheck will make merge check types while overwriting it (must be used with WithOverride). +func WithTypeCheck(config *Config) { + config.TypeCheck = true } func merge(dst, src interface{}, opts ...func(*Config)) error { diff --git a/merge_interface_concrete_test.go b/merge_interface_concrete_test.go new file mode 100644 index 0000000..9b5e82a --- /dev/null +++ b/merge_interface_concrete_test.go @@ -0,0 +1,42 @@ +package mergo + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +type ifaceTypesTest struct { + N int + Handler http.Handler +} + +type ifaceTypesHandler int + +func (*ifaceTypesHandler) ServeHTTP(rw http.ResponseWriter, _ *http.Request) { + rw.Header().Set("Test", "ifaceTypesHandler") +} + +func TestMergeInterfaceWithDifferentConcreteTypes(t *testing.T) { + dst := ifaceTypesTest{ + Handler: new(ifaceTypesHandler), + } + + src := ifaceTypesTest{ + N: 42, + Handler: http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { + rw.Header().Set("Test", "handlerFunc") + }), + } + + if err := Merge(&dst, src); err != nil { + t.Errorf("Error while merging %s", err) + } + + rw := httptest.NewRecorder() + dst.Handler.ServeHTTP(rw, nil) + + if got, want := rw.Header().Get("Test"), "ifaceTypesHandler"; got != want { + t.Errorf("Handler not merged in properly: got %q header value %q, want %q", "Test", got, want) + } +} diff --git a/mergo_test.go b/mergo_test.go index d777538..10ec34c 100644 --- a/mergo_test.go +++ b/mergo_test.go @@ -8,8 +8,11 @@ import ( "io/ioutil" "reflect" + "strings" "testing" "time" + + "github.com/stretchr/testify/assert" "gopkg.in/yaml.v2" ) @@ -294,6 +297,7 @@ 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{2, 3}, []int{1, 2, 3}, WithAppendSlice, WithOverride) testSlice(t, []int{1}, []int{}, []int{1}, WithAppendSlice) testSlice(t, []int{1}, nil, []int{1}, WithAppendSlice) } @@ -345,7 +349,7 @@ func TestMapsWithOverwrite(t *testing.T) { m := map[string]simpleTest{ "a": {}, // overwritten by 16 - "b": {42}, // not overwritten by empty value + "b": {42}, // overwritten by 0, as map Value is not addressable and it doesn't check for b is set or not set in `n` "c": {13}, // overwritten by 12 "d": {61}, } @@ -372,6 +376,167 @@ } } +func TestMapWithEmbeddedStructPointer(t *testing.T) { + m := map[string]*simpleTest{ + "a": {}, // overwritten by 16 + "b": {42}, // not overwritten by empty value + "c": {13}, // overwritten by 12 + "d": {61}, + } + n := map[string]*simpleTest{ + "a": {16}, + "b": {}, + "c": {12}, + "e": {14}, + } + expect := map[string]*simpleTest{ + "a": {16}, + "b": {42}, + "c": {12}, + "d": {61}, + "e": {14}, + } + + if err := Merge(&m, n, WithOverride); err != nil { + t.Fatalf(err.Error()) + } + + assert.Equalf(t, expect, m, "Test Failed") + if !reflect.DeepEqual(m, expect) { + t.Fatalf("Test failed:\ngot :\n%#v\n\nwant :\n%#v\n\n", m, expect) + } +} + +func TestMergeUsingStructAndMap(t *testing.T) { + type multiPtr struct { + Text string + Number int + } + type final struct { + Msg1 string + Msg2 string + } + type params struct { + Name string + Multi *multiPtr + Final *final + } + type config struct { + Foo string + Bar string + Params *params + } + + cases := []struct { + name string + overwrite bool + changes *config + target *config + output *config + }{ + { + name: "Should overwrite values in target for non-nil values in source", + overwrite: true, + changes: &config{ + Bar: "from changes", + Params: ¶ms{ + Final: &final{ + Msg1: "from changes", + Msg2: "from changes", + }, + }, + }, + target: &config{ + Foo: "from target", + Params: ¶ms{ + Name: "from target", + Multi: &multiPtr{ + Text: "from target", + Number: 5, + }, + Final: &final{ + Msg1: "from target", + Msg2: "", + }, + }, + }, + output: &config{ + Foo: "from target", + Bar: "from changes", + Params: ¶ms{ + Name: "from target", + Multi: &multiPtr{ + Text: "from target", + Number: 5, + }, + Final: &final{ + Msg1: "from changes", + Msg2: "from changes", + }, + }, + }, + }, + { + name: "Should not overwrite values in target for non-nil values in source", + overwrite: false, + changes: &config{ + Bar: "from changes", + Params: ¶ms{ + Final: &final{ + Msg1: "from changes", + Msg2: "from changes", + }, + }, + }, + target: &config{ + Foo: "from target", + Params: ¶ms{ + Name: "from target", + Multi: &multiPtr{ + Text: "from target", + Number: 5, + }, + Final: &final{ + Msg1: "from target", + Msg2: "", + }, + }, + }, + output: &config{ + Foo: "from target", + Bar: "from changes", + Params: ¶ms{ + Name: "from target", + Multi: &multiPtr{ + Text: "from target", + Number: 5, + }, + Final: &final{ + Msg1: "from target", + Msg2: "from changes", + }, + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var err error + if tc.overwrite { + err = Merge(tc.target, *tc.changes, WithOverride) + } else { + err = Merge(tc.target, *tc.changes) + } + if err != nil { + t.Error(err) + } + if !reflect.DeepEqual(tc.target, tc.output) { + t.Fatalf("Test failed:\ngot :\n%#v\n\nwant :\n%#v\n\n", tc.target, tc.output) + } + }) + } +} func TestMaps(t *testing.T) { m := map[string]simpleTest{ "a": {}, @@ -731,3 +896,45 @@ t.Fatalf("dst.C should be true") } } + +func TestMergeMapWithInnerSliceOfDifferentType(t *testing.T) { + testCases := []struct { + name string + options []func(*Config) + err string + }{ + { + "With override and append slice", + []func(*Config){WithOverride, WithAppendSlice}, + "cannot append two slices with different type", + }, + { + "With override and type check", + []func(*Config){WithOverride, WithTypeCheck}, + "cannot override two slices with different type", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + src := map[string]interface{}{ + "foo": []string{"a", "b"}, + } + dst := map[string]interface{}{ + "foo": []int{1, 2}, + } + + if err := Merge(&src, &dst, tc.options...); err == nil || !strings.Contains(err.Error(), tc.err) { + t.Fatalf("expected %q, got %q", tc.err, err) + } + }) + } +} + +func TestMergeSlicesIsNotSupported(t *testing.T) { + src := []string{"a", "b"} + dst := []int{1, 2} + + if err := Merge(&src, &dst, WithOverride, WithAppendSlice); err != ErrNotSupported { + t.Fatalf("expected %q, got %q", ErrNotSupported, err) + } +}