From 4c98defe23ead3906b911ad6a2d1f9ef92286abb Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Fri, 11 Oct 2019 13:33:01 +0100 Subject: [PATCH 01/56] support for private struct fields --- diff_float.go | 6 +++++- diff_int.go | 6 +++++- diff_test.go | 16 ++++++++++++++++ diff_uint.go | 6 +++++- 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/diff_float.go b/diff_float.go index 602f97d..4a663ae 100644 --- a/diff_float.go +++ b/diff_float.go @@ -24,7 +24,11 @@ func (d *Differ) diffFloat(path []string, a, b reflect.Value) error { } if a.Float() != b.Float() { - d.cl.add(UPDATE, path, a.Interface(), b.Interface()) + if a.CanInterface() { + d.cl.add(UPDATE, path, a.Interface(), b.Interface()) + } else { + d.cl.add(UPDATE, path, a.Float(), b.Float()) + } } return nil diff --git a/diff_int.go b/diff_int.go index b436a78..e50ddf9 100644 --- a/diff_int.go +++ b/diff_int.go @@ -24,7 +24,11 @@ func (d *Differ) diffInt(path []string, a, b reflect.Value) error { } if a.Int() != b.Int() { - d.cl.add(UPDATE, path, a.Interface(), b.Interface()) + if a.CanInterface() { + d.cl.add(UPDATE, path, a.Interface(), b.Interface()) + } else { + d.cl.add(UPDATE, path, a.Int(), b.Int()) + } } return nil diff --git a/diff_test.go b/diff_test.go index c364f0b..9d9ee7d 100644 --- a/diff_test.go +++ b/diff_test.go @@ -48,6 +48,7 @@ type tstruct struct { Identifiables []tistruct `diff:"identifiables"` Unidentifiables []tuistruct `diff:"unidentifiables"` Nested tnstruct `diff:"nested"` + private int `diff:"private"` } func sptr(s string) *string { @@ -362,6 +363,15 @@ func TestDiff(t *testing.T) { }, nil, }, + { + "private-struct-field", + tstruct{private: 1}, + tstruct{private: 4}, + Changelog{ + Change{Type: UPDATE, Path: []string{"private"}, From: int64(1), To: int64(4)}, + }, + nil, + }, } for _, tc := range cases { @@ -574,3 +584,9 @@ func TestDiffingOptions(t *testing.T) { // some other options.. } + +func TestDiffPrivateField(t *testing.T) { + cl, err := Diff(tstruct{private: 1}, tstruct{private: 3}) + require.Nil(t, err) + assert.Len(t, cl, 1) +} diff --git a/diff_uint.go b/diff_uint.go index 45d0319..1f0edcb 100644 --- a/diff_uint.go +++ b/diff_uint.go @@ -24,7 +24,11 @@ func (d *Differ) diffUint(path []string, a, b reflect.Value) error { } if a.Uint() != b.Uint() { - d.cl.add(UPDATE, path, a.Interface(), b.Interface()) + if a.CanInterface() { + d.cl.add(UPDATE, path, a.Interface(), b.Interface()) + } else { + d.cl.add(UPDATE, path, a.Uint(), b.Uint()) + } } return nil From 76b1bcab161a0627efa1bed182c444f332904a5c Mon Sep 17 00:00:00 2001 From: tianhongw Date: Wed, 20 Nov 2019 21:50:01 +0800 Subject: [PATCH 02/56] compare time in unix nano because of the accuracy lose caused by marshal and unmarshal --- diff_time.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/diff_time.go b/diff_time.go index 49f82c0..bd88b80 100644 --- a/diff_time.go +++ b/diff_time.go @@ -6,6 +6,7 @@ package diff import ( "reflect" + "time" ) func (d *Differ) diffTime(path []string, a, b reflect.Value) error { @@ -23,7 +24,11 @@ func (d *Differ) diffTime(path []string, a, b reflect.Value) error { return ErrTypeMismatch } - if a.Interface() != b.Interface() { + // Marshal and unmarshal time type will lose accuracy. Using unix nano to compare time type. + au := a.Interface().(time.Time).UnixNano() + bu := b.Interface().(time.Time).UnixNano() + + if au != bu { d.cl.add(UPDATE, path, a.Interface(), b.Interface()) } From 7cb6232bcd794ba2a938aeec97c71000ec39431f Mon Sep 17 00:00:00 2001 From: Stefan McShane Date: Tue, 17 Mar 2020 17:38:37 -0400 Subject: [PATCH 03/56] Added go.mod --- go.mod | 5 +++++ go.sum | 10 ++++++++++ 2 files changed, 15 insertions(+) create mode 100644 go.mod create mode 100644 go.sum diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7c30699 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/r3labs/diff + +go 1.13 + +require github.com/stretchr/testify v1.5.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a80206a --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From 121f380591d86fb68353b8531d27dd5a6dc76275 Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Tue, 7 Apr 2020 11:34:22 +0100 Subject: [PATCH 04/56] fix slice diffing when one slice is nil --- diff_slice.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/diff_slice.go b/diff_slice.go index 04d07cc..c6f9cb9 100644 --- a/diff_slice.go +++ b/diff_slice.go @@ -9,6 +9,16 @@ import ( ) func (d *Differ) diffSlice(path []string, a, b reflect.Value) error { + if a.Kind() == reflect.Invalid { + d.cl.add(CREATE, path, nil, b.Interface()) + return nil + } + + if b.Kind() == reflect.Invalid { + d.cl.add(DELETE, path, a.Interface(), nil) + return nil + } + if a.Kind() != b.Kind() { return ErrTypeMismatch } From efaa45470b990d6221d71586f3fd91d186d8841d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lovro=20Ma=C5=BEgon?= Date: Sun, 12 Apr 2020 22:01:27 +0200 Subject: [PATCH 05/56] add support for custom differs --- diff.go | 33 +++++++++++++++++++++++++++++++++ diff_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ options.go | 10 +++++++++- 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/diff.go b/diff.go index 35fe0c2..52b8ac0 100644 --- a/diff.go +++ b/diff.go @@ -32,12 +32,16 @@ const ( type Differ struct { SliceOrdering bool DisableStructValues bool + customValueDiffers []ValueDiffer cl Changelog } // Changelog stores a list of changed items type Changelog []Change +// MutableChangelog is a changelog meant to be mutated +type MutableChangelog Changelog + // Change stores information about a changed item type Change struct { Type string `json:"type"` @@ -46,6 +50,12 @@ type Change struct { To interface{} `json:"to"` } +// ValueDiffer is an interface for custom differs +type ValueDiffer interface { + Match(a, b reflect.Value) bool + Diff(cl *MutableChangelog, path []string, a, b reflect.Value) error +} + // Changed returns true if both values differ func Changed(a, b interface{}) bool { cl, _ := Diff(a, b) @@ -107,6 +117,22 @@ func (d *Differ) diff(path []string, a, b reflect.Value) error { return ErrTypeMismatch } + // first go through custom diff functions + if len(d.customValueDiffers) > 0 { + mcl := MutableChangelog(d.cl) + for _, vd := range d.customValueDiffers { + if vd.Match(a, b) { + err := vd.Diff(&mcl, path, a, b) + if err != nil { + return err + } + d.cl = Changelog(mcl) + return nil + } + } + } + + // then built-in diff functions switch { case are(a, b, reflect.Struct, reflect.Invalid): return d.diffStruct(path, a, b) @@ -142,6 +168,13 @@ func (cl *Changelog) add(t string, path []string, from, to interface{}) { }) } +// Add adds a change to the changelog +func (cl *MutableChangelog) Add(t string, path []string, from, to interface{}) { + d := Changelog(*cl) + d.add(t, path, from, to) + *cl = MutableChangelog(d) +} + func tagName(f reflect.StructField) string { t := f.Tag.Get("diff") diff --git a/diff_test.go b/diff_test.go index 9d9ee7d..2803fe7 100644 --- a/diff_test.go +++ b/diff_test.go @@ -5,6 +5,7 @@ package diff import ( + "reflect" "testing" "time" @@ -590,3 +591,46 @@ func TestDiffPrivateField(t *testing.T) { require.Nil(t, err) assert.Len(t, cl, 1) } + +type testType string +type testTypeDiffer struct{} + +func (testTypeDiffer) Match(a, b reflect.Value) bool { + return areType(a, b, reflect.TypeOf(testType(""))) +} +func (testTypeDiffer) Diff(cl *MutableChangelog, path []string, a, b reflect.Value) error { + if a.String() != "custom" && b.String() != "match" { + cl.Add(UPDATE, path, a.Interface(), b.Interface()) + } + return nil +} + +func TestCustomDiffer(t *testing.T) { + type custom struct { + T testType + } + + d, err := NewDiffer( + CustomValueDiffers( + testTypeDiffer{}, + ), + ) + require.Nil(t, err) + + cl, err := d.Diff(custom{"custom"}, custom{"match"}) + require.Nil(t, err) + + assert.Len(t, cl, 0) + + d, err = NewDiffer( + CustomValueDiffers( + testTypeDiffer{}, + ), + ) + require.Nil(t, err) + + cl, err = d.Diff(custom{"same"}, custom{"same"}) + require.Nil(t, err) + + assert.Len(t, cl, 1) +} diff --git a/options.go b/options.go index 627d2b8..1807378 100644 --- a/options.go +++ b/options.go @@ -8,7 +8,7 @@ func SliceOrdering(enabled bool) func(d *Differ) error { } } -// DisableStructValues disables populating a seperate change for each item in a struct, +// DisableStructValues disables populating a separate change for each item in a struct, // where the struct is being compared to a nil value func DisableStructValues() func(d *Differ) error { return func(d *Differ) error { @@ -16,3 +16,11 @@ func DisableStructValues() func(d *Differ) error { return nil } } + +// CustomValueDiffers allows you to register custom differs for specific types +func CustomValueDiffers(vd ...ValueDiffer) func(d *Differ) error { + return func(d *Differ) error { + d.customValueDiffers = append(d.customValueDiffers, vd...) + return nil + } +} From e7b1aca25d2fe7507591d66fc686c4f0a53a345c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lovro=20Ma=C5=BEgon?= Date: Sat, 18 Apr 2020 16:51:32 +0200 Subject: [PATCH 06/56] expose method changelog.Add --- diff.go | 18 +++--------------- diff_bool.go | 6 +++--- diff_float.go | 8 ++++---- diff_int.go | 8 ++++---- diff_interface.go | 8 ++++---- diff_pointer.go | 4 ++-- diff_slice.go | 4 ++-- diff_string.go | 6 +++--- diff_struct.go | 4 ++-- diff_test.go | 2 +- diff_time.go | 6 +++--- diff_uint.go | 8 ++++---- 12 files changed, 35 insertions(+), 47 deletions(-) diff --git a/diff.go b/diff.go index 52b8ac0..691ad1f 100644 --- a/diff.go +++ b/diff.go @@ -39,9 +39,6 @@ type Differ struct { // Changelog stores a list of changed items type Changelog []Change -// MutableChangelog is a changelog meant to be mutated -type MutableChangelog Changelog - // Change stores information about a changed item type Change struct { Type string `json:"type"` @@ -53,7 +50,7 @@ type Change struct { // ValueDiffer is an interface for custom differs type ValueDiffer interface { Match(a, b reflect.Value) bool - Diff(cl *MutableChangelog, path []string, a, b reflect.Value) error + Diff(cl *Changelog, path []string, a, b reflect.Value) error } // Changed returns true if both values differ @@ -119,14 +116,12 @@ func (d *Differ) diff(path []string, a, b reflect.Value) error { // first go through custom diff functions if len(d.customValueDiffers) > 0 { - mcl := MutableChangelog(d.cl) for _, vd := range d.customValueDiffers { if vd.Match(a, b) { - err := vd.Diff(&mcl, path, a, b) + err := vd.Diff(&d.cl, path, a, b) if err != nil { return err } - d.cl = Changelog(mcl) return nil } } @@ -159,7 +154,7 @@ func (d *Differ) diff(path []string, a, b reflect.Value) error { } } -func (cl *Changelog) add(t string, path []string, from, to interface{}) { +func (cl *Changelog) Add(t string, path []string, from, to interface{}) { (*cl) = append((*cl), Change{ Type: t, Path: path, @@ -168,13 +163,6 @@ func (cl *Changelog) add(t string, path []string, from, to interface{}) { }) } -// Add adds a change to the changelog -func (cl *MutableChangelog) Add(t string, path []string, from, to interface{}) { - d := Changelog(*cl) - d.add(t, path, from, to) - *cl = MutableChangelog(d) -} - func tagName(f reflect.StructField) string { t := f.Tag.Get("diff") diff --git a/diff_bool.go b/diff_bool.go index 86c3cb4..9aad631 100644 --- a/diff_bool.go +++ b/diff_bool.go @@ -8,12 +8,12 @@ import "reflect" func (d *Differ) diffBool(path []string, a, b reflect.Value) error { if a.Kind() == reflect.Invalid { - d.cl.add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, b.Interface()) return nil } if b.Kind() == reflect.Invalid { - d.cl.add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, a.Interface(), nil) return nil } @@ -22,7 +22,7 @@ func (d *Differ) diffBool(path []string, a, b reflect.Value) error { } if a.Bool() != b.Bool() { - d.cl.add(UPDATE, path, a.Interface(), b.Interface()) + d.cl.Add(UPDATE, path, a.Interface(), b.Interface()) } return nil diff --git a/diff_float.go b/diff_float.go index 4a663ae..2746dc0 100644 --- a/diff_float.go +++ b/diff_float.go @@ -10,12 +10,12 @@ import ( func (d *Differ) diffFloat(path []string, a, b reflect.Value) error { if a.Kind() == reflect.Invalid { - d.cl.add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, b.Interface()) return nil } if b.Kind() == reflect.Invalid { - d.cl.add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, a.Interface(), nil) return nil } @@ -25,9 +25,9 @@ func (d *Differ) diffFloat(path []string, a, b reflect.Value) error { if a.Float() != b.Float() { if a.CanInterface() { - d.cl.add(UPDATE, path, a.Interface(), b.Interface()) + d.cl.Add(UPDATE, path, a.Interface(), b.Interface()) } else { - d.cl.add(UPDATE, path, a.Float(), b.Float()) + d.cl.Add(UPDATE, path, a.Float(), b.Float()) } } diff --git a/diff_int.go b/diff_int.go index e50ddf9..03158b5 100644 --- a/diff_int.go +++ b/diff_int.go @@ -10,12 +10,12 @@ import ( func (d *Differ) diffInt(path []string, a, b reflect.Value) error { if a.Kind() == reflect.Invalid { - d.cl.add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, b.Interface()) return nil } if b.Kind() == reflect.Invalid { - d.cl.add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, a.Interface(), nil) return nil } @@ -25,9 +25,9 @@ func (d *Differ) diffInt(path []string, a, b reflect.Value) error { if a.Int() != b.Int() { if a.CanInterface() { - d.cl.add(UPDATE, path, a.Interface(), b.Interface()) + d.cl.Add(UPDATE, path, a.Interface(), b.Interface()) } else { - d.cl.add(UPDATE, path, a.Int(), b.Int()) + d.cl.Add(UPDATE, path, a.Int(), b.Int()) } } diff --git a/diff_interface.go b/diff_interface.go index 5ddedf8..d4161c6 100644 --- a/diff_interface.go +++ b/diff_interface.go @@ -8,12 +8,12 @@ import "reflect" func (d *Differ) diffInterface(path []string, a, b reflect.Value) error { if a.Kind() == reflect.Invalid { - d.cl.add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, b.Interface()) return nil } if b.Kind() == reflect.Invalid { - d.cl.add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, a.Interface(), nil) return nil } @@ -26,12 +26,12 @@ func (d *Differ) diffInterface(path []string, a, b reflect.Value) error { } if a.IsNil() { - d.cl.add(UPDATE, path, nil, b.Interface()) + d.cl.Add(UPDATE, path, nil, b.Interface()) return nil } if b.IsNil() { - d.cl.add(UPDATE, path, a.Interface(), nil) + d.cl.Add(UPDATE, path, a.Interface(), nil) return nil } diff --git a/diff_pointer.go b/diff_pointer.go index 342f782..5b14b8a 100644 --- a/diff_pointer.go +++ b/diff_pointer.go @@ -30,12 +30,12 @@ func (d *Differ) diffPtr(path []string, a, b reflect.Value) error { } if a.IsNil() { - d.cl.add(UPDATE, path, nil, b.Interface()) + d.cl.Add(UPDATE, path, nil, b.Interface()) return nil } if b.IsNil() { - d.cl.add(UPDATE, path, a.Interface(), nil) + d.cl.Add(UPDATE, path, a.Interface(), nil) return nil } diff --git a/diff_slice.go b/diff_slice.go index c6f9cb9..27143fa 100644 --- a/diff_slice.go +++ b/diff_slice.go @@ -10,12 +10,12 @@ import ( func (d *Differ) diffSlice(path []string, a, b reflect.Value) error { if a.Kind() == reflect.Invalid { - d.cl.add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, b.Interface()) return nil } if b.Kind() == reflect.Invalid { - d.cl.add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, a.Interface(), nil) return nil } diff --git a/diff_string.go b/diff_string.go index 52dd911..fd5b281 100644 --- a/diff_string.go +++ b/diff_string.go @@ -8,12 +8,12 @@ import "reflect" func (d *Differ) diffString(path []string, a, b reflect.Value) error { if a.Kind() == reflect.Invalid { - d.cl.add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, b.Interface()) return nil } if b.Kind() == reflect.Invalid { - d.cl.add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, a.Interface(), nil) return nil } @@ -22,7 +22,7 @@ func (d *Differ) diffString(path []string, a, b reflect.Value) error { } if a.String() != b.String() { - d.cl.add(UPDATE, path, a.String(), b.String()) + d.cl.Add(UPDATE, path, a.String(), b.String()) } return nil diff --git a/diff_struct.go b/diff_struct.go index 16e7faa..9b4afb3 100644 --- a/diff_struct.go +++ b/diff_struct.go @@ -16,7 +16,7 @@ func (d *Differ) diffStruct(path []string, a, b reflect.Value) error { if a.Kind() == reflect.Invalid { if d.DisableStructValues { - d.cl.add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, b.Interface()) return nil } return d.structValues(CREATE, path, b) @@ -24,7 +24,7 @@ func (d *Differ) diffStruct(path []string, a, b reflect.Value) error { if b.Kind() == reflect.Invalid { if d.DisableStructValues { - d.cl.add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, a.Interface(), nil) return nil } return d.structValues(DELETE, path, a) diff --git a/diff_test.go b/diff_test.go index 2803fe7..2fa4644 100644 --- a/diff_test.go +++ b/diff_test.go @@ -598,7 +598,7 @@ type testTypeDiffer struct{} func (testTypeDiffer) Match(a, b reflect.Value) bool { return areType(a, b, reflect.TypeOf(testType(""))) } -func (testTypeDiffer) Diff(cl *MutableChangelog, path []string, a, b reflect.Value) error { +func (testTypeDiffer) Diff(cl *Changelog, path []string, a, b reflect.Value) error { if a.String() != "custom" && b.String() != "match" { cl.Add(UPDATE, path, a.Interface(), b.Interface()) } diff --git a/diff_time.go b/diff_time.go index bd88b80..4f583ff 100644 --- a/diff_time.go +++ b/diff_time.go @@ -11,12 +11,12 @@ import ( func (d *Differ) diffTime(path []string, a, b reflect.Value) error { if a.Kind() == reflect.Invalid { - d.cl.add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, b.Interface()) return nil } if b.Kind() == reflect.Invalid { - d.cl.add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, a.Interface(), nil) return nil } @@ -29,7 +29,7 @@ func (d *Differ) diffTime(path []string, a, b reflect.Value) error { bu := b.Interface().(time.Time).UnixNano() if au != bu { - d.cl.add(UPDATE, path, a.Interface(), b.Interface()) + d.cl.Add(UPDATE, path, a.Interface(), b.Interface()) } return nil diff --git a/diff_uint.go b/diff_uint.go index 1f0edcb..1e16cb7 100644 --- a/diff_uint.go +++ b/diff_uint.go @@ -10,12 +10,12 @@ import ( func (d *Differ) diffUint(path []string, a, b reflect.Value) error { if a.Kind() == reflect.Invalid { - d.cl.add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, b.Interface()) return nil } if b.Kind() == reflect.Invalid { - d.cl.add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, a.Interface(), nil) return nil } @@ -25,9 +25,9 @@ func (d *Differ) diffUint(path []string, a, b reflect.Value) error { if a.Uint() != b.Uint() { if a.CanInterface() { - d.cl.add(UPDATE, path, a.Interface(), b.Interface()) + d.cl.Add(UPDATE, path, a.Interface(), b.Interface()) } else { - d.cl.add(UPDATE, path, a.Uint(), b.Uint()) + d.cl.Add(UPDATE, path, a.Uint(), b.Uint()) } } From e3d3ed6fbf7bceca77f9e23119d5255796dc790b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lovro=20Ma=C5=BEgon?= Date: Sat, 18 Apr 2020 16:53:20 +0200 Subject: [PATCH 07/56] expose function AreType --- diff.go | 2 +- diff_struct.go | 2 +- diff_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/diff.go b/diff.go index 691ad1f..4c34bba 100644 --- a/diff.go +++ b/diff.go @@ -256,7 +256,7 @@ func are(a, b reflect.Value, kinds ...reflect.Kind) bool { return amatch && bmatch } -func areType(a, b reflect.Value, types ...reflect.Type) bool { +func AreType(a, b reflect.Value, types ...reflect.Type) bool { var amatch, bmatch bool for _, t := range types { diff --git a/diff_struct.go b/diff_struct.go index 9b4afb3..2f7baad 100644 --- a/diff_struct.go +++ b/diff_struct.go @@ -10,7 +10,7 @@ import ( ) func (d *Differ) diffStruct(path []string, a, b reflect.Value) error { - if areType(a, b, reflect.TypeOf(time.Time{})) { + if AreType(a, b, reflect.TypeOf(time.Time{})) { return d.diffTime(path, a, b) } diff --git a/diff_test.go b/diff_test.go index 2fa4644..c7efd85 100644 --- a/diff_test.go +++ b/diff_test.go @@ -596,7 +596,7 @@ type testType string type testTypeDiffer struct{} func (testTypeDiffer) Match(a, b reflect.Value) bool { - return areType(a, b, reflect.TypeOf(testType(""))) + return AreType(a, b, reflect.TypeOf(testType(""))) } func (testTypeDiffer) Diff(cl *Changelog, path []string, a, b reflect.Value) error { if a.String() != "custom" && b.String() != "match" { From d71f8bacdfcff98444d8a3e2e342d2cc4213ab5f Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Sat, 18 Apr 2020 19:38:30 +0100 Subject: [PATCH 08/56] fix ci config --- .circleci/config.yml | 6 ------ Makefile | 8 -------- go.sum | 1 + 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ac4f6a2..2149e4d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,12 +6,6 @@ jobs: working_directory: /home/circleci/.go_workspace/src/github.com/r3labs/diff steps: - checkout - - run: - name: Install Dependencies - command: make dev-deps - - run: - name: Code Analysis - command: make lint - run: name: Unit Tests command: make test diff --git a/Makefile b/Makefile index f16fa85..0b1c346 100644 --- a/Makefile +++ b/Makefile @@ -6,11 +6,3 @@ test: cover: @go test -coverprofile cover.out - -dev-deps: - @go get -u github.com/stretchr/testify - @go get github.com/alecthomas/gometalinter - @gometalinter --install > /dev/null - -lint: - @gometalinter --vendor --disable-all --enable=errcheck --enable=golint --enable=vet ./... diff --git a/go.sum b/go.sum index a80206a..331fa69 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From 8808485bb5b6c15bd0d23ae7e2cb865241b7ebae Mon Sep 17 00:00:00 2001 From: Burak Nehbit Date: Wed, 29 Apr 2020 19:08:39 -0700 Subject: [PATCH 09/56] =?UTF-8?q?This=20allows=20custom=20differs=20to=20b?= =?UTF-8?q?e=20used=20as=20intermediate=20steps.=20Essentially,=20as=20it?= =?UTF-8?q?=20stands,=20once=20you=20go=20into=20a=20custom=20differ,=20yo?= =?UTF-8?q?u=20can't=20ever=20go=20back=20=E2=80=94=20there=20is=20no=20wa?= =?UTF-8?q?y=20to=20pass=20the=20control=20back=20to=20the=20standard=20di?= =?UTF-8?q?ffers.=20This=20adds=20another=20method=20to=20the=20interface?= =?UTF-8?q?=20that=20diff.Differ=20uses=20to=20pass=20the=20internal=20dif?= =?UTF-8?q?f()=20function=20into=20the=20custom=20differ.=20By=20calling?= =?UTF-8?q?=20that=20internal=20function,=20a=20custom=20differ=20can=20gi?= =?UTF-8?q?ve=20back=20control,=20and=20diffing=20can=20continue=20normall?= =?UTF-8?q?y.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A test case with recursive struct is also added. In the test case, the custom differ gives back control to the slice differ, and slice differ in turn calls the custom differ again, so as to be able to deal with recursive trees. --- diff.go | 1 + diff_test.go | 91 +++++++++++++++++++++++++++++++++++++++++++++++++--- options.go | 3 ++ 3 files changed, 90 insertions(+), 5 deletions(-) diff --git a/diff.go b/diff.go index 4c34bba..b1c20c7 100644 --- a/diff.go +++ b/diff.go @@ -51,6 +51,7 @@ type Change struct { type ValueDiffer interface { Match(a, b reflect.Value) bool Diff(cl *Changelog, path []string, a, b reflect.Value) error + InsertParentDiffer(dfunc func(path []string, a, b reflect.Value) error) } // Changed returns true if both values differ diff --git a/diff_test.go b/diff_test.go index c7efd85..03748c0 100644 --- a/diff_test.go +++ b/diff_test.go @@ -593,12 +593,18 @@ func TestDiffPrivateField(t *testing.T) { } type testType string -type testTypeDiffer struct{} +type testTypeDiffer struct { + DiffFunc (func(path []string, a, b reflect.Value) error) +} + +func (o *testTypeDiffer) InsertParentDiffer(dfunc func(path []string, a, b reflect.Value) error) { + o.DiffFunc = dfunc +} -func (testTypeDiffer) Match(a, b reflect.Value) bool { +func (o *testTypeDiffer) Match(a, b reflect.Value) bool { return AreType(a, b, reflect.TypeOf(testType(""))) } -func (testTypeDiffer) Diff(cl *Changelog, path []string, a, b reflect.Value) error { +func (o *testTypeDiffer) Diff(cl *Changelog, path []string, a, b reflect.Value) error { if a.String() != "custom" && b.String() != "match" { cl.Add(UPDATE, path, a.Interface(), b.Interface()) } @@ -612,7 +618,7 @@ func TestCustomDiffer(t *testing.T) { d, err := NewDiffer( CustomValueDiffers( - testTypeDiffer{}, + &testTypeDiffer{}, ), ) require.Nil(t, err) @@ -624,7 +630,7 @@ func TestCustomDiffer(t *testing.T) { d, err = NewDiffer( CustomValueDiffers( - testTypeDiffer{}, + &testTypeDiffer{}, ), ) require.Nil(t, err) @@ -634,3 +640,78 @@ func TestCustomDiffer(t *testing.T) { assert.Len(t, cl, 1) } + +type RecursiveTestStruct struct { + Id int + Children []RecursiveTestStruct +} + +type recursiveTestStructDiffer struct { + DiffFunc (func(path []string, a, b reflect.Value) error) +} + +func (o *recursiveTestStructDiffer) InsertParentDiffer(dfunc func(path []string, a, b reflect.Value) error) { + o.DiffFunc = dfunc +} + +func (o *recursiveTestStructDiffer) Match(a, b reflect.Value) bool { + return AreType(a, b, reflect.TypeOf(RecursiveTestStruct{})) +} + +func (o *recursiveTestStructDiffer) Diff(cl *Changelog, path []string, a, b reflect.Value) error { + if a.Kind() == reflect.Invalid { + cl.Add(CREATE, path, nil, b.Interface()) + return nil + } + if b.Kind() == reflect.Invalid { + cl.Add(DELETE, path, a.Interface(), nil) + return nil + } + var awt, bwt RecursiveTestStruct + awt, _ = a.Interface().(RecursiveTestStruct) + bwt, _ = b.Interface().(RecursiveTestStruct) + if awt.Id != bwt.Id { + cl.Add(UPDATE, path, a.Interface(), b.Interface()) + } + for i := 0; i < a.NumField(); i++ { + field := a.Type().Field(i) + tname := field.Name + if tname != "Children" { + continue + } + af := a.Field(i) + bf := b.FieldByName(field.Name) + fpath := copyAppend(path, tname) + err := o.DiffFunc(fpath, af, bf) + if err != nil { + return err + } + } + return nil +} + +func TestRecursiveCustomDiffer(t *testing.T) { + treeA := RecursiveTestStruct{ + Id: 1, + Children: []RecursiveTestStruct{}, + } + + treeB := RecursiveTestStruct{ + Id: 1, + Children: []RecursiveTestStruct{ + RecursiveTestStruct{ + Id: 4, + Children: []RecursiveTestStruct{}, + }, + }, + } + d, err := NewDiffer( + CustomValueDiffers( + &recursiveTestStructDiffer{}, + ), + ) + require.Nil(t, err) + cl, err := d.Diff(treeA, treeB) + require.Nil(t, err) + assert.Len(t, cl, 1) +} diff --git a/options.go b/options.go index 1807378..ba86587 100644 --- a/options.go +++ b/options.go @@ -21,6 +21,9 @@ func DisableStructValues() func(d *Differ) error { func CustomValueDiffers(vd ...ValueDiffer) func(d *Differ) error { return func(d *Differ) error { d.customValueDiffers = append(d.customValueDiffers, vd...) + for k, _ := range d.customValueDiffers { + d.customValueDiffers[k].InsertParentDiffer(d.diff) + } return nil } } From 5d6c21f8cb2d7024a291b836d8b6ac20a52898e1 Mon Sep 17 00:00:00 2001 From: Burak Nehbit Date: Wed, 29 Apr 2020 19:43:47 -0700 Subject: [PATCH 10/56] I have to change this to be able to use this, otherwise go-get fails. --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 7c30699..498a49a 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/r3labs/diff +module github.com/nehbit/diff go 1.13 From 6aab0e34361db942615f54e7f89ddcf20f513d33 Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Mon, 1 Jun 2020 09:54:09 +0100 Subject: [PATCH 11/56] fix go mod file --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 498a49a..7c30699 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/nehbit/diff +module github.com/r3labs/diff go 1.13 From b44e196bcb97eb695fd43943d16d83037ba60dbb Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Mon, 1 Jun 2020 15:24:31 +0100 Subject: [PATCH 12/56] add custom differ --- diff.go | 21 +++++++++++++-------- diff_comparative.go | 6 +++--- diff_slice.go | 6 +++--- diff_struct.go | 6 +++--- options.go | 8 ++++++++ 5 files changed, 30 insertions(+), 17 deletions(-) diff --git a/diff.go b/diff.go index b1c20c7..e0b5a96 100644 --- a/diff.go +++ b/diff.go @@ -30,6 +30,7 @@ const ( // Differ a configurable diff instance type Differ struct { + TagName string SliceOrdering bool DisableStructValues bool customValueDiffers []ValueDiffer @@ -62,14 +63,18 @@ func Changed(a, b interface{}) bool { // Diff returns a changelog of all mutated values from both func Diff(a, b interface{}) (Changelog, error) { - var d Differ + d := Differ{ + TagName: "diff", + } return d.cl, d.diff([]string{}, reflect.ValueOf(a), reflect.ValueOf(b)) } // NewDiffer creates a new configurable diffing object func NewDiffer(opts ...func(d *Differ) error) (*Differ, error) { - var d Differ + d := Differ{ + TagName: "diff", + } for _, opt := range opts { err := opt(&d) @@ -164,8 +169,8 @@ func (cl *Changelog) Add(t string, path []string, from, to interface{}) { }) } -func tagName(f reflect.StructField) string { - t := f.Tag.Get("diff") +func tagName(tag string, f reflect.StructField) string { + t := f.Tag.Get(tag) parts := strings.Split(t, ",") if len(parts) < 1 { @@ -175,9 +180,9 @@ func tagName(f reflect.StructField) string { return parts[0] } -func identifier(v reflect.Value) interface{} { +func identifier(tag string, v reflect.Value) interface{} { for i := 0; i < v.NumField(); i++ { - if hasTagOption(v.Type().Field(i), "identifier") { + if hasTagOption(tag, v.Type().Field(i), "identifier") { return v.Field(i).Interface() } } @@ -185,8 +190,8 @@ func identifier(v reflect.Value) interface{} { return nil } -func hasTagOption(f reflect.StructField, opt string) bool { - parts := strings.Split(f.Tag.Get("diff"), ",") +func hasTagOption(tag string, f reflect.StructField, opt string) bool { + parts := strings.Split(f.Tag.Get(tag), ",") if len(parts) < 2 { return false } diff --git a/diff_comparative.go b/diff_comparative.go index 83cad41..670f0a2 100644 --- a/diff_comparative.go +++ b/diff_comparative.go @@ -31,13 +31,13 @@ func (d *Differ) diffComparative(path []string, c *ComparativeList) error { return nil } -func comparative(a, b reflect.Value) bool { +func (d *Differ) comparative(a, b reflect.Value) bool { if a.Len() > 0 { ae := a.Index(0) ak := getFinalValue(ae) if ak.Kind() == reflect.Struct { - if identifier(ak) != nil { + if identifier(d.TagName, ak) != nil { return true } } @@ -48,7 +48,7 @@ func comparative(a, b reflect.Value) bool { bk := getFinalValue(be) if bk.Kind() == reflect.Struct { - if identifier(bk) != nil { + if identifier(d.TagName, bk) != nil { return true } } diff --git a/diff_slice.go b/diff_slice.go index 27143fa..f164541 100644 --- a/diff_slice.go +++ b/diff_slice.go @@ -23,7 +23,7 @@ func (d *Differ) diffSlice(path []string, a, b reflect.Value) error { return ErrTypeMismatch } - if comparative(a, b) { + if d.comparative(a, b) { return d.diffSliceComparative(path, a, b) } @@ -66,7 +66,7 @@ func (d *Differ) diffSliceComparative(path []string, a, b reflect.Value) error { ae := a.Index(i) ak := getFinalValue(ae) - id := identifier(ak) + id := identifier(d.TagName, ak) if id != nil { c.addA(id, &ae) } @@ -76,7 +76,7 @@ func (d *Differ) diffSliceComparative(path []string, a, b reflect.Value) error { be := b.Index(i) bk := getFinalValue(be) - id := identifier(bk) + id := identifier(d.TagName, bk) if id != nil { c.addB(id, &be) } diff --git a/diff_struct.go b/diff_struct.go index 2f7baad..d16a21f 100644 --- a/diff_struct.go +++ b/diff_struct.go @@ -32,9 +32,9 @@ func (d *Differ) diffStruct(path []string, a, b reflect.Value) error { for i := 0; i < a.NumField(); i++ { field := a.Type().Field(i) - tname := tagName(field) + tname := tagName(d.TagName, field) - if tname == "-" || hasTagOption(field, "immutable") { + if tname == "-" || hasTagOption(d.TagName, field, "immutable") { continue } @@ -76,7 +76,7 @@ func (d *Differ) structValues(t string, path []string, a reflect.Value) error { for i := 0; i < a.NumField(); i++ { field := a.Type().Field(i) - tname := tagName(field) + tname := tagName(d.TagName, field) if tname == "-" { continue diff --git a/options.go b/options.go index ba86587..c8456d1 100644 --- a/options.go +++ b/options.go @@ -8,6 +8,14 @@ func SliceOrdering(enabled bool) func(d *Differ) error { } } +// TagName sets the tag name to use when getting field names and options +func TagName(tag string) func(d *Differ) error { + return func(d *Differ) error { + d.TagName = tag + return nil + } +} + // DisableStructValues disables populating a separate change for each item in a struct, // where the struct is being compared to a nil value func DisableStructValues() func(d *Differ) error { From c09284b20b46c9e21de7a21247ad4b3310252388 Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Mon, 1 Jun 2020 15:29:14 +0100 Subject: [PATCH 13/56] fix structvalues setup --- diff.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/diff.go b/diff.go index e0b5a96..9c75fb7 100644 --- a/diff.go +++ b/diff.go @@ -90,7 +90,10 @@ func NewDiffer(opts ...func(d *Differ) error) (*Differ, error) { // values are stored as "created" or "deleted" entries in the changelog, // depending on the change type specified func StructValues(t string, path []string, s interface{}) (Changelog, error) { - var d Differ + d := Differ{ + TagName: "diff", + } + v := reflect.ValueOf(s) return d.cl, d.structValues(t, path, v) From 406746115cb7ce23cc12b9d602ee30b9f1f9e4e4 Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Sat, 27 Jun 2020 11:11:50 +0100 Subject: [PATCH 14/56] reset internal state on each call to Diff() --- diff.go | 5 ++++- diff_test.go | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/diff.go b/diff.go index 9c75fb7..371c8d9 100644 --- a/diff.go +++ b/diff.go @@ -93,7 +93,7 @@ func StructValues(t string, path []string, s interface{}) (Changelog, error) { d := Differ{ TagName: "diff", } - + v := reflect.ValueOf(s) return d.cl, d.structValues(t, path, v) @@ -114,6 +114,9 @@ func (cl *Changelog) Filter(path []string) Changelog { // Diff returns a changelog of all mutated values from both func (d *Differ) Diff(a, b interface{}) (Changelog, error) { + // reset the state of the diff + d.cl = Changelog{} + return d.cl, d.diff([]string{}, reflect.ValueOf(a), reflect.ValueOf(b)) } diff --git a/diff_test.go b/diff_test.go index 03748c0..ac9e6f5 100644 --- a/diff_test.go +++ b/diff_test.go @@ -562,6 +562,29 @@ func TestStructValues(t *testing.T) { } } +func TestDifferReuse(t *testing.T) { + d, err := NewDiffer() + require.Nil(t, err) + + cl, err := d.Diff([]string{"1", "2", "3"}, []string{"1"}) + require.Nil(t, err) + + require.Len(t, cl, 2) + + assert.Equal(t, "2", cl[0].From) + assert.Equal(t, nil, cl[0].To) + assert.Equal(t, "3", cl[1].From) + assert.Equal(t, nil, cl[1].To) + + cl, err = d.Diff([]string{"a", "b"}, []string{"a", "c"}) + require.Nil(t, err) + + require.Len(t, cl, 1) + + assert.Equal(t, "b", cl[0].From) + assert.Equal(t, "c", cl[0].To) +} + func TestDiffingOptions(t *testing.T) { d, err := NewDiffer(SliceOrdering(false)) require.Nil(t, err) From 1b547005f9180342414b83b89b092f1c5a1d9605 Mon Sep 17 00:00:00 2001 From: Saulius Alisauskas Date: Thu, 16 Jul 2020 13:00:03 -0400 Subject: [PATCH 15/56] option to handle value type change --- diff.go | 5 ++++ diff_test.go | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++ options.go | 8 ++++++ 3 files changed, 92 insertions(+) diff --git a/diff.go b/diff.go index 371c8d9..d2c0173 100644 --- a/diff.go +++ b/diff.go @@ -35,6 +35,7 @@ type Differ struct { DisableStructValues bool customValueDiffers []ValueDiffer cl Changelog + AllowTypeMismatch bool } // Changelog stores a list of changed items @@ -123,6 +124,10 @@ func (d *Differ) Diff(a, b interface{}) (Changelog, error) { func (d *Differ) diff(path []string, a, b reflect.Value) error { // check if types match or are if invalid(a, b) { + if d.AllowTypeMismatch { + d.cl.Add(UPDATE, path, a.Interface(), b.Interface()) + return nil + } return ErrTypeMismatch } diff --git a/diff_test.go b/diff_test.go index ac9e6f5..68226b2 100644 --- a/diff_test.go +++ b/diff_test.go @@ -738,3 +738,82 @@ func TestRecursiveCustomDiffer(t *testing.T) { require.Nil(t, err) assert.Len(t, cl, 1) } + + +func TestHandleDifferentTypes(t *testing.T) { + cases := []struct { + Name string + A, B interface{} + Changelog Changelog + Error error + HandleTypeMismatch bool + }{ + { + "type-change-not-allowed-error", + 1, "1", + nil, + ErrTypeMismatch, + false, + }, + { + "type-change-not-allowed-error-struct", + struct { + p1 string + p2 int + }{"1", 1}, + struct{ + p1 string + p2 string + }{"1", "1"}, + nil, + ErrTypeMismatch, + false, + }, + { + "type-change-allowed", + 1, "1", + Changelog{ + Change{Type: UPDATE, Path: []string{}, From: 1, To: "1"}, + }, + nil, + true, + }, + { + "type-change-allowed-struct", + struct { + P1 string + P2 int + P3 map[string]string + }{"1", 1, map[string]string{"1": "1"}}, + struct{ + P1 string + P2 string + P3 string + }{"1", "1", "1"}, + Changelog{ + Change{Type: UPDATE, Path: []string{"P2"}, From: 1, To: "1"}, + Change{Type: UPDATE, Path: []string{"P3"}, From: map[string]string{"1": "1"}, To: "1"}, + }, + nil, + true, + }, + } + + for _, tc := range cases { + t.Run(tc.Name, func(t *testing.T) { + d, err := NewDiffer(AllowTypeMismatch(tc.HandleTypeMismatch)) + require.Nil(t, err) + cl, err := d.Diff(tc.A, tc.B) + + assert.Equal(t, tc.Error, err) + require.Equal(t, len(tc.Changelog), len(cl)) + + for i, c := range cl { + assert.Equal(t, tc.Changelog[i].Type, c.Type) + assert.Equal(t, tc.Changelog[i].Path, c.Path) + assert.Equal(t, tc.Changelog[i].From, c.From) + assert.Equal(t, tc.Changelog[i].To, c.To) + } + }) + } +} diff --git a/options.go b/options.go index c8456d1..5fd4d00 100644 --- a/options.go +++ b/options.go @@ -35,3 +35,11 @@ func CustomValueDiffers(vd ...ValueDiffer) func(d *Differ) error { return nil } } + +// AllowTypeMismatch changed behaviour to report value as "updated" when its type has changed instead of error +func AllowTypeMismatch(enabled bool) func(d *Differ) error { + return func(d *Differ) error { + d.AllowTypeMismatch = enabled + return nil + } +} From f3d34884da4dc7f8704f90e66b05030f306dc980 Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Sat, 18 Jul 2020 17:45:10 -0700 Subject: [PATCH 16/56] add filtering of struct fields --- diff.go | 6 +++++ diff_examples_test.go | 52 +++++++++++++++++++++++++++++++++++++++++++ diff_struct.go | 4 ++++ options.go | 8 +++++++ 4 files changed, 70 insertions(+) diff --git a/diff.go b/diff.go index d2c0173..b65c3c4 100644 --- a/diff.go +++ b/diff.go @@ -36,6 +36,7 @@ type Differ struct { customValueDiffers []ValueDiffer cl Changelog AllowTypeMismatch bool + Filter FilterFunc } // Changelog stores a list of changed items @@ -87,6 +88,11 @@ func NewDiffer(opts ...func(d *Differ) error) (*Differ, error) { return &d, nil } +// FilterFunc is a function that determines whether to descend into a struct field. +// parent is the struct being examined and field is a field on that struct. path +// is the path to the field from the root of the diff. +type FilterFunc func(path []string, parent reflect.Type, field reflect.StructField) bool + // StructValues gets all values from a struct // values are stored as "created" or "deleted" entries in the changelog, // depending on the change type specified diff --git a/diff_examples_test.go b/diff_examples_test.go index d492b7b..6cceba5 100644 --- a/diff_examples_test.go +++ b/diff_examples_test.go @@ -2,6 +2,7 @@ package diff import ( "fmt" + "reflect" ) func ExampleDiff() { @@ -63,3 +64,54 @@ func ExampleDiff() { fmt.Printf("%#v", changelog) // Produces: diff.Changelog{diff.Change{Type:"update", Path:[]string{"id"}, From:1, To:2}, diff.Change{Type:"update", Path:[]string{"name"}, From:"Green Apple", To:"Red Apple"}, diff.Change{Type:"create", Path:[]string{"nutrients", "2"}, From:interface {}(nil), To:"vitamin e"}, diff.Change{Type:"create", Path:[]string{"tags", "popularity"}, From:interface {}(nil), To:main.Tag{Name:"popularity", Value:"high"}}} } + +func ExampleFilter() { + type Tag struct { + Name string `diff:"name,identifier"` + Value string `diff:"value"` + } + + type Fruit struct { + ID int `diff:"id"` + Name string `diff:"name"` + Healthy bool `diff:"healthy"` + Nutrients []string `diff:"nutrients"` + Tags []Tag `diff:"tags"` + } + + a := Fruit{ + ID: 1, + Name: "Green Apple", + Healthy: true, + Nutrients: []string{ + "vitamin c", + "vitamin d", + }, + } + + b := Fruit{ + ID: 2, + Name: "Red Apple", + Healthy: true, + Nutrients: []string{ + "vitamin c", + "vitamin d", + "vitamin e", + }, + } + + d, err := NewDiffer(Filter(func(path []string, parent reflect.Type, field reflect.StructField) bool { + return field.Name != "Name" + })) + if err != nil { + panic(err) + } + + changelog, err := d.Diff(a, b) + if err != nil { + panic(err) + } + + fmt.Printf("%#v", changelog) + // Output: diff.Changelog{diff.Change{Type:"update", Path:[]string{"id"}, From:1, To:2}, diff.Change{Type:"create", Path:[]string{"nutrients", "2"}, From:interface {}(nil), To:"vitamin e"}} +} diff --git a/diff_struct.go b/diff_struct.go index d16a21f..56d0ca4 100644 --- a/diff_struct.go +++ b/diff_struct.go @@ -47,6 +47,10 @@ func (d *Differ) diffStruct(path []string, a, b reflect.Value) error { fpath := copyAppend(path, tname) + if d.Filter != nil && !d.Filter(fpath, a.Type(), field) { + continue + } + err := d.diff(fpath, af, bf) if err != nil { return err diff --git a/options.go b/options.go index 5fd4d00..ab56160 100644 --- a/options.go +++ b/options.go @@ -43,3 +43,11 @@ func AllowTypeMismatch(enabled bool) func(d *Differ) error { return nil } } + +// Filter allows you to determine which fields the differ descends into +func Filter(f FilterFunc) func(d *Differ) error { + return func(d *Differ) error { + d.Filter = f + return nil + } +} From 484340b7972af5480de2c4d3e9948d7309950dc9 Mon Sep 17 00:00:00 2001 From: Kyenos Date: Mon, 17 Aug 2020 12:19:44 -0400 Subject: [PATCH 17/56] My first attempt at a Patch / Merge implementation for Diff's change log format. Folded into diff's source tree. Features include: - Patch function which takes a change log and a pointer to a target - Merge function which takes origin and changed values as well as a pointer to a target - produces a PatchLog, similar to a ChangeLog in which what action was taken is described - Offers a couple of struct tag additions that effect how patch applies changes, these include: - omitunequal, which applies the strict rule that the target value must match the origin value or it will not get replaced - create, which allows for the creation of map and slice elements should they not already be present - Patch honors the "-" name in the diff tag's name portion. thus excluding said strut member from updates - Patch honors the diff struct tags 'name' and will apply the change to the target member or map appropriately. - There are examples in diff_examples-_test.go These help maintain the code bases unit test coverage percentage - There is a new error utility, which allows for nested link lists of errors in a special type. - The business of patching is fuzzy and errors don't necessarily mean the operation failed so patch is a 'best effort' operation and will fulfill all the changes in the change lot to the target as best it can. Audit / errors are contained in the PatchLog --- .gitignore | 2 + change_value.go | 154 ++++++++++++++++++++++++ diff.go | 7 -- diff_examples_test.go | 222 ++++++++++++++++++++++++++++++++++ error.go | 74 ++++++++++++ patch.go | 273 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 725 insertions(+), 7 deletions(-) create mode 100644 change_value.go create mode 100644 error.go create mode 100644 patch.go diff --git a/.gitignore b/.gitignore index a1338d6..f0608b1 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ + +patchflags_string.go \ No newline at end of file diff --git a/change_value.go b/change_value.go new file mode 100644 index 0000000..5ee4fb3 --- /dev/null +++ b/change_value.go @@ -0,0 +1,154 @@ +package diff + +import ( + "reflect" +) + +//Not strictly necessary but migh be nice in some cases +//go:generate stringer -type=PatchFlags +type PatchFlags uint32 +const ( + OptionCreate PatchFlags = 1 << iota + OptionOmitUnequal + FlagInvalidTarget + FlagApplied + FlagFailed + FlagCreated + FlagIgnored + FlagDeleted + FlagUpdated +) + +//ChangeValue is a specialized struct for monitoring patching +type ChangeValue struct { + val reflect.Value + flags PatchFlags + change *Change + err error + index int + key reflect.Value +} + +//PatchLogEntry defines how a DiffLog entry was applied +type PatchLogEntry struct{ + Path []string `json:"path"` + From interface{} `json:"from"` + To interface{} `json:"to"` + Flags PatchFlags `json:"flags"` + Errors error `json:"errors"` +} +type PatchLog []PatchLogEntry + +//NewChangeValue idiomatic constructor +func NewChangeValue(c Change, target interface{}) *ChangeValue{ + return &ChangeValue{ + val: reflect.ValueOf(target), + change: &c, + } +} + +//NewPatchLogEntry converts our complicated reflection based struct to +//a simpler format for the consumer +func NewPatchLogEntry(change *ChangeValue) PatchLogEntry { + return PatchLogEntry{ + Path: change.change.Path, + From: change.change.From, + To: change.change.To, + Flags: change.flags, + Errors: change.err, + } +} + +// Sets a flag on the node and saves the change +func (c *ChangeValue) SetFlag(flag PatchFlags) { + if c != nil { + c.flags = c.flags|flag + } +} + +//ClearFlag Clears a flag on the node and saves the change +func (c *ChangeValue) ClearFlags(){ + if c != nil {c.flags = 0} +} + +//HasFlag indicates if a flag is set on the node. returns false if node is bad +func (c *ChangeValue) HasFlag(flag PatchFlags) bool { + return (c.flags & flag) != 0 +} + +//CanSet echos the reflection can set +func (c ChangeValue) CanSet() bool { + return c.val.CanSet() +} + +//IsValid echo for is valid +func (c *ChangeValue) IsValid() bool { + if c != nil {return c.val.IsValid() || !c.HasFlag(FlagInvalidTarget)} + return false +} + +//Len echo for len +func (c ChangeValue) Len() int { + return c.val.Len() +} + +//Kind echos the reflection kind +func (c ChangeValue) Kind() reflect.Kind { + return c.val.Kind() +} + +//Type echos Type +func (c ChangeValue) Type() reflect.Type { + return c.val.Type() +} + +//Set echos reflect set +func (c *ChangeValue) Set(value reflect.Value){ + if c != nil { + defer func() { + if r := recover(); r != nil { + c.SetFlag(FlagFailed) + } + }() + c.val.Set(value) + c.SetFlag(FlagApplied) + } +} + +//SetMapValue is used to set a map value +func (c *ChangeValue) SetMapValue(key reflect.Value, value reflect.Value){ + if c != nil { + defer func() { + if r := recover(); r != nil { + c.SetFlag(FlagFailed) + } + }() + c.val.SetMapIndex(key, value) + } +} + +//Index echo for index +func (c ChangeValue) Index(i int) reflect.Value { + return c.val.Index(i) +} + +//Interface gets the interface for the value +func (c ChangeValue) Interface() interface{} { + return c.val.Interface() +} + +//IsNil echo for is nil +func (c ChangeValue) IsNil() bool { + return c.val.IsNil() +} + +//KeyType returns the key type of a map if it is one +func (c ChangeValue) KeyType() reflect.Type { + return c.Type().Key() +} + +//AddError appends errors to this change value +func (c *ChangeValue) AddError(err error) *ChangeValue{ + if c != nil {c.err = err} + return c +} \ No newline at end of file diff --git a/diff.go b/diff.go index b65c3c4..c415dc4 100644 --- a/diff.go +++ b/diff.go @@ -12,13 +12,6 @@ import ( "strings" ) -var ( - // ErrTypeMismatch Compared types do not match - ErrTypeMismatch = errors.New("types do not match") - // ErrInvalidChangeType The specified change values are not unsupported - ErrInvalidChangeType = errors.New("change type must be one of 'create' or 'delete'") -) - const ( // CREATE represents when an element has been added CREATE = "create" diff --git a/diff_examples_test.go b/diff_examples_test.go index 6cceba5..2b61f84 100644 --- a/diff_examples_test.go +++ b/diff_examples_test.go @@ -5,6 +5,228 @@ import ( "reflect" ) +//Try to do a bunch of stuff that will result in some or all failures +//when trying to apply either a valid or invalid changelog +func ExamplePatchWithErrors(){ + + type Fruit struct { + ID int `diff:"ID" json:"Identifier"` + Name string `diff:"name"` + Healthy bool `diff:"-"` + Nutrients []string `diff:"nutrients"` + Labels map[string]int `diff:"labs"` + } + + type Bat struct { + ID string `diff:"ID"` + Name string `diff:"-"` + } + + a := Fruit{ + ID: 1, + Name: "Green Apple", + Healthy: true, + Nutrients: []string{ + "vitamin a", + "vitamin b", + "vitamin c", + "vitamin d", + }, + Labels: make(map[string]int), + } + a.Labels["likes"] = 10 + a.Labels["colors"] = 2 + + b := Fruit{ + ID: 2, + Name: "Red Apple", + Healthy: true, + Nutrients: []string{ + "vitamin c", + "vitamin d", + "vitamin e", + }, + Labels: make(map[string]int), + } + b.Labels["forests"] = 1223 + b.Labels["colors"] = 1222 + + c := Fruit{ + Labels: make(map[string]int), + Nutrients: []string{ + "vitamin c", + "vitamin d", + "vitamin a", + }, + } + c.Labels["likes"] = 21 + c.Labels["colors"] = 42 + + d := Bat{ + ID: "first", + Name: "second", + } + + changelog, err := Diff(a, b) + if err != nil { + panic(err) + } + + //This fails in total because c is not assignable (passed by value) + patchLog := Patch(changelog, c) + + //this also demonstrated the nested errors with 'next' + errors := patchLog[7].Errors.(*DiffError) + + //we can also continue to nest errors if we like + message := errors.WithCause(NewError("This is a custom message")). + WithCause(fmt.Errorf("this is an error from somewhere else but still compatible")). + Error() + + //invoke a few failures, i.e. bad changelog + changelog[2].Path[1] = "bad index" + changelog[3].Path[0] = "bad struct field" + + patchLog = Patch(changelog, &c) + + patchLog, _ = Merge(a,nil, &c) + + patchLog, _ = Merge(a, d, &c) + + //try patching a string + patchLog = Patch(changelog, message) + + //test an invalid change value + var bad *ChangeValue + if bad.IsValid() { + fmt.Print("this should never happen") + } + + //Output: +} + +//ExampleMerge demonstrates how to use the Merge function +func ExampleMerge() { + type Fruit struct { + ID int `diff:"ID" json:"Identifier"` + Name string `diff:"name"` + Healthy bool `diff:"healthy"` + Nutrients []string `diff:"nutrients,create,omitunequal"` + Labels map[string]int `diff:"labs,create"` + } + + a := Fruit{ + ID: 1, + Name: "Green Apple", + Healthy: true, + Nutrients: []string{ + "vitamin a", + "vitamin b", + "vitamin c", + "vitamin d", + }, + Labels: make(map[string]int), + } + a.Labels["likes"] = 10 + a.Labels["colors"] = 2 + + b := Fruit{ + ID: 2, + Name: "Red Apple", + Healthy: true, + Nutrients: []string{ + "vitamin c", + "vitamin d", + "vitamin e", + }, + Labels: make(map[string]int), + } + b.Labels["forests"] = 1223 + b.Labels["colors"] = 1222 + + c := Fruit{ + Labels: make(map[string]int), + Nutrients: []string{ + "vitamin a", + "vitamin c", + "vitamin d", + }, + } + c.Labels["likes"] = 21 + c.Labels["colors"] = 42 + + //the only error that can happen here comes from the diff step + patchLog, _ := Merge(a,b, &c) + + //Note that unlike our patch version we've not included 'create' in the + //tag for nutrients. This will omit "vitamin e" from ending up in c + fmt.Printf("%#v", len(patchLog)) + + //Output: 8 +} + +//ExamplePatch demonstrates how to use the Patch function +func ExamplePatch(){ + + type Fruit struct { + ID int `diff:"ID" json:"Identifier"` + Name string `diff:"name"` + Healthy bool `diff:"healthy"` + Nutrients []string `diff:"nutrients,create,omitunequal"` + Labels map[string]int `diff:"labs,create"` + } + + a := Fruit{ + ID: 1, + Name: "Green Apple", + Healthy: true, + Nutrients: []string{ + "vitamin a", + "vitamin b", + "vitamin c", + "vitamin d", + }, + Labels: make(map[string]int), + } + a.Labels["likes"] = 10 + a.Labels["colors"] = 2 + + b := Fruit{ + ID: 2, + Name: "Red Apple", + Healthy: true, + Nutrients: []string{ + "vitamin c", + "vitamin d", + "vitamin e", + }, + Labels: make(map[string]int), + } + b.Labels["forests"] = 1223 + b.Labels["colors"] = 1222 + + c := Fruit{ + Labels: make(map[string]int), + Nutrients: []string{ + "vitamin a", + "vitamin c", + "vitamin d", + }, + } + c.Labels["likes"] = 21 + + changelog, err := Diff(a, b) + if err != nil { + panic(err) + } + + patchLog := Patch(changelog, &c) + + fmt.Printf("%#v", len(patchLog)) + + //Output: 8 +} + func ExampleDiff() { type Tag struct { Name string `diff:"name,identifier"` diff --git a/error.go b/error.go new file mode 100644 index 0000000..a8986ae --- /dev/null +++ b/error.go @@ -0,0 +1,74 @@ +package diff + +import ( + "fmt" +) + +var ( + // ErrTypeMismatch Compared types do not match + ErrTypeMismatch = NewError("types do not match") + // ErrInvalidChangeType The specified change values are not unsupported + ErrInvalidChangeType = NewError("change type must be one of 'create' or 'delete'") +) + +//our own version of an error, which can wrap others +type DiffError struct { + count int + message string + next error +} + +//Unwrap implement 1.13 unwrap feature for compatibility +func(s *DiffError) Unwrap() error { + return s.next +} + +//Error implements the error interface +func (s DiffError) Error() string { + cause := "" + if s.next != nil { + cause = s.next.Error() + } + return fmt.Sprintf(" %s (cause count %d)\n%s", s.message, s.count, cause) +} + +//AppendCause appends a new cause error to the chain +func (s *DiffError) WithCause(err error) *DiffError { + if s != nil && err != nil { + s.count++ + if s.next != nil { + if v, ok := err.(DiffError); ok { + s.next = v.WithCause(s.next) + } else if v, ok := err.(*DiffError); ok { + s.next = v.WithCause(s.next) + } else { + v = &DiffError{ + message: "auto wrapped error", + next: err, + } + s.next = v.WithCause(s.next) + } + } else { + s.next = err + } + } + return s +} + +//NewErrorf just give me a plain error with formatting +func NewErrorf(format string, messages ...interface{}) *DiffError { + return &DiffError{ + message: fmt.Sprintf(format, messages...), + } +} + +//NewError just give me a plain error +func NewError(message string, causes ...error) *DiffError { + s := &DiffError{ + message: message, + } + for _, cause := range causes { + s.WithCause(cause) + } + return s +} diff --git a/patch.go b/patch.go new file mode 100644 index 0000000..3472d8d --- /dev/null +++ b/patch.go @@ -0,0 +1,273 @@ +package diff + +import ( + "reflect" + "strconv" +) + +/** + This is a method of applying a changelog to a value or struct. change logs + should be generated with Diff and never manually created. This DOES NOT + apply fuzzy logic as would be in the case of a text patch. It does however + have a few additional features added to our struct tags. + + 1) create. This tag on a struct field indicates that the patch should + create the value if it's not there. I.e. if it's nil. This works for + pointers, maps and slices. + + 2) omitunequal. Generally, you don't want to do this, the expectation is + that if an item isn't there, you want to add it. For example, if your + diff shows an array element at index 6 is a string 'hello' but your target + only has 3 elements, none of them matching... you want to add 'hello' + regardless of the index. (think in a distributed context, another process + may have deleted more than one entry and 'hello' may no longer be in that + indexed spot. + + So given this scenario, the default behavior is to scan for the previous + value and replace it anyway, or simply append the new value. For maps the + default behavior is to simply add the key if it doesn't match. + + However, if you don't like the default behavior, and add the omitunequal + tag to your struct, patch will *NOT* update an array or map with the key + or array value unless they key or index contains a 'match' to the + previous value. In which case it will skip over that change. + + Patch is implemented as a best effort algorithm. That means you can receive + multiple nested errors and still successfully have a modified target. This + may even be acceptable depending on your use case. So keep in mind, just + because err != nil *DOESN'T* mean that the patch didn't accomplish your goal + in setting those changes that are actually available. For example, you may + diff two structs of the same type, then attempt to apply to an entirely + different struct that is similar in constitution (think interface here) and + you may in fact get all of the values populated you wished to anyway. + */ + +// Merge is a convenience function that diffs, the original and changed items +// and merges said changes with target all in one call. +func Merge(original interface{}, changed interface{}, target interface{}) (PatchLog, error) { + if cl, err := Diff(original, changed); err == nil { + return Patch(cl, target), nil + }else{ + return nil, err + } +} + +//Patch... the missing feature. +func Patch(cl Changelog, target interface{}) (ret PatchLog) { + for _, c := range cl { + ret = append(ret, NewPatchLogEntry(patch(NewChangeValue(c, target)))) + } + return ret +} + +//patch apply an individual change as opposed to a change log +func patch(c *ChangeValue) (cv *ChangeValue){ + + //Can be messy to clean up when doing reflection + defer func() { + if r := recover(); r != nil { + cv.AddError(NewError("cannot set values on target", + NewError("passed by value not reference"))) + cv.SetFlag(FlagInvalidTarget) + } + }() + cv = c + cv.val = cv.val.Elem() + + //resolve where we're actually going to set this value + cv.index = -1 + if cv.Kind() == reflect.Struct { //substitute and solve for t (path) + for _, p := range cv.change.Path { + if cv.Kind() == reflect.Slice { + //field better be an index of the slice + if cv.index, cv.err = strconv.Atoi(p); cv.err != nil { + return cv.AddError(NewErrorf("invalid index in path: %s", p). + WithCause(cv.err)) + } + break //last item in a path that's a slice is it's index + } + if cv.Kind() == reflect.Map { + keytype := cv.KeyType() + cv.key = reflect.ValueOf(p) + cv.key.Convert(keytype) + break //same as a slice + } + cv = renderTargetField(cv, p) + } + } + + //we have to know that the new element we're trying to set is valid + if !cv.CanSet(){ + cv.SetFlag(FlagInvalidTarget) + return cv.AddError(NewError("cannot set values on target", + NewError("passed by value not reference"))) + } + + switch cv.change.Type { + case DELETE: + deleteOperation(cv) + case UPDATE, CREATE: + updateOperation(cv) + } + return cv +} + +//renderTargetField this interrogates the path and returns the correct value to +//change. Note that his is a recursion, t is not the same value as ret +func renderTargetField(t *ChangeValue, field string) (ret *ChangeValue) { + ret = t + //substitute and solve for t (path) + switch t.val.Kind() { + case reflect.Struct: + for i := 0; i < t.val.NumField(); i++ { + f := t.val.Type().Field(i) + tname := tagName("diff", f) + if tname == "-" || hasTagOption("diff", f, "immutable") { + continue + } + if tname == field || f.Name == field{ + ret = &ChangeValue{ + val: t.val.Field(i), + change: t.change, + } + ret.ClearFlags() + if hasTagOption("diff", f, "create") { + ret.SetFlag(OptionCreate) + } + if hasTagOption("diff", f, "omitunequal"){ + ret.SetFlag(OptionOmitUnequal) + } + } + } + default: + ret = &ChangeValue{ + val: t.val, + change: t.change, + } + } + if !ret.IsValid() { + ret.AddError(NewErrorf("Unable to access path value %v. Target field is invalid", field)) + } + return +} + +//deleteOperation takes out some of the cyclomatic complexity from the patch fuction +func deleteOperation(cv *ChangeValue) { + switch cv.Kind() { + case reflect.Slice: + var x reflect.Value + if cv.Len() > cv.index { + x = cv.Index(cv.index) + } + found := true + if !x.IsValid() || !reflect.DeepEqual(x.Interface(), cv.change.From) { + found = false + if !cv.HasFlag(OptionOmitUnequal){ + cv.AddError(NewErrorf("value index %d is invalid", cv.index). + WithCause(NewError("scanning for value index"))) + for i := 0; i < cv.Len(); i++ { + x = cv.Index(i) + if reflect.DeepEqual(x, cv.change.From) { + cv.AddError(NewErrorf("value changed index to %d", i)) + found = true + cv.index = i + } + } + } + } + if x.IsValid() && found{ + cv.val.Index(cv.index).Set(cv.val.Index(cv.Len() - 1)) + cv.val.Set(cv.val.Slice(0, cv.Len() - 1)) + cv.SetFlag(FlagDeleted) + }else{ + cv.SetFlag(FlagIgnored) + cv.AddError(NewError("Unable to find matching slice index entry")) + } + case reflect.Map: + if !reflect.DeepEqual(cv.change.From, cv.Interface()) && + cv.HasFlag(OptionOmitUnequal){ + cv.SetFlag(FlagIgnored) + cv.AddError(NewError("target change doesn't match original")) + return + } + if cv.IsNil() { + cv.SetFlag(FlagIgnored) + cv.AddError(NewError("target has nil map nothing to delete")) + return + }else{ + cv.SetFlag(FlagDeleted) + cv.SetMapValue(cv.key, reflect.Value{}) + } + default: + cv.Set(reflect.Zero(cv.Type())) + cv.SetFlag(FlagDeleted) + } +} + +//updateOperation takes out some of the cyclomatic complexity from the patch fuction +func updateOperation(cv *ChangeValue) { + switch cv.Kind() { + case reflect.Slice: + var x reflect.Value + if cv.Len() > cv.index { + x = cv.Index(cv.index) + } + found := true + if !x.IsValid() || !reflect.DeepEqual(x.Interface(), cv.change.From) { + found = false + if !cv.HasFlag(OptionOmitUnequal){ + cv.AddError(NewErrorf("value index %d is invalid", cv.index). + WithCause(NewError("scanning for value index"))) + for i := 0; i < cv.Len(); i++ { + x = cv.Index(i) + if reflect.DeepEqual(x, cv.change.From) { + cv.AddError(NewErrorf("value changed index to %d", i)) + found = true + } + } + } + } + if x.IsValid() && found{ + x.Set(reflect.ValueOf(cv.change.To)) + cv.SetFlag(FlagUpdated) + }else if cv.HasFlag(OptionCreate) && cv.change.Type == CREATE { + cv.Set(reflect.Append(cv.val, reflect.ValueOf(cv.change.To))) + cv.SetFlag(FlagCreated) + }else{ + cv.AddError(NewError("Unable to find matching slice index entry")) + cv.SetFlag(FlagIgnored) + } + case reflect.Map: + if !reflect.DeepEqual(cv.change.From, cv.Interface()) && + cv.HasFlag(OptionOmitUnequal){ + cv.SetFlag(FlagIgnored) + cv.AddError(NewError("target change doesn't match original")) + return + } + if cv.IsNil() { + if cv.HasFlag(OptionCreate) { + nm := reflect.MakeMap(cv.Type()) + nm.SetMapIndex(cv.key, reflect.ValueOf(cv.change.To)) + cv.Set(nm) + cv.SetFlag(FlagCreated) + }else{ + cv.SetFlag(FlagIgnored) + cv.AddError(NewError("target has nil map and create not set")) + return + } + }else{ + cv.SetFlag(FlagUpdated) + cv.SetMapValue(cv.key, reflect.ValueOf(cv.change.To)) + } + default: + if !reflect.DeepEqual(cv.change.From, cv.Interface()) && + cv.HasFlag(OptionOmitUnequal){ + cv.SetFlag(FlagIgnored) + cv.AddError(NewError("target change doesn't match original")) + return + } + cv.Set(reflect.ValueOf(cv.change.To)) + cv.SetFlag(FlagUpdated) + } +} + From d36c3254c9d2d04be387eaaa487260a75d062bf9 Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Sun, 6 Sep 2020 16:01:57 +0100 Subject: [PATCH 18/56] update readme for v2 branch and move to travis ci --- .travis.yml | 8 ++++++++ Makefile | 3 +++ README.md | 41 +++++++++++++++++++++++------------------ 3 files changed, 34 insertions(+), 18 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..17952b0 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: go + +go: + - "1.14" + - master + +before_script: + - make deps diff --git a/Makefile b/Makefile index 0b1c346..f119f8c 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,9 @@ install: go install -v ${LDFLAGS} +deps: + go get github.com/stretchr/testify + test: @go test -v -cover ./... diff --git a/README.md b/README.md index 0248536..74d9eff 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,16 @@ -# diff +# Diff [![PkgGoDev](https://pkg.go.dev/badge/github.com/r3labs/diff)](https://pkg.go.dev/github.com/r3labs/diff) [![Go Report Card](https://goreportcard.com/badge/github.com/r3labs/diff)](https://goreportcard.com/report/github.com/r3labs/diff) [![Build Status](https://travis-ci.org/r3labs/diff.svg?branch=master)](https://travis-ci.org/r3labs/diff) A library for diffing golang structures and values. Utilizing field tags and reflection, it is able to compare two structures of the same type and create a changelog of all modified values. The produced changelog can easily be serialized to json. -## Build status - -* Master [![CircleCI](https://circleci.com/gh/r3labs/diff/tree/master.svg?style=svg)](https://circleci.com/gh/r3labs/diff/tree/master) +NOTE: All active development now takes place on the v2 branch. ## Installation +For version 2: ``` -go get github.com/r3labs/diff +go get github.com/r3labs/diff/v2 ``` ## Changelog Format @@ -51,23 +50,30 @@ Change{ A diffable value can be/contain any of the following types: -* struct -* slice -* string -* int -* bool -* map -* pointer -### Tags +| Type | Supported | +|--------------|-----------| +| struct | ✔ | +| slice | ✔ | +| string | ✔ | +| int | ✔ | +| bool | ✔ | +| map | ✔ | +| pointer | ✔ | +| custom types | ✔ | -In order for struct fields to be compared, they must be tagged with a given name. All tag values are prefixed with `diff`. i.e. `diff:"items"`. -* `-` : In the event that you want to exclude a value from the diff, you can use the tag `diff:"-"` and the field will be ignored. +Please see the docs for more supported types, options and features. -* `identifier` : If you need to compare arrays by a matching identifier and not based on order, you can specify the `identifier` tag. If an identifiable element is found in both the from and to structures, they will be directly compared. i.e. `diff:"name,identifier"` +### Tags -* `immutable` : Will omit this struct field from diffing. When using `diff.StructValues()` these values will be added to the returned changelog. It's usecase is for when we have nothing to compare a struct to and want to show all of its relevant values. +In order for struct fields to be compared, they must be tagged with a given name. All tag values are prefixed with `diff`. i.e. `diff:"items"`. + +| Tag | Usage | +|--------------|------------------------------------| +| `-` | Excludes a value from being diffed | +| `identifier` | If you need to compare arrays by a matching identifier and not based on order, you can specify the `identifier` tag. If an identifiable element is found in both the from and to structures, they will be directly compared. i.e. `diff:"name, identifier"` | +| `imutable` | Will omit this struct field from diffing. When using `diff.StructValues()` these values will be added to the returned changelog. It's usecase is for when we have nothing to compare a struct to and want to show all of its relevant values. | ## Usage @@ -150,7 +156,6 @@ Supported options are: `SliceOrdering` ensures that the ordering of items in a slice is taken into account - ## Running Tests ``` From 7117c5ab12941e9ca11627ac713dc94186313492 Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Sun, 6 Sep 2020 16:03:32 +0100 Subject: [PATCH 19/56] fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 74d9eff..266eb04 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ In order for struct fields to be compared, they must be tagged with a given name |--------------|------------------------------------| | `-` | Excludes a value from being diffed | | `identifier` | If you need to compare arrays by a matching identifier and not based on order, you can specify the `identifier` tag. If an identifiable element is found in both the from and to structures, they will be directly compared. i.e. `diff:"name, identifier"` | -| `imutable` | Will omit this struct field from diffing. When using `diff.StructValues()` these values will be added to the returned changelog. It's usecase is for when we have nothing to compare a struct to and want to show all of its relevant values. | +| `immutable` | Will omit this struct field from diffing. When using `diff.StructValues()` these values will be added to the returned changelog. It's usecase is for when we have nothing to compare a struct to and want to show all of its relevant values. | ## Usage From 7936b7c11fa6c87bc6a26899dde88979127b8b7f Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Sun, 6 Sep 2020 16:06:13 +0100 Subject: [PATCH 20/56] remove circle ci config --- .circleci/config.yml | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 2149e4d..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,11 +0,0 @@ -version: 2 -jobs: - build: - docker: - - image: circleci/golang:1.11 - working_directory: /home/circleci/.go_workspace/src/github.com/r3labs/diff - steps: - - checkout - - run: - name: Unit Tests - command: make test From 8d010b79b0662cf5e6a2d22057584eaef19d7483 Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Sun, 6 Sep 2020 16:14:54 +0100 Subject: [PATCH 21/56] update go mod to v2 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 7c30699..9fee1db 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/r3labs/diff +module github.com/r3labs/diff/v2 go 1.13 From 1a39d951535866043b161d0f0ca74be228891bbe Mon Sep 17 00:00:00 2001 From: Kyenos Date: Wed, 14 Oct 2020 12:34:56 -0400 Subject: [PATCH 22/56] Re-worked patch engine. A number of edge cases, bugs and just flat out assumptions caused me to re-think the underlying reflection code and ultimately justified a refactoring. A lot of changes include: - Changes to the change log structure to include a reference to a parent struct, should there be one. This satisfies an edge case of the target being an unallocated complex struct. This comes a a complexity expense as well as a memory footprint cost. It does not add a computational expense to the operation. - An option to turn off the above edge case feature for memory constrained environments - Full recursion into complex structures. Note that this is some what opinionated but should operate intuitively - Support for complex map keys. This comes with the need for type information in the change path. In order to support this, struct keys are marshaled into their equivalent strings using msgpack. I'm considering the ability to turn this off however no option is there yet. - I've split out the code to mimic the layout of the diff_ files for consistency - Updated tests to account for the differing issues and new code - Added the ability to pass Differ options directly to the function Diff when making the call. - Corrected the lack of honoring all options when setting values - Updated the README to include patch information and examples --- .gitignore | 3 +- README.md | 113 ++++++++++++++- change_value.go | 155 ++++++++++---------- diff.go | 68 +++++---- diff_bool.go | 4 +- diff_comparative.go | 4 +- diff_examples_test.go | 320 ++++++++++++++++++++++++++++++++++++----- diff_float.go | 6 +- diff_int.go | 6 +- diff_interface.go | 8 +- diff_map.go | 12 +- diff_pointer.go | 12 +- diff_slice.go | 8 +- diff_string.go | 4 +- diff_struct.go | 4 +- diff_test.go | 18 +-- diff_uint.go | 6 +- go.mod | 6 +- go.sum | 13 ++ options.go | 13 ++ patch.go | 326 ++++++++++++++++-------------------------- patch_map.go | 61 ++++++++ patch_slice.go | 67 +++++++++ patch_struct.go | 41 ++++++ 24 files changed, 892 insertions(+), 386 deletions(-) create mode 100644 patch_map.go create mode 100644 patch_slice.go create mode 100644 patch_struct.go diff --git a/.gitignore b/.gitignore index f0608b1..e04e61e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,6 @@ # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ +.idea/ -patchflags_string.go \ No newline at end of file +patchflags_string.go diff --git a/README.md b/README.md index 266eb04..992bdc8 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,9 @@ In order for struct fields to be compared, they must be tagged with a given name |--------------|------------------------------------| | `-` | Excludes a value from being diffed | | `identifier` | If you need to compare arrays by a matching identifier and not based on order, you can specify the `identifier` tag. If an identifiable element is found in both the from and to structures, they will be directly compared. i.e. `diff:"name, identifier"` | -| `immutable` | Will omit this struct field from diffing. When using `diff.StructValues()` these values will be added to the returned changelog. It's usecase is for when we have nothing to compare a struct to and want to show all of its relevant values. | +| `immutable` | Will omit this struct field from diffing. When using `diff.StructValues()` these values will be added to the returned changelog. It's use case is for when we have nothing to compare a struct to and want to show all of its relevant values. | +| `nocreate` | The default patch action is to allocate instances in the target strut, map or slice should they not exist. Adding this flag will tell patch to skip elements that it would otherwise need to allocate. This is separate from immutable, which is also honored while patching. | +| `omitunequal` | Patching is a 'best effort' operation, and will by default attempt to update the 'correct' member of the target even if the underlying value has already changed to something other than the value in the change log 'from'. This tag will selectively ignore values that are not a 100% match. | ## Usage @@ -121,7 +123,32 @@ When marshalling the changelog to json, the output will look like: ### Options and Configuration -You can create a new instance of a differ that allows options to be set. +Options can be set on the differ at call time which effect how diff acts when building the change log. +```go +import "github.com/r3labs/diff" + +type Order struct { + ID string `diff:"id"` + Items []int `diff:"items"` +} + +func main() { + a := Order{ + ID: "1234", + Items: []int{1, 2, 3, 4}, + } + + b := Order{ + ID: "1234", + Items: []int{1, 2, 4}, + } + + changelog, err := diff.Diff(a, b, diff.DisableStructValues(), diff.AllowTypeMismatch(true)) + ... +} +``` + +You can also create a new instance of a differ that allows options to be set. ```go import "github.com/r3labs/diff" @@ -156,6 +183,88 @@ Supported options are: `SliceOrdering` ensures that the ordering of items in a slice is taken into account +`DiscardComplexOrigin` is a directive to diff to omit additional origin information about structs. This alters the behavior of patch and can lead to some pitfalls and non-intuitive behavior if used. On the other hand, it can significantly reduce the memory footprint of large complex diffs. + +`AllowTypeMismatch` is a global directive to either allow (true) or not to allow (false) patch apply the changes if 'from' is not equal. This is effectively a global version of the omitunequal tag. + +`Filter` provides a callback that allows you to determine which fields the differ descends into + +`DisableStructValues` disables populating a separate change for each item in a struct, where the struct is being compared to a nil Value. + +`TagName` sets the tag name to use when getting field names and options. + +### Patch and merge support +Diff additionally supports merge and patch. Similar in concept to text patching / merging the Patch function, given +a change log and a target instance will make a _best effort_ to apply the changes in the change log to the variable +pointed to. The intention is that the target pointer is of the same type however, that doesn't necessarily have to be +true. For example, two slices of differing structs may be similar enough to apply changes to in a polymorphic way, and +patch will certainly try. + +The patch function doesn't actually fail, and even if there are errors, it may succeed sufficiently for the task at hand. +To accommodate this patch keeps track of each change log option it attempts to apply and reports the details of what +happened for further scrutiny. + +```go +import "github.com/r3labs/diff" + +type Order struct { + ID string `diff:"id"` + Items []int `diff:"items"` +} + +func main() { + a := Order{ + ID: "1234", + Items: []int{1, 2, 3, 4}, + } + + b := Order{ + ID: "1234", + Items: []int{1, 2, 4}, + } + + c := Order{} + changelog, err := diff.Diff(a, b) + + patchlog := diff.Patch(changelog, &c) + //Note the lack of an error. Patch is best effort and uses flags to indicate actions taken + //and keeps any errors encountered along the way for review + fmt.Printf("Encountered %d errors while patching", patchlog.ErrorCount()) + ... +} +``` + +As a convenience, there is a Merge function that allows one to take three interfaces and perform all the tasks at the same +time. + +```go +import "github.com/r3labs/diff" + +type Order struct { + ID string `diff:"id"` + Items []int `diff:"items"` +} + +func main() { + a := Order{ + ID: "1234", + Items: []int{1, 2, 3, 4}, + } + + b := Order{ + ID: "1234", + Items: []int{1, 2, 4}, + } + + c := Order{} + patchlog, err := diff.Merge(a, b, &c) + if err != nil { + fmt.Printf("Error encountered while diffing a & b") + } + fmt.Printf("Encountered %d errors while patching", patchlog.ErrorCount()) + ... +} +``` ## Running Tests ``` diff --git a/change_value.go b/change_value.go index 5ee4fb3..15404eb 100644 --- a/change_value.go +++ b/change_value.go @@ -4,58 +4,26 @@ import ( "reflect" ) -//Not strictly necessary but migh be nice in some cases -//go:generate stringer -type=PatchFlags -type PatchFlags uint32 -const ( - OptionCreate PatchFlags = 1 << iota - OptionOmitUnequal - FlagInvalidTarget - FlagApplied - FlagFailed - FlagCreated - FlagIgnored - FlagDeleted - FlagUpdated -) - //ChangeValue is a specialized struct for monitoring patching type ChangeValue struct { - val reflect.Value + parent *reflect.Value + target *reflect.Value flags PatchFlags change *Change err error + pos int index int key reflect.Value } -//PatchLogEntry defines how a DiffLog entry was applied -type PatchLogEntry struct{ - Path []string `json:"path"` - From interface{} `json:"from"` - To interface{} `json:"to"` - Flags PatchFlags `json:"flags"` - Errors error `json:"errors"` -} -type PatchLog []PatchLogEntry - -//NewChangeValue idiomatic constructor -func NewChangeValue(c Change, target interface{}) *ChangeValue{ - return &ChangeValue{ - val: reflect.ValueOf(target), - change: &c, - } -} - -//NewPatchLogEntry converts our complicated reflection based struct to -//a simpler format for the consumer -func NewPatchLogEntry(change *ChangeValue) PatchLogEntry { - return PatchLogEntry{ - Path: change.change.Path, - From: change.change.From, - To: change.change.To, - Flags: change.flags, - Errors: change.err, +//swap swaps out the target as we move down the path. Note that a nil +// check is foregone here due to the fact we control usage. +func (c *ChangeValue) swap(newTarget *reflect.Value){ + if newTarget.IsValid(){ + c.ClearFlag(FlagInvalidTarget) + c.parent = c.target + c.target = newTarget + c.pos++ } } @@ -66,9 +34,11 @@ func (c *ChangeValue) SetFlag(flag PatchFlags) { } } -//ClearFlag Clears a flag on the node and saves the change -func (c *ChangeValue) ClearFlags(){ - if c != nil {c.flags = 0} +//ClearFlag removes just a single flag +func (c *ChangeValue) ClearFlag(flag PatchFlags) { + if c != nil { + c.flags = c.flags&^flag + } } //HasFlag indicates if a flag is set on the node. returns false if node is bad @@ -76,75 +46,96 @@ func (c *ChangeValue) HasFlag(flag PatchFlags) bool { return (c.flags & flag) != 0 } -//CanSet echos the reflection can set -func (c ChangeValue) CanSet() bool { - return c.val.CanSet() -} - //IsValid echo for is valid func (c *ChangeValue) IsValid() bool { - if c != nil {return c.val.IsValid() || !c.HasFlag(FlagInvalidTarget)} + if c != nil {return c.target.IsValid() || !c.HasFlag(FlagInvalidTarget)} return false } -//Len echo for len -func (c ChangeValue) Len() int { - return c.val.Len() -} - -//Kind echos the reflection kind -func (c ChangeValue) Kind() reflect.Kind { - return c.val.Kind() +//ParentKind - helps keep us nil safe +func (c ChangeValue) ParentKind() (reflect.Kind){ + if c.parent != nil { + return c.parent.Kind() + } + return reflect.Invalid } -//Type echos Type -func (c ChangeValue) Type() reflect.Type { - return c.val.Type() +//ParentLen is a nil safe parent length check +func (c ChangeValue) ParentLen() (ret int) { + if c.parent != nil && + (c.parent.Kind() == reflect.Slice || + c.parent.Kind() == reflect.Map){ + ret = c.parent.Len() + } + return } -//Set echos reflect set -func (c *ChangeValue) Set(value reflect.Value){ - if c != nil { +//ParentSet - nil safe parent set +func (c *ChangeValue) ParentSet(value reflect.Value){ + if c != nil && c.parent != nil { defer func() { if r := recover(); r != nil { - c.SetFlag(FlagFailed) + c.SetFlag(FlagParentSetFailed) } }() - c.val.Set(value) - c.SetFlag(FlagApplied) + c.parent.Set(value) + c.SetFlag(FlagParentSetApplied) } } -//SetMapValue is used to set a map value -func (c *ChangeValue) SetMapValue(key reflect.Value, value reflect.Value){ +//Len echo for len +func (c ChangeValue) Len() int { + return c.target.Len() +} + +//Set echos reflect set +func (c *ChangeValue) Set(value reflect.Value){ if c != nil { defer func() { if r := recover(); r != nil { + c.AddError(NewError(r.(string))) c.SetFlag(FlagFailed) } }() - c.val.SetMapIndex(key, value) + if c.HasFlag(OptionImmutable){ + c.SetFlag(FlagIgnored) + return + } + c.target.Set(value) + c.SetFlag(FlagApplied) } } //Index echo for index func (c ChangeValue) Index(i int) reflect.Value { - return c.val.Index(i) + return c.target.Index(i) } -//Interface gets the interface for the value -func (c ChangeValue) Interface() interface{} { - return c.val.Interface() +//ParentIndex - get us the parent version, nil safe +func (c ChangeValue) ParentIndex(i int) (ret reflect.Value) { + if c.parent != nil{ + ret = c.parent.Index(i) + } + return } -//IsNil echo for is nil -func (c ChangeValue) IsNil() bool { - return c.val.IsNil() +//Instance a new element of type for target. Taking the +//copy of the complex origin avoids the 'lack of data' issue +//present when allocating complex structs with slices and +//arrays +func (c ChangeValue) NewElement() reflect.Value { + ret := c.change.parent + if ret != nil { + return reflect.ValueOf(ret) + } + return reflect.New(c.target.Type().Elem()).Elem() } -//KeyType returns the key type of a map if it is one -func (c ChangeValue) KeyType() reflect.Type { - return c.Type().Key() +//NewArrayElement gives us a dynamically typed new element +func (c ChangeValue) NewArrayElement() reflect.Value { + c.target.Set(reflect.Append(*c.target, c.NewElement())) + c.SetFlag(FlagCreated) + return c.Index(c.Len() - 1) } //AddError appends errors to this change value diff --git a/diff.go b/diff.go index c415dc4..87fa0d3 100644 --- a/diff.go +++ b/diff.go @@ -6,7 +6,7 @@ package diff import ( "errors" - "fmt" + "github.com/vmihailenco/msgpack" "reflect" "strconv" "strings" @@ -29,6 +29,7 @@ type Differ struct { customValueDiffers []ValueDiffer cl Changelog AllowTypeMismatch bool + DiscardParent bool Filter FilterFunc } @@ -41,13 +42,14 @@ type Change struct { Path []string `json:"path"` From interface{} `json:"from"` To interface{} `json:"to"` + parent interface{} `json:"parent"` } // ValueDiffer is an interface for custom differs type ValueDiffer interface { Match(a, b reflect.Value) bool Diff(cl *Changelog, path []string, a, b reflect.Value) error - InsertParentDiffer(dfunc func(path []string, a, b reflect.Value) error) + InsertParentDiffer(dfunc func(path []string, a, b reflect.Value, p interface{}) error) } // Changed returns true if both values differ @@ -57,18 +59,19 @@ func Changed(a, b interface{}) bool { } // Diff returns a changelog of all mutated values from both -func Diff(a, b interface{}) (Changelog, error) { - d := Differ{ - TagName: "diff", +func Diff(a, b interface{}, opts ...func(d *Differ) error) (Changelog, error) { + d, err := NewDiffer(opts...) + if err != nil { + return nil, err } - - return d.cl, d.diff([]string{}, reflect.ValueOf(a), reflect.ValueOf(b)) + return d.Diff(a, b) } // NewDiffer creates a new configurable diffing object func NewDiffer(opts ...func(d *Differ) error) (*Differ, error) { d := Differ{ - TagName: "diff", + TagName: "diff", + DiscardParent: false, } for _, opt := range opts { @@ -91,7 +94,8 @@ type FilterFunc func(path []string, parent reflect.Type, field reflect.StructFie // depending on the change type specified func StructValues(t string, path []string, s interface{}) (Changelog, error) { d := Differ{ - TagName: "diff", + TagName: "diff", + DiscardParent: false, } v := reflect.ValueOf(s) @@ -117,10 +121,18 @@ func (d *Differ) Diff(a, b interface{}) (Changelog, error) { // reset the state of the diff d.cl = Changelog{} - return d.cl, d.diff([]string{}, reflect.ValueOf(a), reflect.ValueOf(b)) + return d.cl, d.diff([]string{}, reflect.ValueOf(a), reflect.ValueOf(b), nil) } -func (d *Differ) diff(path []string, a, b reflect.Value) error { +func (d *Differ) diff(path []string, a, b reflect.Value, parent interface{}) error { + + //look and see if we need to discard the parent + if parent != nil { + if d.DiscardParent || reflect.TypeOf(parent).Kind() != reflect.Struct { + parent = nil + } + } + // check if types match or are if invalid(a, b) { if d.AllowTypeMismatch { @@ -150,33 +162,37 @@ func (d *Differ) diff(path []string, a, b reflect.Value) error { case are(a, b, reflect.Slice, reflect.Invalid): return d.diffSlice(path, a, b) case are(a, b, reflect.String, reflect.Invalid): - return d.diffString(path, a, b) + return d.diffString(path, a, b, parent) case are(a, b, reflect.Bool, reflect.Invalid): - return d.diffBool(path, a, b) + return d.diffBool(path, a, b, parent) case are(a, b, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Invalid): - return d.diffInt(path, a, b) + return d.diffInt(path, a, b, parent) case are(a, b, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Invalid): - return d.diffUint(path, a, b) + return d.diffUint(path, a, b, parent) case are(a, b, reflect.Float32, reflect.Float64, reflect.Invalid): - return d.diffFloat(path, a, b) + return d.diffFloat(path, a, b, parent) case are(a, b, reflect.Map, reflect.Invalid): return d.diffMap(path, a, b) case are(a, b, reflect.Ptr, reflect.Invalid): - return d.diffPtr(path, a, b) + return d.diffPtr(path, a, b, parent) case are(a, b, reflect.Interface, reflect.Invalid): - return d.diffInterface(path, a, b) + return d.diffInterface(path, a, b, parent) default: return errors.New("unsupported type: " + a.Kind().String()) } } -func (cl *Changelog) Add(t string, path []string, from, to interface{}) { - (*cl) = append((*cl), Change{ +func (cl *Changelog) Add(t string, path []string, ftco ...interface{}) { + change := Change{ Type: t, Path: path, - From: from, - To: to, - }) + From: ftco[0], + To: ftco[1], + } + if len(ftco) > 2 { + change.parent = ftco[2] + } + (*cl) = append((*cl), change) } func tagName(tag string, f reflect.StructField) string { @@ -238,7 +254,11 @@ func idstring(v interface{}) string { case int: return strconv.Itoa(v.(int)) default: - return fmt.Sprint(v) + b, err := msgpack.Marshal(v) + if err != nil { + panic(err) + } + return string(b) } } diff --git a/diff_bool.go b/diff_bool.go index 9aad631..2ab2a70 100644 --- a/diff_bool.go +++ b/diff_bool.go @@ -6,7 +6,7 @@ package diff import "reflect" -func (d *Differ) diffBool(path []string, a, b reflect.Value) error { +func (d *Differ) diffBool(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { d.cl.Add(CREATE, path, nil, b.Interface()) return nil @@ -22,7 +22,7 @@ func (d *Differ) diffBool(path []string, a, b reflect.Value) error { } if a.Bool() != b.Bool() { - d.cl.Add(UPDATE, path, a.Interface(), b.Interface()) + d.cl.Add(UPDATE, path, a.Interface(), b.Interface(), parent) } return nil diff --git a/diff_comparative.go b/diff_comparative.go index 670f0a2..86dda18 100644 --- a/diff_comparative.go +++ b/diff_comparative.go @@ -8,7 +8,7 @@ import ( "reflect" ) -func (d *Differ) diffComparative(path []string, c *ComparativeList) error { +func (d *Differ) diffComparative(path []string, c *ComparativeList, parent interface{}) error { for _, k := range c.keys { fpath := copyAppend(path, idstring(k)) @@ -22,7 +22,7 @@ func (d *Differ) diffComparative(path []string, c *ComparativeList) error { c.m[k].B = &nv } - err := d.diff(fpath, *c.m[k].A, *c.m[k].B) + err := d.diff(fpath, *c.m[k].A, *c.m[k].B, parent) if err != nil { return err } diff --git a/diff_examples_test.go b/diff_examples_test.go index 2b61f84..51968bf 100644 --- a/diff_examples_test.go +++ b/diff_examples_test.go @@ -7,18 +7,18 @@ import ( //Try to do a bunch of stuff that will result in some or all failures //when trying to apply either a valid or invalid changelog -func ExamplePatchWithErrors(){ +func ExamplePatchWithErrors() { type Fruit struct { - ID int `diff:"ID" json:"Identifier"` - Name string `diff:"name"` - Healthy bool `diff:"-"` - Nutrients []string `diff:"nutrients"` + ID int `diff:"ID" json:"Identifier"` + Name string `diff:"name"` + Healthy bool `diff:"-"` + Nutrients []string `diff:"nutrients"` Labels map[string]int `diff:"labs"` } type Bat struct { - ID string `diff:"ID"` + ID string `diff:"ID"` Name string `diff:"-"` } @@ -63,7 +63,7 @@ func ExamplePatchWithErrors(){ c.Labels["colors"] = 42 d := Bat{ - ID: "first", + ID: "first", Name: "second", } @@ -72,7 +72,7 @@ func ExamplePatchWithErrors(){ panic(err) } - //This fails in total because c is not assignable (passed by value) + //This fails in total because c is not assignable (passed by Value) patchLog := Patch(changelog, c) //this also demonstrated the nested errors with 'next' @@ -80,8 +80,8 @@ func ExamplePatchWithErrors(){ //we can also continue to nest errors if we like message := errors.WithCause(NewError("This is a custom message")). - WithCause(fmt.Errorf("this is an error from somewhere else but still compatible")). - Error() + WithCause(fmt.Errorf("this is an error from somewhere else but still compatible")). + Error() //invoke a few failures, i.e. bad changelog changelog[2].Path[1] = "bad index" @@ -89,14 +89,14 @@ func ExamplePatchWithErrors(){ patchLog = Patch(changelog, &c) - patchLog, _ = Merge(a,nil, &c) + patchLog, _ = Merge(a, nil, &c) patchLog, _ = Merge(a, d, &c) //try patching a string patchLog = Patch(changelog, message) - //test an invalid change value + //test an invalid change Value var bad *ChangeValue if bad.IsValid() { fmt.Print("this should never happen") @@ -108,10 +108,10 @@ func ExamplePatchWithErrors(){ //ExampleMerge demonstrates how to use the Merge function func ExampleMerge() { type Fruit struct { - ID int `diff:"ID" json:"Identifier"` - Name string `diff:"name"` - Healthy bool `diff:"healthy"` - Nutrients []string `diff:"nutrients,create,omitunequal"` + ID int `diff:"ID" json:"Identifier"` + Name string `diff:"name"` + Healthy bool `diff:"healthy"` + Nutrients []string `diff:"nutrients,create,omitunequal"` Labels map[string]int `diff:"labs,create"` } @@ -156,7 +156,7 @@ func ExampleMerge() { c.Labels["colors"] = 42 //the only error that can happen here comes from the diff step - patchLog, _ := Merge(a,b, &c) + patchLog, _ := Merge(a, b, &c) //Note that unlike our patch version we've not included 'create' in the //tag for nutrients. This will omit "vitamin e" from ending up in c @@ -165,15 +165,239 @@ func ExampleMerge() { //Output: 8 } +//ExamplePrimitiveSlice demonstrates working with arrays and primitive values +func ExamplePrimitiveSlice() { + + sla := []string{ + "this", + "is", + "a", + "simple", + } + + slb := []string{ + "slice", + "That", + "can", + "be", + "diff'ed", + } + + slc := []string{ + "ok", + } + + patch, err := Diff(sla, slb) + if err != nil { + fmt.Print("failed to diff sla and slb") + } + cl := Patch(patch, &slc) + + //now the other way, round + sla = []string{ + "slice", + "That", + "can", + "be", + "diff'ed", + } + slb = []string{ + "this", + "is", + "a", + "simple", + } + + patch, err = Diff(sla, slb) + if err != nil { + fmt.Print("failed to diff sla and slb") + } + cl = Patch(patch, &slc) + + //and finally a clean view + sla = []string{ + "slice", + "That", + "can", + "be", + "diff'ed", + } + slb = []string{ + + } + + patch, err = Diff(sla, slb) + if err != nil { + fmt.Print("failed to diff sla and slb") + } + cl = Patch(patch, &slc) + + fmt.Printf("%d changes made to string array; %v", len(cl), slc) + + //Output: 5 changes made to string array; [simple a] +} + +//ExampleComplexMapPatch demonstrates how to use the Patch function for complex slices +//NOTE: There is a potential pitfall here, take a close look at b[2]. If patching the +// original, the operation will work intuitively however, in a merge situation we +// may not get everything we expect because it's a true diff between a and b and +// the diff log will not contain enough information to fully recreate b from an +// empty slice. This is exemplified in that the test "colors" is dropped in element +// 3 of c. Change "colors" to "color" and see what happens. Keep in mind this only +// happens when we need to allocate a new complex element. In normal operations we +// fix for this by keeping a copy of said element in the diff log (as parent) and +// allocate such an element as a whole copy prior to applying any updates? +// +// The new default is to carry this information forward, we invoke this pitfall +// by creating such a situation and explicitly telling diff to discard the parent +// In memory constrained environments if the developer is careful, they can use +// the discard feature but unless you REALLY understand what's happening here, use +// the default. +func ExampleComplexSlicePatch() { + + type Content struct { + Text string `diff:",create"` + Number int `diff:",create"` + } + type Attributes struct { + Labels []Content `diff:",create"` + } + + a := Attributes{ + Labels: []Content{ + { + Text: "likes", + Number: 10, + }, + { + Text: "forests", + Number: 10, + }, + { + Text: "colors", + Number: 2, + }, + }, + } + + b := Attributes{ + Labels: []Content{ + { + Text: "forests", + Number: 14, + }, + { + Text: "location", + Number: 0x32, + }, + { + Text: "colors", + Number: 1222, + }, + { + Text: "trees", + Number: 34, + }, + }, + } + c := Attributes{} + + changelog, err := Diff(a, b, DiscardComplexOrigin()) + if err != nil { + panic(err) + } + + patchLog := Patch(changelog, &c) + + fmt.Printf("Patched %d entries and encountered %d errors", len(patchLog), patchLog.ErrorCount()) + + //Output: Patched 7 entries and encountered 4 errors +} + +//ExampleComplexMapPatch demonstrates how to use the Patch function for complex slices. +func ExampleComplexMapPatch() { + + type Key struct { + Value string + weight int + } + type Content struct { + Text string + Number float64 + WholeNumber int + } + type Attributes struct { + Labels map[Key]Content + } + + a := Attributes{ + Labels: make(map[Key]Content), + } + a.Labels[Key{Value: "likes"}] = Content{ + WholeNumber: 10, + Number: 23.4, + } + + a.Labels[Key{Value: "colors"}] = Content{ + WholeNumber: 2, + } + + b := Attributes{ + Labels: make(map[Key]Content), + } + b.Labels[Key{Value: "forests"}] = Content{ + Text: "Sherwood", + } + b.Labels[Key{Value: "colors"}] = Content{ + Number: 1222, + } + b.Labels[Key{Value: "latitude"}] = Content{ + Number: 38.978797, + } + b.Labels[Key{Value: "longitude"}] = Content{ + Number: -76.490986, + } + + //c := Attributes{} + c := Attributes{ + Labels: make(map[Key]Content), + } + c.Labels[Key{Value: "likes"}] = Content{ + WholeNumber: 210, + Number: 23.4453, + } + + changelog, err := Diff(a, b) + if err != nil { + panic(err) + } + + patchLog := Patch(changelog, &c) + + fmt.Printf("%#v", len(patchLog)) + + //Output: 7 +} + //ExamplePatch demonstrates how to use the Patch function -func ExamplePatch(){ +func ExamplePatch() { + type Key struct { + value string + weight int + } + type Cycle struct { + Name string `diff:"name,create"` + Count int `diff:"count,create"` + } type Fruit struct { - ID int `diff:"ID" json:"Identifier"` - Name string `diff:"name"` - Healthy bool `diff:"healthy"` - Nutrients []string `diff:"nutrients,create,omitunequal"` - Labels map[string]int `diff:"labs,create"` + ID int `diff:"ID" json:"Identifier"` + Name string `diff:"name"` + Healthy bool `diff:"healthy"` + Nutrients []string `diff:"nutrients,create,omitunequal"` + Labels map[Key]Cycle `diff:"labs,create"` + Cycles []Cycle `diff:"cycles,immutable"` + Weights []int } a := Fruit{ @@ -186,10 +410,14 @@ func ExamplePatch(){ "vitamin c", "vitamin d", }, - Labels: make(map[string]int), + Labels: make(map[Key]Cycle), + } + a.Labels[Key{value: "likes"}] = Cycle{ + Count: 10, + } + a.Labels[Key{value: "colors"}] = Cycle{ + Count: 2, } - a.Labels["likes"] = 10 - a.Labels["colors"] = 2 b := Fruit{ ID: 2, @@ -200,20 +428,43 @@ func ExamplePatch(){ "vitamin d", "vitamin e", }, - Labels: make(map[string]int), + Labels: make(map[Key]Cycle), + Weights: []int{ + 1, + 2, + 3, + 4, + }, + } + b.Labels[Key{value: "forests"}] = Cycle{ + Count: 1223, + } + b.Labels[Key{value: "colors"}] = Cycle{ + Count: 1222, } - b.Labels["forests"] = 1223 - b.Labels["colors"] = 1222 c := Fruit{ - Labels: make(map[string]int), + //Labels: make(map[string]int), Nutrients: []string{ "vitamin a", "vitamin c", "vitamin d", }, } - c.Labels["likes"] = 21 + //c.Labels["likes"] = 21 + + d := a + d.Cycles = []Cycle{ + Cycle{ + Name: "First", + Count: 45, + }, + Cycle{ + Name: "Third", + Count: 4, + }, + } + d.Nutrients = append(d.Nutrients, "minerals") changelog, err := Diff(a, b) if err != nil { @@ -222,9 +473,12 @@ func ExamplePatch(){ patchLog := Patch(changelog, &c) + changelog, _ = Diff(a, d) + patchLog = Patch(changelog, &c) + fmt.Printf("%#v", len(patchLog)) - //Output: 8 + //Output: 1 } func ExampleDiff() { @@ -335,5 +589,5 @@ func ExampleFilter() { } fmt.Printf("%#v", changelog) - // Output: diff.Changelog{diff.Change{Type:"update", Path:[]string{"id"}, From:1, To:2}, diff.Change{Type:"create", Path:[]string{"nutrients", "2"}, From:interface {}(nil), To:"vitamin e"}} + // Output: diff.Changelog{diff.Change{Type:"update", Path:[]string{"id"}, From:1, To:2, parent:diff.Fruit{ID:1, Name:"Green Apple", Healthy:true, Nutrients:[]string{"vitamin c", "vitamin d"}, Tags:[]diff.Tag(nil)}}, diff.Change{Type:"create", Path:[]string{"nutrients", "2"}, From:interface {}(nil), To:"vitamin e", parent:interface {}(nil)}} } diff --git a/diff_float.go b/diff_float.go index 2746dc0..5fcf2e0 100644 --- a/diff_float.go +++ b/diff_float.go @@ -8,7 +8,7 @@ import ( "reflect" ) -func (d *Differ) diffFloat(path []string, a, b reflect.Value) error { +func (d *Differ) diffFloat(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { d.cl.Add(CREATE, path, nil, b.Interface()) return nil @@ -25,9 +25,9 @@ func (d *Differ) diffFloat(path []string, a, b reflect.Value) error { if a.Float() != b.Float() { if a.CanInterface() { - d.cl.Add(UPDATE, path, a.Interface(), b.Interface()) + d.cl.Add(UPDATE, path, a.Interface(), b.Interface(), parent) } else { - d.cl.Add(UPDATE, path, a.Float(), b.Float()) + d.cl.Add(UPDATE, path, a.Float(), b.Float(), parent) } } diff --git a/diff_int.go b/diff_int.go index 03158b5..77f7e64 100644 --- a/diff_int.go +++ b/diff_int.go @@ -8,7 +8,7 @@ import ( "reflect" ) -func (d *Differ) diffInt(path []string, a, b reflect.Value) error { +func (d *Differ) diffInt(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { d.cl.Add(CREATE, path, nil, b.Interface()) return nil @@ -25,9 +25,9 @@ func (d *Differ) diffInt(path []string, a, b reflect.Value) error { if a.Int() != b.Int() { if a.CanInterface() { - d.cl.Add(UPDATE, path, a.Interface(), b.Interface()) + d.cl.Add(UPDATE, path, a.Interface(), b.Interface(), parent) } else { - d.cl.Add(UPDATE, path, a.Int(), b.Int()) + d.cl.Add(UPDATE, path, a.Int(), b.Int(), parent) } } diff --git a/diff_interface.go b/diff_interface.go index d4161c6..821a275 100644 --- a/diff_interface.go +++ b/diff_interface.go @@ -6,7 +6,7 @@ package diff import "reflect" -func (d *Differ) diffInterface(path []string, a, b reflect.Value) error { +func (d *Differ) diffInterface(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { d.cl.Add(CREATE, path, nil, b.Interface()) return nil @@ -26,14 +26,14 @@ func (d *Differ) diffInterface(path []string, a, b reflect.Value) error { } if a.IsNil() { - d.cl.Add(UPDATE, path, nil, b.Interface()) + d.cl.Add(UPDATE, path, nil, b.Interface(), parent) return nil } if b.IsNil() { - d.cl.Add(UPDATE, path, a.Interface(), nil) + d.cl.Add(UPDATE, path, a.Interface(), nil, parent) return nil } - return d.diff(path, a.Elem(), b.Elem()) + return d.diff(path, a.Elem(), b.Elem(), parent) } diff --git a/diff_map.go b/diff_map.go index 815cbeb..c401c3e 100644 --- a/diff_map.go +++ b/diff_map.go @@ -5,7 +5,7 @@ package diff import ( - "fmt" + "github.com/vmihailenco/msgpack" "reflect" ) @@ -30,7 +30,7 @@ func (d *Differ) diffMap(path []string, a, b reflect.Value) error { c.addB(k.Interface(), &be) } - return d.diffComparative(path, c) + return d.diffComparative(path, c, a.Interface()) } func (d *Differ) mapValues(t string, path []string, a reflect.Value) error { @@ -52,7 +52,13 @@ func (d *Differ) mapValues(t string, path []string, a reflect.Value) error { ae := a.MapIndex(k) xe := x.MapIndex(k) - err := d.diff(append(path, fmt.Sprint(k.Interface())), xe, ae) + //it's not enough to turn k to a string, we need to able to marshal a type when + //we apply it in patch so... we'll marshal it to JSON + b, err := msgpack.Marshal(k.Interface()) + if err != nil { + return err + } + err = d.diff(append(path, string(b)), xe, ae, a.Interface()) if err != nil { return err } diff --git a/diff_pointer.go b/diff_pointer.go index 5b14b8a..dc67479 100644 --- a/diff_pointer.go +++ b/diff_pointer.go @@ -8,17 +8,17 @@ import ( "reflect" ) -func (d *Differ) diffPtr(path []string, a, b reflect.Value) error { +func (d *Differ) diffPtr(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() != b.Kind() { if a.Kind() == reflect.Invalid { if !b.IsNil() { - return d.diff(path, reflect.ValueOf(nil), reflect.Indirect(b)) + return d.diff(path, reflect.ValueOf(nil), reflect.Indirect(b), parent) } } if b.Kind() == reflect.Invalid { if !a.IsNil() { - return d.diff(path, reflect.Indirect(a), reflect.ValueOf(nil)) + return d.diff(path, reflect.Indirect(a), reflect.ValueOf(nil), parent) } } @@ -30,14 +30,14 @@ func (d *Differ) diffPtr(path []string, a, b reflect.Value) error { } if a.IsNil() { - d.cl.Add(UPDATE, path, nil, b.Interface()) + d.cl.Add(UPDATE, path, nil, b.Interface(), parent) return nil } if b.IsNil() { - d.cl.Add(UPDATE, path, a.Interface(), nil) + d.cl.Add(UPDATE, path, a.Interface(), nil, parent) return nil } - return d.diff(path, reflect.Indirect(a), reflect.Indirect(b)) + return d.diff(path, reflect.Indirect(a), reflect.Indirect(b), parent) } diff --git a/diff_slice.go b/diff_slice.go index f164541..4a36d27 100644 --- a/diff_slice.go +++ b/diff_slice.go @@ -56,7 +56,7 @@ func (d *Differ) diffSliceGeneric(path []string, a, b reflect.Value) error { return nil } - return d.diffComparative(path, missing) + return d.diffComparative(path, missing, a.Interface()) } func (d *Differ) diffSliceComparative(path []string, a, b reflect.Value) error { @@ -82,10 +82,10 @@ func (d *Differ) diffSliceComparative(path []string, a, b reflect.Value) error { } } - return d.diffComparative(path, c) + return d.diffComparative(path, c, a.Interface()) } -// keeps track of elements that have already been matched, to stop duplicate matches from occuring +// keeps track of elements that have already been matched, to stop duplicate matches from occurring type sliceTracker []bool func (st *sliceTracker) has(s, v reflect.Value) bool { @@ -121,7 +121,7 @@ func getFinalValue(t reflect.Value) reflect.Value { } func hasAtSameIndex(s, v reflect.Value, atIndex int) bool { - // check the element in the slice at atIndex to see if it matches value, if it is a valid index into the slice + // check the element in the slice at atIndex to see if it matches Value, if it is a valid index into the slice if atIndex < s.Len() { x := s.Index(atIndex) return reflect.DeepEqual(x.Interface(), v.Interface()) diff --git a/diff_string.go b/diff_string.go index fd5b281..15ebe1b 100644 --- a/diff_string.go +++ b/diff_string.go @@ -6,7 +6,7 @@ package diff import "reflect" -func (d *Differ) diffString(path []string, a, b reflect.Value) error { +func (d *Differ) diffString(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { d.cl.Add(CREATE, path, nil, b.Interface()) return nil @@ -22,7 +22,7 @@ func (d *Differ) diffString(path []string, a, b reflect.Value) error { } if a.String() != b.String() { - d.cl.Add(UPDATE, path, a.String(), b.String()) + d.cl.Add(UPDATE, path, a.String(), b.String(), parent) } return nil diff --git a/diff_struct.go b/diff_struct.go index 56d0ca4..320e064 100644 --- a/diff_struct.go +++ b/diff_struct.go @@ -51,7 +51,7 @@ func (d *Differ) diffStruct(path []string, a, b reflect.Value) error { continue } - err := d.diff(fpath, af, bf) + err := d.diff(fpath, af, bf, a.Interface()) if err != nil { return err } @@ -95,7 +95,7 @@ func (d *Differ) structValues(t string, path []string, a reflect.Value) error { fpath := copyAppend(path, tname) - err := nd.diff(fpath, xf, af) + err := nd.diff(fpath, xf, af, a.Interface()) if err != nil { return err } diff --git a/diff_test.go b/diff_test.go index 68226b2..6a6f34a 100644 --- a/diff_test.go +++ b/diff_test.go @@ -161,14 +161,14 @@ func TestDiff(t *testing.T) { { "map-nil", map[string]string{"one": "test"}, nil, Changelog{ - Change{Type: DELETE, Path: []string{"one"}, From: "test", To: nil}, + Change{Type: DELETE, Path: []string{"\xa3one"}, From: "test", To: nil}, }, nil, }, { "nil-map", nil, map[string]string{"one": "test"}, Changelog{ - Change{Type: CREATE, Path: []string{"one"}, From: nil, To: "test"}, + Change{Type: CREATE, Path: []string{"\xa3one"}, From: nil, To: "test"}, }, nil, }, @@ -339,8 +339,8 @@ func TestDiff(t *testing.T) { "mixed-slice-map", []map[string]interface{}{{"name": "name1", "type": []string{"null", "string"}}}, []map[string]interface{}{{"name": "name1", "type": []string{"null", "int"}}, {"name": "name2", "type": []string{"null", "string"}}}, Changelog{ Change{Type: UPDATE, Path: []string{"0", "type", "1"}, From: "string", To: "int"}, - Change{Type: CREATE, Path: []string{"1", "name"}, From: nil, To: "name2"}, - Change{Type: CREATE, Path: []string{"1", "type"}, From: nil, To: []string{"null", "string"}}, + Change{Type: CREATE, Path: []string{"1", "\xa4name"}, From: nil, To: "name2"}, + Change{Type: CREATE, Path: []string{"1", "\xa4type"}, From: nil, To: []string{"null", "string"}}, }, nil, }, @@ -617,10 +617,10 @@ func TestDiffPrivateField(t *testing.T) { type testType string type testTypeDiffer struct { - DiffFunc (func(path []string, a, b reflect.Value) error) + DiffFunc (func(path []string, a, b reflect.Value, p interface{}) error) } -func (o *testTypeDiffer) InsertParentDiffer(dfunc func(path []string, a, b reflect.Value) error) { +func (o *testTypeDiffer) InsertParentDiffer(dfunc func(path []string, a, b reflect.Value, p interface{}) error) { o.DiffFunc = dfunc } @@ -670,10 +670,10 @@ type RecursiveTestStruct struct { } type recursiveTestStructDiffer struct { - DiffFunc (func(path []string, a, b reflect.Value) error) + DiffFunc (func(path []string, a, b reflect.Value, p interface{}) error) } -func (o *recursiveTestStructDiffer) InsertParentDiffer(dfunc func(path []string, a, b reflect.Value) error) { +func (o *recursiveTestStructDiffer) InsertParentDiffer(dfunc func(path []string, a, b reflect.Value, p interface{}) error) { o.DiffFunc = dfunc } @@ -705,7 +705,7 @@ func (o *recursiveTestStructDiffer) Diff(cl *Changelog, path []string, a, b refl af := a.Field(i) bf := b.FieldByName(field.Name) fpath := copyAppend(path, tname) - err := o.DiffFunc(fpath, af, bf) + err := o.DiffFunc(fpath, af, bf, nil) if err != nil { return err } diff --git a/diff_uint.go b/diff_uint.go index 1e16cb7..5244b27 100644 --- a/diff_uint.go +++ b/diff_uint.go @@ -8,7 +8,7 @@ import ( "reflect" ) -func (d *Differ) diffUint(path []string, a, b reflect.Value) error { +func (d *Differ) diffUint(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { d.cl.Add(CREATE, path, nil, b.Interface()) return nil @@ -25,9 +25,9 @@ func (d *Differ) diffUint(path []string, a, b reflect.Value) error { if a.Uint() != b.Uint() { if a.CanInterface() { - d.cl.Add(UPDATE, path, a.Interface(), b.Interface()) + d.cl.Add(UPDATE, path, a.Interface(), b.Interface(), parent) } else { - d.cl.Add(UPDATE, path, a.Uint(), b.Uint()) + d.cl.Add(UPDATE, path, a.Uint(), b.Uint(), parent) } } diff --git a/go.mod b/go.mod index 7c30699..b293221 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,8 @@ module github.com/r3labs/diff go 1.13 -require github.com/stretchr/testify v1.5.1 +require ( + github.com/stretchr/testify v1.5.1 + github.com/vmihailenco/msgpack v4.0.4+incompatible + google.golang.org/appengine v1.6.6 // indirect +) diff --git a/go.sum b/go.sum index 331fa69..9426b74 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,23 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= +github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= diff --git a/options.go b/options.go index ab56160..ca9d726 100644 --- a/options.go +++ b/options.go @@ -44,6 +44,19 @@ func AllowTypeMismatch(enabled bool) func(d *Differ) error { } } +//DiscardComplexOrigin - by default, we are now keeping the complex struct associated with a create entry. +//This allows us to fix the merge to new object issue of not having enough change log details when allocating +//new objects. This however is a trade off of memory size and complexity vs correctness which is often only +//necessary when embedding structs in slices and arrays. It memory constrained environments, it may be desirable +//to turn this feature off however from a computational perspective, keeping the complex origin is actually quite +//cheap so, make sure you're extremely clear on the pitfalls of turning this off prior to doing so. +func DiscardComplexOrigin() func(d *Differ) error { + return func(d *Differ) error { + d.DiscardParent = true + return nil + } +} + // Filter allows you to determine which fields the differ descends into func Filter(f FilterFunc) func(d *Differ) error { return func(d *Differ) error { diff --git a/patch.go b/patch.go index 3472d8d..2e91300 100644 --- a/patch.go +++ b/patch.go @@ -1,9 +1,6 @@ package diff -import ( - "reflect" - "strconv" -) +import "reflect" /** This is a method of applying a changelog to a value or struct. change logs @@ -42,6 +39,55 @@ import ( you may in fact get all of the values populated you wished to anyway. */ +//Not strictly necessary but might be nice in some cases +//go:generate stringer -type=PatchFlags +type PatchFlags uint32 +const ( + OptionCreate PatchFlags = 1 << iota + OptionNoCreate + OptionOmitUnequal + OptionImmutable + FlagInvalidTarget + FlagApplied + FlagFailed + FlagCreated + FlagIgnored + FlagDeleted + FlagUpdated + FlagParentSetApplied + FlagParentSetFailed +) + +//PatchLogEntry defines how a DiffLog entry was applied +type PatchLogEntry struct{ + Path []string `json:"path"` + From interface{} `json:"from"` + To interface{} `json:"to"` + Flags PatchFlags `json:"flags"` + Errors error `json:"errors"` +} +type PatchLog []PatchLogEntry + +//HasErrors - indicates if a patch log contains any errors +func (p PatchLog) HasErrors() (ret bool) { + for _, ple := range p { + if ple.Errors != nil { + ret = true + } + } + return +} + +//ErrorCount -- counts the number of errors encountered while patching +func (p PatchLog) ErrorCount() (ret uint) { + for _, ple := range p { + if ple.Errors != nil { + ret++ + } + } + return +} + // Merge is a convenience function that diffs, the original and changed items // and merges said changes with target all in one call. func Merge(original interface{}, changed interface{}, target interface{}) (PatchLog, error) { @@ -55,219 +101,99 @@ func Merge(original interface{}, changed interface{}, target interface{}) (Patch //Patch... the missing feature. func Patch(cl Changelog, target interface{}) (ret PatchLog) { for _, c := range cl { - ret = append(ret, NewPatchLogEntry(patch(NewChangeValue(c, target)))) + ret = append(ret, NewPatchLogEntry(NewChangeValue(c, target))) } return ret } -//patch apply an individual change as opposed to a change log -func patch(c *ChangeValue) (cv *ChangeValue){ - - //Can be messy to clean up when doing reflection - defer func() { - if r := recover(); r != nil { - cv.AddError(NewError("cannot set values on target", - NewError("passed by value not reference"))) - cv.SetFlag(FlagInvalidTarget) - } - }() - cv = c - cv.val = cv.val.Elem() - - //resolve where we're actually going to set this value - cv.index = -1 - if cv.Kind() == reflect.Struct { //substitute and solve for t (path) - for _, p := range cv.change.Path { - if cv.Kind() == reflect.Slice { - //field better be an index of the slice - if cv.index, cv.err = strconv.Atoi(p); cv.err != nil { - return cv.AddError(NewErrorf("invalid index in path: %s", p). - WithCause(cv.err)) - } - break //last item in a path that's a slice is it's index - } - if cv.Kind() == reflect.Map { - keytype := cv.KeyType() - cv.key = reflect.ValueOf(p) - cv.key.Convert(keytype) - break //same as a slice - } - cv = renderTargetField(cv, p) - } +//NewPatchLogEntry converts our complicated reflection based struct to +//a simpler format for the consumer +func NewPatchLogEntry(cv *ChangeValue) PatchLogEntry { + return PatchLogEntry{ + Path: cv.change.Path, + From: cv.change.From, + To: cv.change.To, + Flags: cv.flags, + Errors: cv.err, } +} - //we have to know that the new element we're trying to set is valid - if !cv.CanSet(){ - cv.SetFlag(FlagInvalidTarget) - return cv.AddError(NewError("cannot set values on target", - NewError("passed by value not reference"))) - } - switch cv.change.Type { - case DELETE: - deleteOperation(cv) - case UPDATE, CREATE: - updateOperation(cv) +//NewChangeValue idiomatic constructor (also invokes render) +func NewChangeValue(c Change, target interface{}) (ret *ChangeValue){ + val := reflect.ValueOf(target) + ret = &ChangeValue{ + target: &val, + change: &c, } - return cv + renderChangeTarget(ret) + return } -//renderTargetField this interrogates the path and returns the correct value to -//change. Note that his is a recursion, t is not the same value as ret -func renderTargetField(t *ChangeValue, field string) (ret *ChangeValue) { - ret = t +//renderChangeValue applies 'path' in change to target. nil check is foregone +// here as we control usage +func renderChangeTarget(c *ChangeValue) { + + //This particular change element may potentially have the immutable flag + if c.HasFlag(OptionImmutable){ + c.AddError(NewError("Option immutable set, cannot apply change")) + return + } //the we always set a failure, and only unset if we successfully render the element + c.SetFlag(FlagInvalidTarget) + //substitute and solve for t (path) - switch t.val.Kind() { + switch c.target.Kind() { + + //path element that is a map + case reflect.Map: + //map elements are 'copies' and immutable so if we set the new value to the + //map prior to editing the value, it will fail to stick. To fix this, we + //defer the safe until the stack unwinds + m, k, v := c.renderMap() + defer c.deleteMapEntry(m, k, v) + + //path element that is a slice + case reflect.Slice: + c.renderSlice() + + //walking a path means dealing with real elements + case reflect.Interface, reflect.Ptr: + el := c.target.Elem() + c.target = &el + c.ClearFlag(FlagInvalidTarget) + + //path element that is a struct case reflect.Struct: - for i := 0; i < t.val.NumField(); i++ { - f := t.val.Type().Field(i) - tname := tagName("diff", f) - if tname == "-" || hasTagOption("diff", f, "immutable") { - continue - } - if tname == field || f.Name == field{ - ret = &ChangeValue{ - val: t.val.Field(i), - change: t.change, - } - ret.ClearFlags() - if hasTagOption("diff", f, "create") { - ret.SetFlag(OptionCreate) - } - if hasTagOption("diff", f, "omitunequal"){ - ret.SetFlag(OptionOmitUnequal) - } - } - } - default: - ret = &ChangeValue{ - val: t.val, - change: t.change, - } + c.patchStruct() } - if !ret.IsValid() { - ret.AddError(NewErrorf("Unable to access path value %v. Target field is invalid", field)) - } - return -} -//deleteOperation takes out some of the cyclomatic complexity from the patch fuction -func deleteOperation(cv *ChangeValue) { - switch cv.Kind() { - case reflect.Slice: - var x reflect.Value - if cv.Len() > cv.index { - x = cv.Index(cv.index) - } - found := true - if !x.IsValid() || !reflect.DeepEqual(x.Interface(), cv.change.From) { - found = false - if !cv.HasFlag(OptionOmitUnequal){ - cv.AddError(NewErrorf("value index %d is invalid", cv.index). - WithCause(NewError("scanning for value index"))) - for i := 0; i < cv.Len(); i++ { - x = cv.Index(i) - if reflect.DeepEqual(x, cv.change.From) { - cv.AddError(NewErrorf("value changed index to %d", i)) - found = true - cv.index = i - } - } - } - } - if x.IsValid() && found{ - cv.val.Index(cv.index).Set(cv.val.Index(cv.Len() - 1)) - cv.val.Set(cv.val.Slice(0, cv.Len() - 1)) - cv.SetFlag(FlagDeleted) - }else{ - cv.SetFlag(FlagIgnored) - cv.AddError(NewError("Unable to find matching slice index entry")) - } - case reflect.Map: - if !reflect.DeepEqual(cv.change.From, cv.Interface()) && - cv.HasFlag(OptionOmitUnequal){ - cv.SetFlag(FlagIgnored) - cv.AddError(NewError("target change doesn't match original")) - return - } - if cv.IsNil() { - cv.SetFlag(FlagIgnored) - cv.AddError(NewError("target has nil map nothing to delete")) - return - }else{ - cv.SetFlag(FlagDeleted) - cv.SetMapValue(cv.key, reflect.Value{}) - } - default: - cv.Set(reflect.Zero(cv.Type())) - cv.SetFlag(FlagDeleted) + //if for some reason, rendering this element fails, c will no longer be valid + //we are best effort though, so we keep on trucking + if !c.IsValid() { + c.AddError(NewErrorf("Unable to access path position %d. Target field is invalid", c.pos)) } -} -//updateOperation takes out some of the cyclomatic complexity from the patch fuction -func updateOperation(cv *ChangeValue) { - switch cv.Kind() { - case reflect.Slice: - var x reflect.Value - if cv.Len() > cv.index { - x = cv.Index(cv.index) - } - found := true - if !x.IsValid() || !reflect.DeepEqual(x.Interface(), cv.change.From) { - found = false - if !cv.HasFlag(OptionOmitUnequal){ - cv.AddError(NewErrorf("value index %d is invalid", cv.index). - WithCause(NewError("scanning for value index"))) - for i := 0; i < cv.Len(); i++ { - x = cv.Index(i) - if reflect.DeepEqual(x, cv.change.From) { - cv.AddError(NewErrorf("value changed index to %d", i)) - found = true - } - } + //we've taken care of this path element, are there any more? if so, process + //else, let's take some action + if c.pos < len(c.change.Path) && !c.HasFlag(FlagInvalidTarget){ + renderChangeTarget(c) + + }else{ //we're at the end of the line... set the Value + switch c.change.Type { + case DELETE: + switch c.ParentKind() { + case reflect.Slice: + c.deleteSliceEntry() + case reflect.Struct: + c.deleteStructEntry() + default: + c.SetFlag(FlagIgnored) } + case UPDATE, CREATE: + // this is generic because... we only deal in primitives here. AND + // the diff format To field already contains the correct type. + c.Set(reflect.ValueOf(c.change.To)) + c.SetFlag(FlagUpdated) } - if x.IsValid() && found{ - x.Set(reflect.ValueOf(cv.change.To)) - cv.SetFlag(FlagUpdated) - }else if cv.HasFlag(OptionCreate) && cv.change.Type == CREATE { - cv.Set(reflect.Append(cv.val, reflect.ValueOf(cv.change.To))) - cv.SetFlag(FlagCreated) - }else{ - cv.AddError(NewError("Unable to find matching slice index entry")) - cv.SetFlag(FlagIgnored) - } - case reflect.Map: - if !reflect.DeepEqual(cv.change.From, cv.Interface()) && - cv.HasFlag(OptionOmitUnequal){ - cv.SetFlag(FlagIgnored) - cv.AddError(NewError("target change doesn't match original")) - return - } - if cv.IsNil() { - if cv.HasFlag(OptionCreate) { - nm := reflect.MakeMap(cv.Type()) - nm.SetMapIndex(cv.key, reflect.ValueOf(cv.change.To)) - cv.Set(nm) - cv.SetFlag(FlagCreated) - }else{ - cv.SetFlag(FlagIgnored) - cv.AddError(NewError("target has nil map and create not set")) - return - } - }else{ - cv.SetFlag(FlagUpdated) - cv.SetMapValue(cv.key, reflect.ValueOf(cv.change.To)) - } - default: - if !reflect.DeepEqual(cv.change.From, cv.Interface()) && - cv.HasFlag(OptionOmitUnequal){ - cv.SetFlag(FlagIgnored) - cv.AddError(NewError("target change doesn't match original")) - return - } - cv.Set(reflect.ValueOf(cv.change.To)) - cv.SetFlag(FlagUpdated) } } - diff --git a/patch_map.go b/patch_map.go new file mode 100644 index 0000000..bbae76a --- /dev/null +++ b/patch_map.go @@ -0,0 +1,61 @@ +package diff + +import ( + "github.com/vmihailenco/msgpack" + "reflect" +) + +//renderMap - handle map rendering for patch +func (c *ChangeValue) renderMap() (m, k, v *reflect.Value){ + + //we must tease out the type of the key, we use the msgpack from diff to recreate the key + kt := c.target.Type().Key() + field := reflect.New(kt) + if err := msgpack.Unmarshal([]byte(c.change.Path[c.pos]), field.Interface()); err != nil { + c.SetFlag(FlagIgnored) + c.AddError(NewError("Unable to unmarshal path element to target type for key in map", err)) + return + } + c.key = field.Elem() + if c.target.IsNil() && c.target.IsValid() { + c.target.Set(reflect.MakeMap(c.target.Type())) + } + x := c.target.MapIndex(c.key) + + if !x.IsValid() && c.change.Type != DELETE && !c.HasFlag(OptionNoCreate) { + x = c.NewElement() + } + if x.IsValid(){ //Map elements come out as read only so we must convert + nv := reflect.New(x.Type()).Elem() + nv.Set(x) + x = nv + } + + if x.IsValid() && !reflect.DeepEqual(c.change.From, x.Interface()) && + c.HasFlag(OptionOmitUnequal) { + c.SetFlag(FlagIgnored) + c.AddError(NewError("target change doesn't match original")) + return + } + mp := *c.target //these may change out from underneath us as we recurse + key := c.key //so we make copies and pass back pointers to them + c.swap(&x) + return &mp, &key, &x + +} + +//deleteMapEntry - deletes are special, they are handled differently based on options +// container type etc. We have to have special handling for each +// type. Set values are more generic even if they must be instanced +func (c *ChangeValue) deleteMapEntry(m, k, v *reflect.Value) { + if m!= nil && m.CanSet() && v.IsValid() { + for x := 0; x < v.NumField(); x++ { + if !v.Field(x).IsZero() { + m.SetMapIndex(*k, *v) + return + } + } //if all the fields are zero, remove from map + m.SetMapIndex(*k, reflect.Value{}) + c.SetFlag(FlagDeleted) + } +} diff --git a/patch_slice.go b/patch_slice.go new file mode 100644 index 0000000..b94c1f3 --- /dev/null +++ b/patch_slice.go @@ -0,0 +1,67 @@ +package diff + +/** + Types are being split out to more closely follow the library structure already + in place. Keeps the file simpler as well. + */ +import ( + "reflect" + "strconv" +) + +//renderSlice - handle slice rendering for patch +func (c *ChangeValue) renderSlice(){ + + var err error + field := c.change.Path[c.pos] + + //field better be an index of the slice + if c.index, err = strconv.Atoi(field); err != nil { + c.AddError(NewErrorf("invalid index in path. %s is not a number", field). + WithCause(err)) + } + var x reflect.Value + if c.Len() > c.index { + x = c.Index(c.index) + } + if !x.IsValid() { + if !c.HasFlag(OptionOmitUnequal){ + c.AddError(NewErrorf("Value index %d is invalid", c.index). + WithCause(NewError("scanning for Value index"))) + for c.index = 0; c.index < c.Len(); c.index++ { + y := c.Index(c.index) + if reflect.DeepEqual(y, c.change.From) { + c.AddError(NewErrorf("Value changed index to %d", c.index)) + x = y + break + } + } + } + } + if !x.IsValid() && c.change.Type != DELETE && !c.HasFlag(OptionNoCreate){ + x = c.NewArrayElement() + } + if !x.IsValid() && c.change.Type == DELETE { + c.index = -1 //no existing element to delete so don't bother + } + c.swap(&x) //containers must swap out the parent Value +} + +//deleteSliceEntry - deletes are special, they are handled differently based on options +// container type etc. We have to have special handling for each +// type. Set values are more generic even if they must be instanced +func (c *ChangeValue) deleteSliceEntry() { + //for a slice with only one element + if c.ParentLen() == 1 && c.index != -1{ + c.ParentSet(reflect.MakeSlice(c.parent.Type(),0,0)) + c.SetFlag(FlagDeleted) + //for a slice with multiple elements + }else if c.index != -1 { //this is an array delete the element from the parent + c.ParentIndex(c.index).Set(c.ParentIndex(c.ParentLen() - 1)) + c.ParentSet(c.parent.Slice(0, c.ParentLen() - 1)) + c.SetFlag(FlagDeleted) + //for other slice elements, we ignore + }else{ + c.SetFlag(FlagIgnored) + } +} diff --git a/patch_struct.go b/patch_struct.go new file mode 100644 index 0000000..96c01e7 --- /dev/null +++ b/patch_struct.go @@ -0,0 +1,41 @@ +package diff + +import "reflect" + +/** + Types are being split out to more closely follow the library structure already + in place. Keeps the file simpler as well. +*/ + +//patchStruct - handles the rendering of a struct field +func (c *ChangeValue) patchStruct(){ + + field := c.change.Path[c.pos] + + for i := 0; i < c.target.NumField(); i++ { + f := c.target.Type().Field(i) + tname := tagName("diff", f) + if tname == "-" || hasTagOption("diff", f, "immutable") { + c.SetFlag(OptionImmutable) + continue + } + if tname == field || f.Name == field{ + x := c.target.Field(i) + if hasTagOption("diff", f, "nocreate") { + c.SetFlag(OptionNoCreate) + } + if hasTagOption("diff", f, "omitunequal"){ + c.SetFlag(OptionOmitUnequal) + } + c.swap(&x) + break + } + } +} + +//track and zero out struct members +func (c *ChangeValue) deleteStructEntry(){ + + //deleting a struct value set's it to the 'basic' type + c.Set(reflect.Zero(c.target.Type())) +} From 98c4992c432f50feaf160440097ff0d3dab12f79 Mon Sep 17 00:00:00 2001 From: Kyenos Date: Wed, 14 Oct 2020 16:22:41 -0400 Subject: [PATCH 23/56] Gofmt'd the whole project for consistency Added convenience functions to PatchLog: - Applied: Returns true if all entries in the patch log were applied regardless of errors - HasFlag: Removes assumption that the user knows how to use the flags --- change_value.go | 32 ++++++++++++++------------ diff.go | 12 +++++----- diff_examples_test.go | 12 +++++----- diff_test.go | 13 +++++------ error.go | 2 +- options.go | 2 +- patch.go | 52 +++++++++++++++++++++++++++++-------------- patch_map.go | 6 ++--- patch_slice.go | 20 ++++++++--------- patch_struct.go | 8 +++---- 10 files changed, 89 insertions(+), 70 deletions(-) diff --git a/change_value.go b/change_value.go index 15404eb..bb5867d 100644 --- a/change_value.go +++ b/change_value.go @@ -18,8 +18,8 @@ type ChangeValue struct { //swap swaps out the target as we move down the path. Note that a nil // check is foregone here due to the fact we control usage. -func (c *ChangeValue) swap(newTarget *reflect.Value){ - if newTarget.IsValid(){ +func (c *ChangeValue) swap(newTarget *reflect.Value) { + if newTarget.IsValid() { c.ClearFlag(FlagInvalidTarget) c.parent = c.target c.target = newTarget @@ -30,14 +30,14 @@ func (c *ChangeValue) swap(newTarget *reflect.Value){ // Sets a flag on the node and saves the change func (c *ChangeValue) SetFlag(flag PatchFlags) { if c != nil { - c.flags = c.flags|flag + c.flags = c.flags | flag } } //ClearFlag removes just a single flag func (c *ChangeValue) ClearFlag(flag PatchFlags) { if c != nil { - c.flags = c.flags&^flag + c.flags = c.flags &^ flag } } @@ -48,12 +48,14 @@ func (c *ChangeValue) HasFlag(flag PatchFlags) bool { //IsValid echo for is valid func (c *ChangeValue) IsValid() bool { - if c != nil {return c.target.IsValid() || !c.HasFlag(FlagInvalidTarget)} + if c != nil { + return c.target.IsValid() || !c.HasFlag(FlagInvalidTarget) + } return false } //ParentKind - helps keep us nil safe -func (c ChangeValue) ParentKind() (reflect.Kind){ +func (c ChangeValue) ParentKind() reflect.Kind { if c.parent != nil { return c.parent.Kind() } @@ -64,14 +66,14 @@ func (c ChangeValue) ParentKind() (reflect.Kind){ func (c ChangeValue) ParentLen() (ret int) { if c.parent != nil && (c.parent.Kind() == reflect.Slice || - c.parent.Kind() == reflect.Map){ + c.parent.Kind() == reflect.Map) { ret = c.parent.Len() } return } //ParentSet - nil safe parent set -func (c *ChangeValue) ParentSet(value reflect.Value){ +func (c *ChangeValue) ParentSet(value reflect.Value) { if c != nil && c.parent != nil { defer func() { if r := recover(); r != nil { @@ -89,7 +91,7 @@ func (c ChangeValue) Len() int { } //Set echos reflect set -func (c *ChangeValue) Set(value reflect.Value){ +func (c *ChangeValue) Set(value reflect.Value) { if c != nil { defer func() { if r := recover(); r != nil { @@ -97,7 +99,7 @@ func (c *ChangeValue) Set(value reflect.Value){ c.SetFlag(FlagFailed) } }() - if c.HasFlag(OptionImmutable){ + if c.HasFlag(OptionImmutable) { c.SetFlag(FlagIgnored) return } @@ -113,7 +115,7 @@ func (c ChangeValue) Index(i int) reflect.Value { //ParentIndex - get us the parent version, nil safe func (c ChangeValue) ParentIndex(i int) (ret reflect.Value) { - if c.parent != nil{ + if c.parent != nil { ret = c.parent.Index(i) } return @@ -139,7 +141,9 @@ func (c ChangeValue) NewArrayElement() reflect.Value { } //AddError appends errors to this change value -func (c *ChangeValue) AddError(err error) *ChangeValue{ - if c != nil {c.err = err} +func (c *ChangeValue) AddError(err error) *ChangeValue { + if c != nil { + c.err = err + } return c -} \ No newline at end of file +} diff --git a/diff.go b/diff.go index 87fa0d3..f62de79 100644 --- a/diff.go +++ b/diff.go @@ -38,10 +38,10 @@ type Changelog []Change // Change stores information about a changed item type Change struct { - Type string `json:"type"` - Path []string `json:"path"` - From interface{} `json:"from"` - To interface{} `json:"to"` + Type string `json:"type"` + Path []string `json:"path"` + From interface{} `json:"from"` + To interface{} `json:"to"` parent interface{} `json:"parent"` } @@ -59,7 +59,7 @@ func Changed(a, b interface{}) bool { } // Diff returns a changelog of all mutated values from both -func Diff(a, b interface{}, opts ...func(d *Differ) error) (Changelog, error) { +func Diff(a, b interface{}, opts ...func(d *Differ) error) (Changelog, error) { d, err := NewDiffer(opts...) if err != nil { return nil, err @@ -183,7 +183,7 @@ func (d *Differ) diff(path []string, a, b reflect.Value, parent interface{}) err } func (cl *Changelog) Add(t string, path []string, ftco ...interface{}) { - change := Change{ + change := Change{ Type: t, Path: path, From: ftco[0], diff --git a/diff_examples_test.go b/diff_examples_test.go index 51968bf..3b7392e 100644 --- a/diff_examples_test.go +++ b/diff_examples_test.go @@ -222,9 +222,7 @@ func ExamplePrimitiveSlice() { "be", "diff'ed", } - slb = []string{ - - } + slb = []string{} patch, err = Diff(sla, slb) if err != nil { @@ -322,8 +320,8 @@ func ExampleComplexMapPatch() { weight int } type Content struct { - Text string - Number float64 + Text string + Number float64 WholeNumber int } type Attributes struct { @@ -335,7 +333,7 @@ func ExampleComplexMapPatch() { } a.Labels[Key{Value: "likes"}] = Content{ WholeNumber: 10, - Number: 23.4, + Number: 23.4, } a.Labels[Key{Value: "colors"}] = Content{ @@ -364,7 +362,7 @@ func ExampleComplexMapPatch() { } c.Labels[Key{Value: "likes"}] = Content{ WholeNumber: 210, - Number: 23.4453, + Number: 23.4453, } changelog, err := Diff(a, b) diff --git a/diff_test.go b/diff_test.go index 6a6f34a..63b54c8 100644 --- a/diff_test.go +++ b/diff_test.go @@ -739,13 +739,12 @@ func TestRecursiveCustomDiffer(t *testing.T) { assert.Len(t, cl, 1) } - func TestHandleDifferentTypes(t *testing.T) { cases := []struct { - Name string - A, B interface{} - Changelog Changelog - Error error + Name string + A, B interface{} + Changelog Changelog + Error error HandleTypeMismatch bool }{ { @@ -761,7 +760,7 @@ func TestHandleDifferentTypes(t *testing.T) { p1 string p2 int }{"1", 1}, - struct{ + struct { p1 string p2 string }{"1", "1"}, @@ -785,7 +784,7 @@ func TestHandleDifferentTypes(t *testing.T) { P2 int P3 map[string]string }{"1", 1, map[string]string{"1": "1"}}, - struct{ + struct { P1 string P2 string P3 string diff --git a/error.go b/error.go index a8986ae..98403c9 100644 --- a/error.go +++ b/error.go @@ -19,7 +19,7 @@ type DiffError struct { } //Unwrap implement 1.13 unwrap feature for compatibility -func(s *DiffError) Unwrap() error { +func (s *DiffError) Unwrap() error { return s.next } diff --git a/options.go b/options.go index ca9d726..dea3295 100644 --- a/options.go +++ b/options.go @@ -50,7 +50,7 @@ func AllowTypeMismatch(enabled bool) func(d *Differ) error { //necessary when embedding structs in slices and arrays. It memory constrained environments, it may be desirable //to turn this feature off however from a computational perspective, keeping the complex origin is actually quite //cheap so, make sure you're extremely clear on the pitfalls of turning this off prior to doing so. -func DiscardComplexOrigin() func(d *Differ) error { +func DiscardComplexOrigin() func(d *Differ) error { return func(d *Differ) error { d.DiscardParent = true return nil diff --git a/patch.go b/patch.go index 2e91300..ad1de21 100644 --- a/patch.go +++ b/patch.go @@ -37,11 +37,12 @@ import "reflect" diff two structs of the same type, then attempt to apply to an entirely different struct that is similar in constitution (think interface here) and you may in fact get all of the values populated you wished to anyway. - */ +*/ //Not strictly necessary but might be nice in some cases //go:generate stringer -type=PatchFlags type PatchFlags uint32 + const ( OptionCreate PatchFlags = 1 << iota OptionNoCreate @@ -59,15 +60,33 @@ const ( ) //PatchLogEntry defines how a DiffLog entry was applied -type PatchLogEntry struct{ - Path []string `json:"path"` - From interface{} `json:"from"` - To interface{} `json:"to"` - Flags PatchFlags `json:"flags"` - Errors error `json:"errors"` +type PatchLogEntry struct { + Path []string `json:"path"` + From interface{} `json:"from"` + To interface{} `json:"to"` + Flags PatchFlags `json:"flags"` + Errors error `json:"errors"` } type PatchLog []PatchLogEntry +//HasFlag - convenience function for users +func (p PatchLogEntry) HasFlag(flag PatchFlags) bool { + return (p.Flags & flag) != 0 +} + +//Applied - returns true if all change log entries were actually +// applied, regardless of if any errors were encountered +func (p PatchLog) Applied() bool { + if p.HasErrors() { + for _, ple := range p { + if !ple.HasFlag(FlagApplied) { + return false + } + } + } + return true +} + //HasErrors - indicates if a patch log contains any errors func (p PatchLog) HasErrors() (ret bool) { for _, ple := range p { @@ -93,7 +112,7 @@ func (p PatchLog) ErrorCount() (ret uint) { func Merge(original interface{}, changed interface{}, target interface{}) (PatchLog, error) { if cl, err := Diff(original, changed); err == nil { return Patch(cl, target), nil - }else{ + } else { return nil, err } } @@ -110,17 +129,16 @@ func Patch(cl Changelog, target interface{}) (ret PatchLog) { //a simpler format for the consumer func NewPatchLogEntry(cv *ChangeValue) PatchLogEntry { return PatchLogEntry{ - Path: cv.change.Path, - From: cv.change.From, - To: cv.change.To, - Flags: cv.flags, + Path: cv.change.Path, + From: cv.change.From, + To: cv.change.To, + Flags: cv.flags, Errors: cv.err, } } - //NewChangeValue idiomatic constructor (also invokes render) -func NewChangeValue(c Change, target interface{}) (ret *ChangeValue){ +func NewChangeValue(c Change, target interface{}) (ret *ChangeValue) { val := reflect.ValueOf(target) ret = &ChangeValue{ target: &val, @@ -135,7 +153,7 @@ func NewChangeValue(c Change, target interface{}) (ret *ChangeValue){ func renderChangeTarget(c *ChangeValue) { //This particular change element may potentially have the immutable flag - if c.HasFlag(OptionImmutable){ + if c.HasFlag(OptionImmutable) { c.AddError(NewError("Option immutable set, cannot apply change")) return } //the we always set a failure, and only unset if we successfully render the element @@ -175,10 +193,10 @@ func renderChangeTarget(c *ChangeValue) { //we've taken care of this path element, are there any more? if so, process //else, let's take some action - if c.pos < len(c.change.Path) && !c.HasFlag(FlagInvalidTarget){ + if c.pos < len(c.change.Path) && !c.HasFlag(FlagInvalidTarget) { renderChangeTarget(c) - }else{ //we're at the end of the line... set the Value + } else { //we're at the end of the line... set the Value switch c.change.Type { case DELETE: switch c.ParentKind() { diff --git a/patch_map.go b/patch_map.go index bbae76a..add4e76 100644 --- a/patch_map.go +++ b/patch_map.go @@ -6,7 +6,7 @@ import ( ) //renderMap - handle map rendering for patch -func (c *ChangeValue) renderMap() (m, k, v *reflect.Value){ +func (c *ChangeValue) renderMap() (m, k, v *reflect.Value) { //we must tease out the type of the key, we use the msgpack from diff to recreate the key kt := c.target.Type().Key() @@ -25,7 +25,7 @@ func (c *ChangeValue) renderMap() (m, k, v *reflect.Value){ if !x.IsValid() && c.change.Type != DELETE && !c.HasFlag(OptionNoCreate) { x = c.NewElement() } - if x.IsValid(){ //Map elements come out as read only so we must convert + if x.IsValid() { //Map elements come out as read only so we must convert nv := reflect.New(x.Type()).Elem() nv.Set(x) x = nv @@ -48,7 +48,7 @@ func (c *ChangeValue) renderMap() (m, k, v *reflect.Value){ // container type etc. We have to have special handling for each // type. Set values are more generic even if they must be instanced func (c *ChangeValue) deleteMapEntry(m, k, v *reflect.Value) { - if m!= nil && m.CanSet() && v.IsValid() { + if m != nil && m.CanSet() && v.IsValid() { for x := 0; x < v.NumField(); x++ { if !v.Field(x).IsZero() { m.SetMapIndex(*k, *v) diff --git a/patch_slice.go b/patch_slice.go index b94c1f3..707ea70 100644 --- a/patch_slice.go +++ b/patch_slice.go @@ -3,14 +3,14 @@ package diff /** Types are being split out to more closely follow the library structure already in place. Keeps the file simpler as well. - */ +*/ import ( "reflect" "strconv" ) //renderSlice - handle slice rendering for patch -func (c *ChangeValue) renderSlice(){ +func (c *ChangeValue) renderSlice() { var err error field := c.change.Path[c.pos] @@ -25,7 +25,7 @@ func (c *ChangeValue) renderSlice(){ x = c.Index(c.index) } if !x.IsValid() { - if !c.HasFlag(OptionOmitUnequal){ + if !c.HasFlag(OptionOmitUnequal) { c.AddError(NewErrorf("Value index %d is invalid", c.index). WithCause(NewError("scanning for Value index"))) for c.index = 0; c.index < c.Len(); c.index++ { @@ -38,7 +38,7 @@ func (c *ChangeValue) renderSlice(){ } } } - if !x.IsValid() && c.change.Type != DELETE && !c.HasFlag(OptionNoCreate){ + if !x.IsValid() && c.change.Type != DELETE && !c.HasFlag(OptionNoCreate) { x = c.NewArrayElement() } if !x.IsValid() && c.change.Type == DELETE { @@ -50,18 +50,18 @@ func (c *ChangeValue) renderSlice(){ //deleteSliceEntry - deletes are special, they are handled differently based on options // container type etc. We have to have special handling for each // type. Set values are more generic even if they must be instanced -func (c *ChangeValue) deleteSliceEntry() { +func (c *ChangeValue) deleteSliceEntry() { //for a slice with only one element - if c.ParentLen() == 1 && c.index != -1{ - c.ParentSet(reflect.MakeSlice(c.parent.Type(),0,0)) + if c.ParentLen() == 1 && c.index != -1 { + c.ParentSet(reflect.MakeSlice(c.parent.Type(), 0, 0)) c.SetFlag(FlagDeleted) //for a slice with multiple elements - }else if c.index != -1 { //this is an array delete the element from the parent + } else if c.index != -1 { //this is an array delete the element from the parent c.ParentIndex(c.index).Set(c.ParentIndex(c.ParentLen() - 1)) - c.ParentSet(c.parent.Slice(0, c.ParentLen() - 1)) + c.ParentSet(c.parent.Slice(0, c.ParentLen()-1)) c.SetFlag(FlagDeleted) //for other slice elements, we ignore - }else{ + } else { c.SetFlag(FlagIgnored) } } diff --git a/patch_struct.go b/patch_struct.go index 96c01e7..d240076 100644 --- a/patch_struct.go +++ b/patch_struct.go @@ -8,7 +8,7 @@ import "reflect" */ //patchStruct - handles the rendering of a struct field -func (c *ChangeValue) patchStruct(){ +func (c *ChangeValue) patchStruct() { field := c.change.Path[c.pos] @@ -19,12 +19,12 @@ func (c *ChangeValue) patchStruct(){ c.SetFlag(OptionImmutable) continue } - if tname == field || f.Name == field{ + if tname == field || f.Name == field { x := c.target.Field(i) if hasTagOption("diff", f, "nocreate") { c.SetFlag(OptionNoCreate) } - if hasTagOption("diff", f, "omitunequal"){ + if hasTagOption("diff", f, "omitunequal") { c.SetFlag(OptionOmitUnequal) } c.swap(&x) @@ -34,7 +34,7 @@ func (c *ChangeValue) patchStruct(){ } //track and zero out struct members -func (c *ChangeValue) deleteStructEntry(){ +func (c *ChangeValue) deleteStructEntry() { //deleting a struct value set's it to the 'basic' type c.Set(reflect.Zero(c.target.Type())) From 87893d1927abf4584885133ef1b29fca580b79f4 Mon Sep 17 00:00:00 2001 From: Kyenos Date: Fri, 16 Oct 2020 23:31:54 -0400 Subject: [PATCH 24/56] As per peer review request, I've wrapped the change of complex id's in an option, StructMapKeySupport. This keeps backwards compatibility with the underlying changelog output however, it puts the onus on the developer to ensure that the result of a diff is compatible with Patch & Merge... Which is no longer the default... But only if you're using complex data types as map keys (i.e. structs which need to be marshaled to and from the path variables) The additional complexity of code dropped the unit tests below 80% and as such, I've put in some additional testing, mainly around uint which had 0% coverage. --- diff.go | 15 ++++++++++++++- diff_comparative.go | 6 +++++- diff_examples_test.go | 4 ++-- diff_map.go | 16 ++++++++++------ diff_test.go | 30 +++++++++++++++++++++++++++++- options.go | 11 +++++++++++ patch.go | 2 +- 7 files changed, 72 insertions(+), 12 deletions(-) diff --git a/diff.go b/diff.go index f62de79..851b7b6 100644 --- a/diff.go +++ b/diff.go @@ -6,6 +6,7 @@ package diff import ( "errors" + "fmt" "github.com/vmihailenco/msgpack" "reflect" "strconv" @@ -30,6 +31,7 @@ type Differ struct { cl Changelog AllowTypeMismatch bool DiscardParent bool + StructMapKeys bool Filter FilterFunc } @@ -247,7 +249,7 @@ func swapChange(t string, c Change) Change { return nc } -func idstring(v interface{}) string { +func idComplex(v interface{}) string { switch v.(type) { case string: return v.(string) @@ -260,6 +262,17 @@ func idstring(v interface{}) string { } return string(b) } + +} +func idstring(v interface{}) string { + switch v.(type) { + case string: + return v.(string) + case int: + return strconv.Itoa(v.(int)) + default: + return fmt.Sprint(v) + } } func invalid(a, b reflect.Value) bool { diff --git a/diff_comparative.go b/diff_comparative.go index 86dda18..878e28f 100644 --- a/diff_comparative.go +++ b/diff_comparative.go @@ -10,7 +10,11 @@ import ( func (d *Differ) diffComparative(path []string, c *ComparativeList, parent interface{}) error { for _, k := range c.keys { - fpath := copyAppend(path, idstring(k)) + id := idstring(k) + if d.StructMapKeys { + id = idComplex(k) + } + fpath := copyAppend(path, id) nv := reflect.ValueOf(nil) diff --git a/diff_examples_test.go b/diff_examples_test.go index 3b7392e..6e4531e 100644 --- a/diff_examples_test.go +++ b/diff_examples_test.go @@ -187,7 +187,7 @@ func ExamplePrimitiveSlice() { "ok", } - patch, err := Diff(sla, slb) + patch, err := Diff(sla, slb, StructMapKeySupport()) if err != nil { fmt.Print("failed to diff sla and slb") } @@ -300,7 +300,7 @@ func ExampleComplexSlicePatch() { } c := Attributes{} - changelog, err := Diff(a, b, DiscardComplexOrigin()) + changelog, err := Diff(a, b, DiscardComplexOrigin(), StructMapKeySupport()) if err != nil { panic(err) } diff --git a/diff_map.go b/diff_map.go index c401c3e..32fbce1 100644 --- a/diff_map.go +++ b/diff_map.go @@ -5,6 +5,7 @@ package diff import ( + "fmt" "github.com/vmihailenco/msgpack" "reflect" ) @@ -52,13 +53,16 @@ func (d *Differ) mapValues(t string, path []string, a reflect.Value) error { ae := a.MapIndex(k) xe := x.MapIndex(k) - //it's not enough to turn k to a string, we need to able to marshal a type when - //we apply it in patch so... we'll marshal it to JSON - b, err := msgpack.Marshal(k.Interface()) - if err != nil { - return err + var err error + if d.StructMapKeys { + //it's not enough to turn k to a string, we need to able to marshal a type when + //we apply it in patch so... we'll marshal it to JSON + if b, err := msgpack.Marshal(k.Interface()); err == nil { + err = d.diff(append(path, string(b)), xe, ae, a.Interface()) + } + } else { + err = d.diff(append(path, fmt.Sprint(k.Interface())), xe, ae, a.Interface()) } - err = d.diff(append(path, string(b)), xe, ae, a.Interface()) if err != nil { return err } diff --git a/diff_test.go b/diff_test.go index 63b54c8..711f425 100644 --- a/diff_test.go +++ b/diff_test.go @@ -63,6 +63,13 @@ func TestDiff(t *testing.T) { Changelog Changelog Error error }{ + { + "uint-slice-insert", []uint{1, 2, 3}, []uint{1, 2, 3, 4}, + Changelog{ + Change{Type: CREATE, Path: []string{"3"}, To: uint(4)}, + }, + nil, + }, { "int-slice-insert", []int{1, 2, 3}, []int{1, 2, 3, 4}, Changelog{ @@ -70,6 +77,13 @@ func TestDiff(t *testing.T) { }, nil, }, + { + "uint-slice-delete", []uint{1, 2, 3}, []uint{1, 3}, + Changelog{ + Change{Type: DELETE, Path: []string{"1"}, From: uint(2)}, + }, + nil, + }, { "int-slice-delete", []int{1, 2, 3}, []int{1, 3}, Changelog{ @@ -77,6 +91,14 @@ func TestDiff(t *testing.T) { }, nil, }, + { + "uint-slice-insert-delete", []uint{1, 2, 3}, []uint{1, 3, 4}, + Changelog{ + Change{Type: DELETE, Path: []string{"1"}, From: uint(2)}, + Change{Type: CREATE, Path: []string{"2"}, To: uint(4)}, + }, + nil, + }, { "int-slice-insert-delete", []int{1, 2, 3}, []int{1, 3, 4}, Changelog{ @@ -377,7 +399,13 @@ func TestDiff(t *testing.T) { for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { - cl, err := Diff(tc.A, tc.B) + + var options []func(d *Differ) error + switch tc.Name { + case "mixed-slice-map", "nil-map", "map-nil": + options = append(options, StructMapKeySupport()) + } + cl, err := Diff(tc.A, tc.B, options...) assert.Equal(t, tc.Error, err) require.Equal(t, len(tc.Changelog), len(cl)) diff --git a/options.go b/options.go index dea3295..8f65b63 100644 --- a/options.go +++ b/options.go @@ -44,6 +44,17 @@ func AllowTypeMismatch(enabled bool) func(d *Differ) error { } } +//StructMapKeySupport - Changelog paths do not provided structured object values for maps that contain complex +//keys (such as other structs). You must enable this support via an option and it then uses msgpack to encode +//path elements that are structs. If you don't have this on, and try to patch, your apply will fail for that +//element. +func StructMapKeySupport() func(d *Differ) error { + return func(d *Differ) error { + d.StructMapKeys = true + return nil + } +} + //DiscardComplexOrigin - by default, we are now keeping the complex struct associated with a create entry. //This allows us to fix the merge to new object issue of not having enough change log details when allocating //new objects. This however is a trade off of memory size and complexity vs correctness which is often only diff --git a/patch.go b/patch.go index ad1de21..49314ad 100644 --- a/patch.go +++ b/patch.go @@ -110,7 +110,7 @@ func (p PatchLog) ErrorCount() (ret uint) { // Merge is a convenience function that diffs, the original and changed items // and merges said changes with target all in one call. func Merge(original interface{}, changed interface{}, target interface{}) (PatchLog, error) { - if cl, err := Diff(original, changed); err == nil { + if cl, err := Diff(original, changed, StructMapKeySupport()); err == nil { return Patch(cl, target), nil } else { return nil, err From 110495df963816dcb159370bfc379a028769adc4 Mon Sep 17 00:00:00 2001 From: ybzhanghx <751761610@qq.com> Date: Mon, 19 Oct 2020 20:24:28 +0800 Subject: [PATCH 25/56] : Add support for private members as Pointers --- diff_examples_test.go | 18 ++++++++++++++++++ diff_pointer.go | 15 +++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/diff_examples_test.go b/diff_examples_test.go index 6e4531e..fa25194 100644 --- a/diff_examples_test.go +++ b/diff_examples_test.go @@ -2,6 +2,7 @@ package diff import ( "fmt" + "math/big" "reflect" ) @@ -589,3 +590,20 @@ func ExampleFilter() { fmt.Printf("%#v", changelog) // Output: diff.Changelog{diff.Change{Type:"update", Path:[]string{"id"}, From:1, To:2, parent:diff.Fruit{ID:1, Name:"Green Apple", Healthy:true, Nutrients:[]string{"vitamin c", "vitamin d"}, Tags:[]diff.Tag(nil)}}, diff.Change{Type:"create", Path:[]string{"nutrients", "2"}, From:interface {}(nil), To:"vitamin e", parent:interface {}(nil)}} } + +func ExamplePrivatePtr() { + type number struct { + value *big.Int + exp int32 + } + a := number{} + b := number{value: big.NewInt(111)} + + changelog, err := Diff(a, b) + if err != nil { + panic(err) + } + + fmt.Printf("%#v", changelog) + // Output: diff.Changelog{diff.Change{Type:"update", Path:[]string{"value"}, From:interface {}(nil), To:111, parent:diff.number{value:(*big.Int)(nil), exp:0}}} +} diff --git a/diff_pointer.go b/diff_pointer.go index dc67479..605abf8 100644 --- a/diff_pointer.go +++ b/diff_pointer.go @@ -6,8 +6,11 @@ package diff import ( "reflect" + "unsafe" ) +var isExportFlag uintptr = (1 << 5) | (1 << 6) + func (d *Differ) diffPtr(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() != b.Kind() { if a.Kind() == reflect.Invalid { @@ -30,14 +33,22 @@ func (d *Differ) diffPtr(path []string, a, b reflect.Value, parent interface{}) } if a.IsNil() { - d.cl.Add(UPDATE, path, nil, b.Interface(), parent) + d.cl.Add(UPDATE, path, nil, exportInterface(b), parent) return nil } if b.IsNil() { - d.cl.Add(UPDATE, path, a.Interface(), nil, parent) + d.cl.Add(UPDATE, path, exportInterface(a), nil, parent) return nil } return d.diff(path, reflect.Indirect(a), reflect.Indirect(b), parent) } + +func exportInterface(v reflect.Value) interface{} { + if !v.CanInterface() { + flagTmp := (*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + 2*unsafe.Sizeof(uintptr(0)))) + *flagTmp = (*flagTmp) & (^isExportFlag) + } + return v.Interface() +} From 8d3cdfb727df41ede446288786b3382b8007cef0 Mon Sep 17 00:00:00 2001 From: Christian Dennig Date: Wed, 23 Dec 2020 13:22:09 +0100 Subject: [PATCH 26/56] Fix flagging change as immutable when finding a field that has "-" tag before the actual field --- patch_struct.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/patch_struct.go b/patch_struct.go index d240076..bcd5fa7 100644 --- a/patch_struct.go +++ b/patch_struct.go @@ -15,8 +15,7 @@ func (c *ChangeValue) patchStruct() { for i := 0; i < c.target.NumField(); i++ { f := c.target.Type().Field(i) tname := tagName("diff", f) - if tname == "-" || hasTagOption("diff", f, "immutable") { - c.SetFlag(OptionImmutable) + if tname == "-" { continue } if tname == field || f.Name == field { @@ -27,6 +26,9 @@ func (c *ChangeValue) patchStruct() { if hasTagOption("diff", f, "omitunequal") { c.SetFlag(OptionOmitUnequal) } + if hasTagOption("diff", f, "immutable") { + c.SetFlag(OptionImmutable) + } c.swap(&x) break } From d6057da21326e0b8a544a52d560e781c7e1d053f Mon Sep 17 00:00:00 2001 From: Johan Sandelin Date: Tue, 2 Feb 2021 14:20:29 +0100 Subject: [PATCH 27/56] Make Patch a method on Differ, so we can specify custom TagName (#46) * Make Patch a method on Differ, so we can specify custom TagName --- README.md | 65 +++++++++++++++++++++++++++++++++++++------------ patch.go | 39 ++++++++++++++++++----------- patch_map.go | 7 +++--- patch_slice.go | 4 +-- patch_struct.go | 12 ++++----- 5 files changed, 86 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 992bdc8..5d0a549 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ A diffable value can be/contain any of the following types: | Type | Supported | -|--------------|-----------| +| ------------ | --------- | | struct | ✔ | | slice | ✔ | | string | ✔ | @@ -69,12 +69,12 @@ Please see the docs for more supported types, options and features. In order for struct fields to be compared, they must be tagged with a given name. All tag values are prefixed with `diff`. i.e. `diff:"items"`. -| Tag | Usage | -|--------------|------------------------------------| -| `-` | Excludes a value from being diffed | -| `identifier` | If you need to compare arrays by a matching identifier and not based on order, you can specify the `identifier` tag. If an identifiable element is found in both the from and to structures, they will be directly compared. i.e. `diff:"name, identifier"` | -| `immutable` | Will omit this struct field from diffing. When using `diff.StructValues()` these values will be added to the returned changelog. It's use case is for when we have nothing to compare a struct to and want to show all of its relevant values. | -| `nocreate` | The default patch action is to allocate instances in the target strut, map or slice should they not exist. Adding this flag will tell patch to skip elements that it would otherwise need to allocate. This is separate from immutable, which is also honored while patching. | +| Tag | Usage | +| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `-` | Excludes a value from being diffed | +| `identifier` | If you need to compare arrays by a matching identifier and not based on order, you can specify the `identifier` tag. If an identifiable element is found in both the from and to structures, they will be directly compared. i.e. `diff:"name, identifier"` | +| `immutable` | Will omit this struct field from diffing. When using `diff.StructValues()` these values will be added to the returned changelog. It's use case is for when we have nothing to compare a struct to and want to show all of its relevant values. | +| `nocreate` | The default patch action is to allocate instances in the target strut, map or slice should they not exist. Adding this flag will tell patch to skip elements that it would otherwise need to allocate. This is separate from immutable, which is also honored while patching. | | `omitunequal` | Patching is a 'best effort' operation, and will by default attempt to update the 'correct' member of the target even if the underlying value has already changed to something other than the value in the change log 'from'. This tag will selectively ignore values that are not a 100% match. | ## Usage @@ -84,7 +84,7 @@ In order for struct fields to be compared, they must be tagged with a given name Diffing a basic set of values can be accomplished using the diff functions. Any items that specify a "diff" tag using a name will be compared. ```go -import "github.com/r3labs/diff" +import "github.com/r3labs/diff/v2" type Order struct { ID string `diff:"id"` @@ -125,7 +125,7 @@ When marshalling the changelog to json, the output will look like: Options can be set on the differ at call time which effect how diff acts when building the change log. ```go -import "github.com/r3labs/diff" +import "github.com/r3labs/diff/v2" type Order struct { ID string `diff:"id"` @@ -151,7 +151,7 @@ func main() { You can also create a new instance of a differ that allows options to be set. ```go -import "github.com/r3labs/diff" +import "github.com/r3labs/diff/v2" type Order struct { ID string `diff:"id"` @@ -169,10 +169,10 @@ func main() { Items: []int{1, 2, 4}, } - d, err := diff.NewDiffer(diff.SliceOrdering(true)) - if err != nil { - panic(err) - } + d, err := diff.NewDiffer(diff.SliceOrdering(true)) + if err != nil { + panic(err) + } changelog, err := d.Diff(a, b) ... @@ -205,7 +205,7 @@ To accommodate this patch keeps track of each change log option it attempts to a happened for further scrutiny. ```go -import "github.com/r3labs/diff" +import "github.com/r3labs/diff/v2" type Order struct { ID string `diff:"id"` @@ -234,11 +234,44 @@ func main() { } ``` +Instances of differ with options set can also be used when patching. + +```go +package main + +import "github.com/r3labs/diff/v2" + +type Order struct { + ID string `json:"id"` + Items []int `json:"items"` +} + +func main() { + a := Order{ + ID: "1234", + Items: []int{1, 2, 3, 4}, + } + + b := Order{ + ID: "1234", + Items: []int{1, 2, 4}, + } + + d, _ := diff.NewDiffer(diff.TagName("json")) + + changelog, _ := d.Diff(a, b) + + d.Patch(changelog, &a) + // reflect.DeepEqual(a, b) == true +} + +``` + As a convenience, there is a Merge function that allows one to take three interfaces and perform all the tasks at the same time. ```go -import "github.com/r3labs/diff" +import "github.com/r3labs/diff/v2" type Order struct { ID string `diff:"id"` diff --git a/patch.go b/patch.go index 49314ad..288f893 100644 --- a/patch.go +++ b/patch.go @@ -107,20 +107,31 @@ func (p PatchLog) ErrorCount() (ret uint) { return } +func Merge(original interface{}, changed interface{}, target interface{}) (PatchLog, error) { + d, _ := NewDiffer() + return d.Merge(original, changed, target) +} + // Merge is a convenience function that diffs, the original and changed items // and merges said changes with target all in one call. -func Merge(original interface{}, changed interface{}, target interface{}) (PatchLog, error) { - if cl, err := Diff(original, changed, StructMapKeySupport()); err == nil { +func (d *Differ) Merge(original interface{}, changed interface{}, target interface{}) (PatchLog, error) { + StructMapKeySupport()(d) + if cl, err := d.Diff(original, changed); err == nil { return Patch(cl, target), nil } else { return nil, err } } -//Patch... the missing feature. func Patch(cl Changelog, target interface{}) (ret PatchLog) { + d, _ := NewDiffer() + return d.Patch(cl, target) +} + +//Patch... the missing feature. +func (d *Differ) Patch(cl Changelog, target interface{}) (ret PatchLog) { for _, c := range cl { - ret = append(ret, NewPatchLogEntry(NewChangeValue(c, target))) + ret = append(ret, NewPatchLogEntry(NewChangeValue(d, c, target))) } return ret } @@ -138,19 +149,19 @@ func NewPatchLogEntry(cv *ChangeValue) PatchLogEntry { } //NewChangeValue idiomatic constructor (also invokes render) -func NewChangeValue(c Change, target interface{}) (ret *ChangeValue) { +func NewChangeValue(d *Differ, c Change, target interface{}) (ret *ChangeValue) { val := reflect.ValueOf(target) ret = &ChangeValue{ target: &val, change: &c, } - renderChangeTarget(ret) + d.renderChangeTarget(ret) return } //renderChangeValue applies 'path' in change to target. nil check is foregone // here as we control usage -func renderChangeTarget(c *ChangeValue) { +func (d *Differ) renderChangeTarget(c *ChangeValue) { //This particular change element may potentially have the immutable flag if c.HasFlag(OptionImmutable) { @@ -167,12 +178,12 @@ func renderChangeTarget(c *ChangeValue) { //map elements are 'copies' and immutable so if we set the new value to the //map prior to editing the value, it will fail to stick. To fix this, we //defer the safe until the stack unwinds - m, k, v := c.renderMap() - defer c.deleteMapEntry(m, k, v) + m, k, v := d.renderMap(c) + defer d.deleteMapEntry(c, m, k, v) //path element that is a slice case reflect.Slice: - c.renderSlice() + d.renderSlice(c) //walking a path means dealing with real elements case reflect.Interface, reflect.Ptr: @@ -182,7 +193,7 @@ func renderChangeTarget(c *ChangeValue) { //path element that is a struct case reflect.Struct: - c.patchStruct() + d.patchStruct(c) } //if for some reason, rendering this element fails, c will no longer be valid @@ -194,16 +205,16 @@ func renderChangeTarget(c *ChangeValue) { //we've taken care of this path element, are there any more? if so, process //else, let's take some action if c.pos < len(c.change.Path) && !c.HasFlag(FlagInvalidTarget) { - renderChangeTarget(c) + d.renderChangeTarget(c) } else { //we're at the end of the line... set the Value switch c.change.Type { case DELETE: switch c.ParentKind() { case reflect.Slice: - c.deleteSliceEntry() + d.deleteSliceEntry(c) case reflect.Struct: - c.deleteStructEntry() + d.deleteStructEntry(c) default: c.SetFlag(FlagIgnored) } diff --git a/patch_map.go b/patch_map.go index add4e76..5eb7698 100644 --- a/patch_map.go +++ b/patch_map.go @@ -1,12 +1,13 @@ package diff import ( - "github.com/vmihailenco/msgpack" "reflect" + + "github.com/vmihailenco/msgpack" ) //renderMap - handle map rendering for patch -func (c *ChangeValue) renderMap() (m, k, v *reflect.Value) { +func (d *Differ) renderMap(c *ChangeValue) (m, k, v *reflect.Value) { //we must tease out the type of the key, we use the msgpack from diff to recreate the key kt := c.target.Type().Key() @@ -47,7 +48,7 @@ func (c *ChangeValue) renderMap() (m, k, v *reflect.Value) { //deleteMapEntry - deletes are special, they are handled differently based on options // container type etc. We have to have special handling for each // type. Set values are more generic even if they must be instanced -func (c *ChangeValue) deleteMapEntry(m, k, v *reflect.Value) { +func (d *Differ) deleteMapEntry(c *ChangeValue, m, k, v *reflect.Value) { if m != nil && m.CanSet() && v.IsValid() { for x := 0; x < v.NumField(); x++ { if !v.Field(x).IsZero() { diff --git a/patch_slice.go b/patch_slice.go index 707ea70..b8a72c7 100644 --- a/patch_slice.go +++ b/patch_slice.go @@ -10,7 +10,7 @@ import ( ) //renderSlice - handle slice rendering for patch -func (c *ChangeValue) renderSlice() { +func (d *Differ) renderSlice(c *ChangeValue) { var err error field := c.change.Path[c.pos] @@ -50,7 +50,7 @@ func (c *ChangeValue) renderSlice() { //deleteSliceEntry - deletes are special, they are handled differently based on options // container type etc. We have to have special handling for each // type. Set values are more generic even if they must be instanced -func (c *ChangeValue) deleteSliceEntry() { +func (d *Differ) deleteSliceEntry(c *ChangeValue) { //for a slice with only one element if c.ParentLen() == 1 && c.index != -1 { c.ParentSet(reflect.MakeSlice(c.parent.Type(), 0, 0)) diff --git a/patch_struct.go b/patch_struct.go index bcd5fa7..2275b3e 100644 --- a/patch_struct.go +++ b/patch_struct.go @@ -8,25 +8,25 @@ import "reflect" */ //patchStruct - handles the rendering of a struct field -func (c *ChangeValue) patchStruct() { +func (d *Differ) patchStruct(c *ChangeValue) { field := c.change.Path[c.pos] for i := 0; i < c.target.NumField(); i++ { f := c.target.Type().Field(i) - tname := tagName("diff", f) + tname := tagName(d.TagName, f) if tname == "-" { continue } if tname == field || f.Name == field { x := c.target.Field(i) - if hasTagOption("diff", f, "nocreate") { + if hasTagOption(d.TagName, f, "nocreate") { c.SetFlag(OptionNoCreate) } - if hasTagOption("diff", f, "omitunequal") { + if hasTagOption(d.TagName, f, "omitunequal") { c.SetFlag(OptionOmitUnequal) } - if hasTagOption("diff", f, "immutable") { + if hasTagOption(d.TagName, f, "immutable") { c.SetFlag(OptionImmutable) } c.swap(&x) @@ -36,7 +36,7 @@ func (c *ChangeValue) patchStruct() { } //track and zero out struct members -func (c *ChangeValue) deleteStructEntry() { +func (d *Differ) deleteStructEntry(c *ChangeValue) { //deleting a struct value set's it to the 'basic' type c.Set(reflect.Zero(c.target.Type())) From 423a001667bd0ff76cfd5b7fac9e1b4c16142e45 Mon Sep 17 00:00:00 2001 From: Johan Sandelin Date: Thu, 4 Feb 2021 18:01:36 +0100 Subject: [PATCH 28/56] Add option to flatten embedded structs (#48) * Add option to flatten embedded structs --- diff.go | 22 +++--- diff_struct.go | 5 +- diff_test.go | 21 +++++ options.go | 8 ++ patch_struct.go | 29 ++++++- patch_test.go | 198 ++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 269 insertions(+), 14 deletions(-) create mode 100644 patch_test.go diff --git a/diff.go b/diff.go index 851b7b6..b54c1fd 100644 --- a/diff.go +++ b/diff.go @@ -7,10 +7,11 @@ package diff import ( "errors" "fmt" - "github.com/vmihailenco/msgpack" "reflect" "strconv" "strings" + + "github.com/vmihailenco/msgpack" ) const ( @@ -24,15 +25,16 @@ const ( // Differ a configurable diff instance type Differ struct { - TagName string - SliceOrdering bool - DisableStructValues bool - customValueDiffers []ValueDiffer - cl Changelog - AllowTypeMismatch bool - DiscardParent bool - StructMapKeys bool - Filter FilterFunc + TagName string + SliceOrdering bool + DisableStructValues bool + customValueDiffers []ValueDiffer + cl Changelog + AllowTypeMismatch bool + DiscardParent bool + StructMapKeys bool + FlattenEmbeddedStructs bool + Filter FilterFunc } // Changelog stores a list of changed items diff --git a/diff_struct.go b/diff_struct.go index 320e064..f69ed73 100644 --- a/diff_struct.go +++ b/diff_struct.go @@ -45,7 +45,10 @@ func (d *Differ) diffStruct(path []string, a, b reflect.Value) error { af := a.Field(i) bf := b.FieldByName(field.Name) - fpath := copyAppend(path, tname) + fpath := path + if !field.Anonymous { + fpath = copyAppend(fpath, tname) + } if d.Filter != nil && !d.Filter(fpath, a.Type(), field) { continue diff --git a/diff_test.go b/diff_test.go index 711f425..145263a 100644 --- a/diff_test.go +++ b/diff_test.go @@ -36,6 +36,16 @@ type tmstruct struct { Bar int `diff:"bar"` } +type Embedded struct { + Foo string `diff:"foo"` + Bar int `diff:"bar"` +} + +type embedstruct struct { + Embedded + Baz bool `diff:"baz"` +} + type tstruct struct { ID string `diff:"id,immutable"` Name string `diff:"name"` @@ -395,6 +405,17 @@ func TestDiff(t *testing.T) { }, nil, }, + { + "embedded-struct-field", + embedstruct{Embedded{Foo: "a", Bar: 2}, true}, + embedstruct{Embedded{Foo: "b", Bar: 3}, false}, + Changelog{ + Change{Type: UPDATE, Path: []string{"foo"}, From: "a", To: "b"}, + Change{Type: UPDATE, Path: []string{"bar"}, From: 2, To: 3}, + Change{Type: UPDATE, Path: []string{"baz"}, From: true, To: false}, + }, + nil, + }, } for _, tc := range cases { diff --git a/options.go b/options.go index 8f65b63..656793c 100644 --- a/options.go +++ b/options.go @@ -1,5 +1,13 @@ package diff +// FlattenEmbeddedStructs determines whether fields of embedded structs should behave as if they are directly under the parent +func FlattenEmbeddedStructs(enabled bool) func(d *Differ) error { + return func(d *Differ) error { + d.FlattenEmbeddedStructs = enabled + return nil + } +} + // SliceOrdering determines whether the ordering of items in a slice results in a change func SliceOrdering(enabled bool) func(d *Differ) error { return func(d *Differ) error { diff --git a/patch_struct.go b/patch_struct.go index 2275b3e..9c7ce29 100644 --- a/patch_struct.go +++ b/patch_struct.go @@ -7,19 +7,42 @@ import "reflect" in place. Keeps the file simpler as well. */ +type structField struct { + f reflect.StructField + v reflect.Value +} + +func getNestedFields(v reflect.Value, flattenEmbedded bool) []structField { + fields := make([]structField, 0) + + for i := 0; i < v.NumField(); i++ { + f := v.Type().Field(i) + fv := v.Field(i) + + if fv.Kind() == reflect.Struct && f.Anonymous && flattenEmbedded { + fields = append(fields, getNestedFields(fv, flattenEmbedded)...) + } else { + fields = append(fields, structField{f, fv}) + } + } + + return fields +} + //patchStruct - handles the rendering of a struct field func (d *Differ) patchStruct(c *ChangeValue) { field := c.change.Path[c.pos] - for i := 0; i < c.target.NumField(); i++ { - f := c.target.Type().Field(i) + structFields := getNestedFields(*c.target, d.FlattenEmbeddedStructs) + for _, structField := range structFields { + f := structField.f tname := tagName(d.TagName, f) if tname == "-" { continue } if tname == field || f.Name == field { - x := c.target.Field(i) + x := structField.v if hasTagOption(d.TagName, f, "nocreate") { c.SetFlag(OptionNoCreate) } diff --git a/patch_test.go b/patch_test.go new file mode 100644 index 0000000..d711917 --- /dev/null +++ b/patch_test.go @@ -0,0 +1,198 @@ +package diff + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPatch(t *testing.T) { + cases := []struct { + Name string + A, B interface{} + Changelog Changelog + Error error + }{ + { + "uint-slice-insert", &[]uint{1, 2, 3}, &[]uint{1, 2, 3, 4}, + Changelog{ + Change{Type: CREATE, Path: []string{"3"}, To: uint(4)}, + }, + nil, + }, + { + "int-slice-insert", &[]int{1, 2, 3}, &[]int{1, 2, 3, 4}, + Changelog{ + Change{Type: CREATE, Path: []string{"3"}, To: 4}, + }, + nil, + }, + { + "uint-slice-delete", &[]uint{1, 2, 3}, &[]uint{1, 3}, + Changelog{ + Change{Type: DELETE, Path: []string{"1"}, From: uint(2)}, + }, + nil, + }, + { + "int-slice-delete", &[]int{1, 2, 3}, &[]int{1, 3}, + Changelog{ + Change{Type: DELETE, Path: []string{"1"}, From: 2}, + }, + nil, + }, + { + "uint-slice-insert-delete", &[]uint{1, 2, 3}, &[]uint{1, 3, 4}, + Changelog{ + Change{Type: DELETE, Path: []string{"1"}, From: uint(2)}, + Change{Type: CREATE, Path: []string{"2"}, To: uint(4)}, + }, + nil, + }, + { + "int-slice-insert-delete", &[]int{1, 2, 3}, &[]int{1, 3, 4}, + Changelog{ + Change{Type: DELETE, Path: []string{"1"}, From: 2}, + Change{Type: CREATE, Path: []string{"2"}, To: 4}, + }, + nil, + }, + { + "string-slice-insert", &[]string{"1", "2", "3"}, &[]string{"1", "2", "3", "4"}, + Changelog{ + Change{Type: CREATE, Path: []string{"3"}, To: "4"}, + }, + nil, + }, + { + "string-slice-delete", &[]string{"1", "2", "3"}, &[]string{"1", "3"}, + Changelog{ + Change{Type: DELETE, Path: []string{"1"}, From: "2"}, + }, + nil, + }, + { + "string-slice-insert-delete", &[]string{"1", "2", "3"}, &[]string{"1", "3", "4"}, + Changelog{ + Change{Type: DELETE, Path: []string{"1"}, From: "2"}, + Change{Type: CREATE, Path: []string{"2"}, To: "4"}, + }, + nil, + }, + { + "comparable-slice-update", &[]tistruct{{"one", 1}}, &[]tistruct{{"one", 50}}, + Changelog{ + Change{Type: UPDATE, Path: []string{"one", "value"}, From: 1, To: 50}, + }, + nil, + }, + { + "struct-string-update", &tstruct{Name: "one"}, &tstruct{Name: "two"}, + Changelog{ + Change{Type: UPDATE, Path: []string{"name"}, From: "one", To: "two"}, + }, + nil, + }, + { + "struct-int-update", &tstruct{Value: 1}, &tstruct{Value: 50}, + Changelog{ + Change{Type: UPDATE, Path: []string{"value"}, From: 1, To: 50}, + }, + nil, + }, + { + "struct-bool-update", &tstruct{Bool: true}, &tstruct{Bool: false}, + Changelog{ + Change{Type: UPDATE, Path: []string{"bool"}, From: true, To: false}, + }, + nil, + }, + { + "struct-time-update", &tstruct{}, &tstruct{Time: currentTime}, + Changelog{ + Change{Type: UPDATE, Path: []string{"time"}, From: time.Time{}, To: currentTime}, + }, + nil, + }, + { + "struct-nil-string-pointer-update", &tstruct{Pointer: nil}, &tstruct{Pointer: sptr("test")}, + Changelog{ + Change{Type: UPDATE, Path: []string{"pointer"}, From: nil, To: sptr("test")}, + }, + nil, + }, + { + "struct-generic-slice-insert", &tstruct{Values: []string{"one"}}, &tstruct{Values: []string{"one", "two"}}, + Changelog{ + Change{Type: CREATE, Path: []string{"values", "1"}, From: nil, To: "two"}, + }, + nil, + }, + { + "struct-generic-slice-delete", &tstruct{Values: []string{"one", "two"}}, &tstruct{Values: []string{"one"}}, + Changelog{ + Change{Type: DELETE, Path: []string{"values", "1"}, From: "two", To: nil}, + }, + nil, + }, + { + "struct-unidentifiable-slice-insert-delete", &tstruct{Unidentifiables: []tuistruct{{1}, {2}, {3}}}, &tstruct{Unidentifiables: []tuistruct{{5}, {2}, {3}, {4}}}, + Changelog{ + Change{Type: UPDATE, Path: []string{"unidentifiables", "0", "value"}, From: 1, To: 5}, + Change{Type: CREATE, Path: []string{"unidentifiables", "3", "value"}, From: nil, To: 4}, + }, + nil, + }, + { + "slice", &tstruct{}, &tstruct{Nested: tnstruct{Slice: []tmstruct{{"one", 1}, {"two", 2}}}}, + Changelog{ + Change{Type: CREATE, Path: []string{"nested", "slice", "0", "foo"}, From: nil, To: "one"}, + Change{Type: CREATE, Path: []string{"nested", "slice", "0", "bar"}, From: nil, To: 1}, + Change{Type: CREATE, Path: []string{"nested", "slice", "1", "foo"}, From: nil, To: "two"}, + Change{Type: CREATE, Path: []string{"nested", "slice", "1", "bar"}, From: nil, To: 2}, + }, + nil, + }, + { + "slice-duplicate-items", &[]int{1}, &[]int{1, 1}, + Changelog{ + Change{Type: CREATE, Path: []string{"1"}, From: nil, To: 1}, + }, + nil, + }, + { + "embedded-struct-field", + &embedstruct{Embedded{Foo: "a", Bar: 2}, true}, + &embedstruct{Embedded{Foo: "b", Bar: 3}, false}, + Changelog{ + Change{Type: UPDATE, Path: []string{"foo"}, From: "a", To: "b"}, + Change{Type: UPDATE, Path: []string{"bar"}, From: 2, To: 3}, + Change{Type: UPDATE, Path: []string{"baz"}, From: true, To: false}, + }, + nil, + }, + } + + for _, tc := range cases { + t.Run(tc.Name, func(t *testing.T) { + + var options []func(d *Differ) error + switch tc.Name { + case "mixed-slice-map", "nil-map", "map-nil": + options = append(options, StructMapKeySupport()) + case "embedded-struct-field": + options = append(options, FlattenEmbeddedStructs(true)) + } + d, err := NewDiffer(options...) + if err != nil { + panic(err) + } + pl := d.Patch(tc.Changelog, tc.A) + + assert.Equal(t, tc.B, tc.A) + require.Equal(t, len(tc.Changelog), len(pl)) + }) + } +} From 9e5baa48b46342ddadbe3da277ffbcbfa6ea0c4c Mon Sep 17 00:00:00 2001 From: Johan Sandelin Date: Tue, 9 Feb 2021 11:43:36 +0100 Subject: [PATCH 29/56] Add option to convert between compatible types (#49) * Linting fixes, improve handling of custom types - Fix bug that made Patch always behave as FlattenEmbeddedStructs was set - Add option ConvertCompatibleTypes that makes Patch convert between compatible types when applying changes - Make Diff store specific type in changelog if the type is an alias for string. Previously they were just stored as string. --- change_value.go | 29 +++++++++++++++++++++++---- diff.go | 13 +++++++------ diff_map.go | 6 ++++-- diff_slice.go | 2 +- diff_string.go | 7 ++++++- diff_struct.go | 2 +- diff_test.go | 46 ++++++++++++++++++++++++++++++++++++++----- error.go | 2 +- options.go | 14 ++++++++++--- patch.go | 4 ++-- patch_slice.go | 4 ++-- patch_struct.go | 2 +- patch_test.go | 52 ++++++++++++++++++++++++++++++++++++++++++++++++- 13 files changed, 153 insertions(+), 30 deletions(-) diff --git a/change_value.go b/change_value.go index bb5867d..7d7ca08 100644 --- a/change_value.go +++ b/change_value.go @@ -1,6 +1,7 @@ package diff import ( + "fmt" "reflect" ) @@ -73,14 +74,24 @@ func (c ChangeValue) ParentLen() (ret int) { } //ParentSet - nil safe parent set -func (c *ChangeValue) ParentSet(value reflect.Value) { +func (c *ChangeValue) ParentSet(value reflect.Value, convertCompatibleTypes bool) { if c != nil && c.parent != nil { defer func() { if r := recover(); r != nil { c.SetFlag(FlagParentSetFailed) } }() - c.parent.Set(value) + + if convertCompatibleTypes { + if !value.Type().ConvertibleTo(c.parent.Type()) { + c.AddError(fmt.Errorf("Value of type %s is not convertible to %s", value.Type().String(), c.parent.Type().String())) + c.SetFlag(FlagParentSetFailed) + return + } + c.parent.Set(value.Convert(c.parent.Type())) + } else { + c.parent.Set(value) + } c.SetFlag(FlagParentSetApplied) } } @@ -91,7 +102,7 @@ func (c ChangeValue) Len() int { } //Set echos reflect set -func (c *ChangeValue) Set(value reflect.Value) { +func (c *ChangeValue) Set(value reflect.Value, convertCompatibleTypes bool) { if c != nil { defer func() { if r := recover(); r != nil { @@ -103,7 +114,17 @@ func (c *ChangeValue) Set(value reflect.Value) { c.SetFlag(FlagIgnored) return } - c.target.Set(value) + + if convertCompatibleTypes { + if !value.Type().ConvertibleTo(c.target.Type()) { + c.AddError(fmt.Errorf("Value of type %s is not convertible to %s", value.Type().String(), c.target.Type().String())) + c.SetFlag(FlagFailed) + return + } + c.target.Set(value.Convert(c.target.Type())) + } else { + c.target.Set(value) + } c.SetFlag(FlagApplied) } } diff --git a/diff.go b/diff.go index b54c1fd..fe0b415 100644 --- a/diff.go +++ b/diff.go @@ -34,6 +34,7 @@ type Differ struct { DiscardParent bool StructMapKeys bool FlattenEmbeddedStructs bool + ConvertCompatibleTypes bool Filter FilterFunc } @@ -252,11 +253,11 @@ func swapChange(t string, c Change) Change { } func idComplex(v interface{}) string { - switch v.(type) { + switch v := v.(type) { case string: - return v.(string) + return v case int: - return strconv.Itoa(v.(int)) + return strconv.Itoa(v) default: b, err := msgpack.Marshal(v) if err != nil { @@ -267,11 +268,11 @@ func idComplex(v interface{}) string { } func idstring(v interface{}) string { - switch v.(type) { + switch v := v.(type) { case string: - return v.(string) + return v case int: - return strconv.Itoa(v.(int)) + return strconv.Itoa(v) default: return fmt.Sprint(v) } diff --git a/diff_map.go b/diff_map.go index 32fbce1..11a17f3 100644 --- a/diff_map.go +++ b/diff_map.go @@ -6,8 +6,9 @@ package diff import ( "fmt" - "github.com/vmihailenco/msgpack" "reflect" + + "github.com/vmihailenco/msgpack" ) func (d *Differ) diffMap(path []string, a, b reflect.Value) error { @@ -57,7 +58,8 @@ func (d *Differ) mapValues(t string, path []string, a reflect.Value) error { if d.StructMapKeys { //it's not enough to turn k to a string, we need to able to marshal a type when //we apply it in patch so... we'll marshal it to JSON - if b, err := msgpack.Marshal(k.Interface()); err == nil { + var b []byte + if b, err = msgpack.Marshal(k.Interface()); err == nil { err = d.diff(append(path, string(b)), xe, ae, a.Interface()) } } else { diff --git a/diff_slice.go b/diff_slice.go index 4a36d27..dff1fae 100644 --- a/diff_slice.go +++ b/diff_slice.go @@ -90,7 +90,7 @@ type sliceTracker []bool func (st *sliceTracker) has(s, v reflect.Value) bool { if len(*st) != s.Len() { - (*st) = make([]bool, s.Len(), s.Len()) + (*st) = make([]bool, s.Len()) } for i := 0; i < s.Len(); i++ { diff --git a/diff_string.go b/diff_string.go index 15ebe1b..abf28ce 100644 --- a/diff_string.go +++ b/diff_string.go @@ -22,7 +22,12 @@ func (d *Differ) diffString(path []string, a, b reflect.Value, parent interface{ } if a.String() != b.String() { - d.cl.Add(UPDATE, path, a.String(), b.String(), parent) + if a.CanInterface() { + // If a and/or b is of a type that is an alias for String, store that type in changelog + d.cl.Add(UPDATE, path, a.Interface(), b.Interface(), parent) + } else { + d.cl.Add(UPDATE, path, a.String(), b.String(), parent) + } } return nil diff --git a/diff_struct.go b/diff_struct.go index f69ed73..57298b5 100644 --- a/diff_struct.go +++ b/diff_struct.go @@ -46,7 +46,7 @@ func (d *Differ) diffStruct(path []string, a, b reflect.Value) error { bf := b.FieldByName(field.Name) fpath := path - if !field.Anonymous { + if !(d.FlattenEmbeddedStructs && field.Anonymous) { fpath = copyAppend(fpath, tname) } diff --git a/diff_test.go b/diff_test.go index 145263a..f39c81d 100644 --- a/diff_test.go +++ b/diff_test.go @@ -46,6 +46,18 @@ type embedstruct struct { Baz bool `diff:"baz"` } +type customTagStruct struct { + Foo string `json:"foo"` + Bar int `json:"bar"` +} + +type CustomStringType string +type CustomIntType int +type customTypeStruct struct { + Foo CustomStringType `diff:"foo"` + Bar CustomIntType `diff:"bar"` +} + type tstruct struct { ID string `diff:"id,immutable"` Name string `diff:"name"` @@ -233,21 +245,21 @@ func TestDiff(t *testing.T) { nil, }, { - "nested-slice-insert", map[string][]int{"a": []int{1, 2, 3}}, map[string][]int{"a": []int{1, 2, 3, 4}}, + "nested-slice-insert", map[string][]int{"a": {1, 2, 3}}, map[string][]int{"a": {1, 2, 3, 4}}, Changelog{ Change{Type: CREATE, Path: []string{"a", "3"}, To: 4}, }, nil, }, { - "nested-slice-update", map[string][]int{"a": []int{1, 2, 3}}, map[string][]int{"a": []int{1, 4, 3}}, + "nested-slice-update", map[string][]int{"a": {1, 2, 3}}, map[string][]int{"a": {1, 4, 3}}, Changelog{ Change{Type: UPDATE, Path: []string{"a", "1"}, From: 2, To: 4}, }, nil, }, { - "nested-slice-delete", map[string][]int{"a": []int{1, 2, 3}}, map[string][]int{"a": []int{1, 3}}, + "nested-slice-delete", map[string][]int{"a": {1, 2, 3}}, map[string][]int{"a": {1, 3}}, Changelog{ Change{Type: DELETE, Path: []string{"a", "1"}, From: 2, To: nil}, }, @@ -416,6 +428,26 @@ func TestDiff(t *testing.T) { }, nil, }, + { + "custom-tags", + customTagStruct{Foo: "abc", Bar: 3}, + customTagStruct{Foo: "def", Bar: 4}, + Changelog{ + Change{Type: UPDATE, Path: []string{"foo"}, From: "abc", To: "def"}, + Change{Type: UPDATE, Path: []string{"bar"}, From: 3, To: 4}, + }, + nil, + }, + { + "custom-types", + customTypeStruct{Foo: "a", Bar: 1}, + customTypeStruct{Foo: "b", Bar: 2}, + Changelog{ + Change{Type: UPDATE, Path: []string{"foo"}, From: CustomStringType("a"), To: CustomStringType("b")}, + Change{Type: UPDATE, Path: []string{"bar"}, From: CustomIntType(1), To: CustomIntType(2)}, + }, + nil, + }, } for _, tc := range cases { @@ -425,6 +457,10 @@ func TestDiff(t *testing.T) { switch tc.Name { case "mixed-slice-map", "nil-map", "map-nil": options = append(options, StructMapKeySupport()) + case "embedded-struct-field": + options = append(options, FlattenEmbeddedStructs()) + case "custom-tags": + options = append(options, TagName("json")) } cl, err := Diff(tc.A, tc.B, options...) @@ -505,7 +541,7 @@ func TestDiffSliceOrdering(t *testing.T) { nil, }, { - "nested-slice-delete", map[string][]int{"a": []int{1, 2, 3}}, map[string][]int{"a": []int{1, 3}}, + "nested-slice-delete", map[string][]int{"a": {1, 2, 3}}, map[string][]int{"a": {1, 3}}, Changelog{ Change{Type: UPDATE, Path: []string{"a", "1"}, From: 2, To: 3}, Change{Type: DELETE, Path: []string{"a", "2"}, From: 3}, @@ -771,7 +807,7 @@ func TestRecursiveCustomDiffer(t *testing.T) { treeB := RecursiveTestStruct{ Id: 1, Children: []RecursiveTestStruct{ - RecursiveTestStruct{ + { Id: 4, Children: []RecursiveTestStruct{}, }, diff --git a/error.go b/error.go index 98403c9..0acc13f 100644 --- a/error.go +++ b/error.go @@ -68,7 +68,7 @@ func NewError(message string, causes ...error) *DiffError { message: message, } for _, cause := range causes { - s.WithCause(cause) + s.WithCause(cause) // nolint: errcheck } return s } diff --git a/options.go b/options.go index 656793c..fadbe86 100644 --- a/options.go +++ b/options.go @@ -1,9 +1,17 @@ package diff +// ConvertTypes enables values that are convertible to the target type to be converted when patching +func ConvertCompatibleTypes() func(d *Differ) error { + return func(d *Differ) error { + d.ConvertCompatibleTypes = true + return nil + } +} + // FlattenEmbeddedStructs determines whether fields of embedded structs should behave as if they are directly under the parent -func FlattenEmbeddedStructs(enabled bool) func(d *Differ) error { +func FlattenEmbeddedStructs() func(d *Differ) error { return func(d *Differ) error { - d.FlattenEmbeddedStructs = enabled + d.FlattenEmbeddedStructs = true return nil } } @@ -37,7 +45,7 @@ func DisableStructValues() func(d *Differ) error { func CustomValueDiffers(vd ...ValueDiffer) func(d *Differ) error { return func(d *Differ) error { d.customValueDiffers = append(d.customValueDiffers, vd...) - for k, _ := range d.customValueDiffers { + for k := range d.customValueDiffers { d.customValueDiffers[k].InsertParentDiffer(d.diff) } return nil diff --git a/patch.go b/patch.go index 288f893..e6b787f 100644 --- a/patch.go +++ b/patch.go @@ -115,7 +115,7 @@ func Merge(original interface{}, changed interface{}, target interface{}) (Patch // Merge is a convenience function that diffs, the original and changed items // and merges said changes with target all in one call. func (d *Differ) Merge(original interface{}, changed interface{}, target interface{}) (PatchLog, error) { - StructMapKeySupport()(d) + StructMapKeySupport()(d) // nolint: errcheck if cl, err := d.Diff(original, changed); err == nil { return Patch(cl, target), nil } else { @@ -221,7 +221,7 @@ func (d *Differ) renderChangeTarget(c *ChangeValue) { case UPDATE, CREATE: // this is generic because... we only deal in primitives here. AND // the diff format To field already contains the correct type. - c.Set(reflect.ValueOf(c.change.To)) + c.Set(reflect.ValueOf(c.change.To), d.ConvertCompatibleTypes) c.SetFlag(FlagUpdated) } } diff --git a/patch_slice.go b/patch_slice.go index b8a72c7..751f64c 100644 --- a/patch_slice.go +++ b/patch_slice.go @@ -53,12 +53,12 @@ func (d *Differ) renderSlice(c *ChangeValue) { func (d *Differ) deleteSliceEntry(c *ChangeValue) { //for a slice with only one element if c.ParentLen() == 1 && c.index != -1 { - c.ParentSet(reflect.MakeSlice(c.parent.Type(), 0, 0)) + c.ParentSet(reflect.MakeSlice(c.parent.Type(), 0, 0), d.ConvertCompatibleTypes) c.SetFlag(FlagDeleted) //for a slice with multiple elements } else if c.index != -1 { //this is an array delete the element from the parent c.ParentIndex(c.index).Set(c.ParentIndex(c.ParentLen() - 1)) - c.ParentSet(c.parent.Slice(0, c.ParentLen()-1)) + c.ParentSet(c.parent.Slice(0, c.ParentLen()-1), d.ConvertCompatibleTypes) c.SetFlag(FlagDeleted) //for other slice elements, we ignore } else { diff --git a/patch_struct.go b/patch_struct.go index 9c7ce29..4e2d247 100644 --- a/patch_struct.go +++ b/patch_struct.go @@ -62,5 +62,5 @@ func (d *Differ) patchStruct(c *ChangeValue) { func (d *Differ) deleteStructEntry(c *ChangeValue) { //deleting a struct value set's it to the 'basic' type - c.Set(reflect.Zero(c.target.Type())) + c.Set(reflect.Zero(c.target.Type()), d.ConvertCompatibleTypes) } diff --git a/patch_test.go b/patch_test.go index d711917..f90e9f9 100644 --- a/patch_test.go +++ b/patch_test.go @@ -173,6 +173,26 @@ func TestPatch(t *testing.T) { }, nil, }, + { + "custom-tags", + &customTagStruct{Foo: "abc", Bar: 3}, + &customTagStruct{Foo: "def", Bar: 4}, + Changelog{ + Change{Type: UPDATE, Path: []string{"foo"}, From: "abc", To: "def"}, + Change{Type: UPDATE, Path: []string{"bar"}, From: 3, To: 4}, + }, + nil, + }, + { + "custom-types", + &customTypeStruct{Foo: "a", Bar: 1}, + &customTypeStruct{Foo: "b", Bar: 2}, + Changelog{ + Change{Type: UPDATE, Path: []string{"foo"}, From: CustomStringType("a"), To: CustomStringType("b")}, + Change{Type: UPDATE, Path: []string{"bar"}, From: CustomIntType(1), To: CustomIntType(2)}, + }, + nil, + }, } for _, tc := range cases { @@ -183,7 +203,9 @@ func TestPatch(t *testing.T) { case "mixed-slice-map", "nil-map", "map-nil": options = append(options, StructMapKeySupport()) case "embedded-struct-field": - options = append(options, FlattenEmbeddedStructs(true)) + options = append(options, FlattenEmbeddedStructs()) + case "custom-tags": + options = append(options, TagName("json")) } d, err := NewDiffer(options...) if err != nil { @@ -195,4 +217,32 @@ func TestPatch(t *testing.T) { require.Equal(t, len(tc.Changelog), len(pl)) }) } + + t.Run("convert-types", func(t *testing.T) { + a := &tmstruct{Foo: "a", Bar: 1} + b := &customTypeStruct{Foo: "b", Bar: 2} + cl := Changelog{ + Change{Type: UPDATE, Path: []string{"foo"}, From: CustomStringType("a"), To: CustomStringType("b")}, + Change{Type: UPDATE, Path: []string{"bar"}, From: CustomIntType(1), To: CustomIntType(2)}, + } + + d, err := NewDiffer() + if err != nil { + panic(err) + } + pl := d.Patch(cl, a) + + assert.True(t, pl.HasErrors()) + + d, err = NewDiffer(ConvertCompatibleTypes()) + if err != nil { + panic(err) + } + pl = d.Patch(cl, a) + + assert.False(t, pl.HasErrors()) + assert.Equal(t, string(b.Foo), a.Foo) + assert.Equal(t, int(b.Bar), a.Bar) + require.Equal(t, len(cl), len(pl)) + }) } From 2060e5a1a5364e79f6acbc115e1b2b0027b3ce7d Mon Sep 17 00:00:00 2001 From: Mahmud Ridwan Date: Sun, 18 Apr 2021 16:14:01 +0600 Subject: [PATCH 30/56] Apply same filters when producing struct values Closes #53 --- diff_struct.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/diff_struct.go b/diff_struct.go index 57298b5..4e7045f 100644 --- a/diff_struct.go +++ b/diff_struct.go @@ -65,6 +65,7 @@ func (d *Differ) diffStruct(path []string, a, b reflect.Value) error { func (d *Differ) structValues(t string, path []string, a reflect.Value) error { var nd Differ + nd.Filter = d.Filter if t != CREATE && t != DELETE { return ErrInvalidChangeType @@ -98,6 +99,10 @@ func (d *Differ) structValues(t string, path []string, a reflect.Value) error { fpath := copyAppend(path, tname) + if nd.Filter != nil && !nd.Filter(fpath, a.Type(), field) { + continue + } + err := nd.diff(fpath, xf, af, a.Interface()) if err != nil { return err From 1352d2502ea919e89bd9dd91050c63709d8032df Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Mon, 24 May 2021 13:45:35 +0100 Subject: [PATCH 31/56] fix calling interface on struct values that are private --- diff_struct.go | 5 +++++ diff_test.go | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/diff_struct.go b/diff_struct.go index 4e7045f..e1dcb5c 100644 --- a/diff_struct.go +++ b/diff_struct.go @@ -54,6 +54,11 @@ func (d *Differ) diffStruct(path []string, a, b reflect.Value) error { continue } + // skip private fields + if !a.CanInterface() { + continue + } + err := d.diff(fpath, af, bf, a.Interface()) if err != nil { return err diff --git a/diff_test.go b/diff_test.go index f39c81d..a0427c2 100644 --- a/diff_test.go +++ b/diff_test.go @@ -6,6 +6,7 @@ package diff import ( "reflect" + "sync" "testing" "time" @@ -51,6 +52,11 @@ type customTagStruct struct { Bar int `json:"bar"` } +type privateValueStruct struct { + Public string + Private *sync.RWMutex +} + type CustomStringType string type CustomIntType int type customTypeStruct struct { @@ -352,6 +358,13 @@ func TestDiff(t *testing.T) { }, nil, }, + { + "struct-with-private-value", privateValueStruct{Public: "one", Private: new(sync.RWMutex)}, privateValueStruct{Public: "two", Private: new(sync.RWMutex)}, + Changelog{ + Change{Type: UPDATE, Path: []string{"Public"}, From: "one", To: "two"}, + }, + nil, + }, { "mismatched-values-struct-map", map[string]string{"test": "one"}, &tstruct{Identifiables: []tistruct{{"one", 1}}}, Changelog{}, From a077fc0fa13c8997f3b2d63f112e8d6617e1bdb2 Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Mon, 28 Jun 2021 18:57:57 +0100 Subject: [PATCH 32/56] fix patching map[string]... --- diff_examples_test.go | 3 ++- patch.go | 4 +++- patch_map.go | 51 ++++++++++++++++++++++++++++++++++++++----- patch_test.go | 12 ++++++++++ 4 files changed, 62 insertions(+), 8 deletions(-) diff --git a/diff_examples_test.go b/diff_examples_test.go index fa25194..f89b321 100644 --- a/diff_examples_test.go +++ b/diff_examples_test.go @@ -77,7 +77,8 @@ func ExamplePatchWithErrors() { patchLog := Patch(changelog, c) //this also demonstrated the nested errors with 'next' - errors := patchLog[7].Errors.(*DiffError) + + errors := patchLog[0].Errors.(*DiffError) //we can also continue to nest errors if we like message := errors.WithCause(NewError("This is a custom message")). diff --git a/patch.go b/patch.go index e6b787f..abe554c 100644 --- a/patch.go +++ b/patch.go @@ -1,6 +1,8 @@ package diff -import "reflect" +import ( + "reflect" +) /** This is a method of applying a changelog to a value or struct. change logs diff --git a/patch_map.go b/patch_map.go index 5eb7698..06a4541 100644 --- a/patch_map.go +++ b/patch_map.go @@ -1,6 +1,7 @@ package diff import ( + "errors" "reflect" "github.com/vmihailenco/msgpack" @@ -12,15 +13,36 @@ func (d *Differ) renderMap(c *ChangeValue) (m, k, v *reflect.Value) { //we must tease out the type of the key, we use the msgpack from diff to recreate the key kt := c.target.Type().Key() field := reflect.New(kt) - if err := msgpack.Unmarshal([]byte(c.change.Path[c.pos]), field.Interface()); err != nil { - c.SetFlag(FlagIgnored) - c.AddError(NewError("Unable to unmarshal path element to target type for key in map", err)) - return + + if d.StructMapKeys { + if err := msgpack.Unmarshal([]byte(c.change.Path[c.pos]), field.Interface()); err != nil { + c.SetFlag(FlagIgnored) + c.AddError(NewError("Unable to unmarshal path element to target type for key in map", err)) + return + } + c.key = field.Elem() + } else { + c.key = reflect.ValueOf(c.change.Path[c.pos]) } - c.key = field.Elem() + if c.target.IsNil() && c.target.IsValid() { c.target.Set(reflect.MakeMap(c.target.Type())) } + + // we need to check that MapIndex does not panic here + // when the key type is not a string + defer func() { + if err := recover(); err != nil { + switch x := err.(type) { + case error: + c.AddError(NewError("Unable to unmarshal path element to target type for key in map", x)) + case string: + c.AddError(NewError("Unable to unmarshal path element to target type for key in map", errors.New(x))) + } + c.SetFlag(FlagIgnored) + } + }() + x := c.target.MapIndex(c.key) if !x.IsValid() && c.change.Type != DELETE && !c.HasFlag(OptionNoCreate) { @@ -41,6 +63,7 @@ func (d *Differ) renderMap(c *ChangeValue) (m, k, v *reflect.Value) { mp := *c.target //these may change out from underneath us as we recurse key := c.key //so we make copies and pass back pointers to them c.swap(&x) + return &mp, &key, &x } @@ -49,7 +72,11 @@ func (d *Differ) renderMap(c *ChangeValue) (m, k, v *reflect.Value) { // container type etc. We have to have special handling for each // type. Set values are more generic even if they must be instanced func (d *Differ) deleteMapEntry(c *ChangeValue, m, k, v *reflect.Value) { - if m != nil && m.CanSet() && v.IsValid() { + if k == nil { + return + } + + if m != nil && m.CanSet() && v.IsValid() && v.Kind() != reflect.Int { for x := 0; x < v.NumField(); x++ { if !v.Field(x).IsZero() { m.SetMapIndex(*k, *v) @@ -58,5 +85,17 @@ func (d *Differ) deleteMapEntry(c *ChangeValue, m, k, v *reflect.Value) { } //if all the fields are zero, remove from map m.SetMapIndex(*k, reflect.Value{}) c.SetFlag(FlagDeleted) + } else { + switch c.change.Type { + case DELETE: + m.SetMapIndex(*k, reflect.Value{}) + c.SetFlag(FlagDeleted) + case CREATE: + m.SetMapIndex(*k, *v) + c.SetFlag(FlagCreated) + case UPDATE: + m.SetMapIndex(*k, *v) + c.SetFlag(FlagUpdated) + } } } diff --git a/patch_test.go b/patch_test.go index f90e9f9..706449f 100644 --- a/patch_test.go +++ b/patch_test.go @@ -193,6 +193,18 @@ func TestPatch(t *testing.T) { }, nil, }, + + { + "map", + map[string]interface{}{"1": "one", "3": "three"}, + map[string]interface{}{"2": "two", "3": "tres"}, + Changelog{ + Change{Type: DELETE, Path: []string{"1"}, From: "one", To: nil}, + Change{Type: CREATE, Path: []string{"2"}, From: nil, To: "two"}, + Change{Type: UPDATE, Path: []string{"3"}, From: "three", To: "tres"}, + }, + nil, + }, } for _, tc := range cases { From bad6782c1ca1b1a78f004cf69e64edf0eb2e170d Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Mon, 28 Jun 2021 19:34:55 +0100 Subject: [PATCH 33/56] only iterate over fields when map uses structs as keys --- patch_map.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patch_map.go b/patch_map.go index 06a4541..a5352a8 100644 --- a/patch_map.go +++ b/patch_map.go @@ -76,7 +76,7 @@ func (d *Differ) deleteMapEntry(c *ChangeValue, m, k, v *reflect.Value) { return } - if m != nil && m.CanSet() && v.IsValid() && v.Kind() != reflect.Int { + if m != nil && m.CanSet() && v.IsValid() && v.Kind() == reflect.Struct { for x := 0; x < v.NumField(); x++ { if !v.Field(x).IsZero() { m.SetMapIndex(*k, *v) From 69da8b89c42da6cda9f4365e4f472c8638bdb6e0 Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Wed, 30 Jun 2021 15:02:29 +0100 Subject: [PATCH 34/56] fix map deletes --- patch.go | 3 +-- patch_map.go | 52 ++++++++++++++++++++++++++++----------------------- patch_test.go | 28 ++++++++++++++++++++++++++- 3 files changed, 57 insertions(+), 26 deletions(-) diff --git a/patch.go b/patch.go index abe554c..73d2a90 100644 --- a/patch.go +++ b/patch.go @@ -164,7 +164,6 @@ func NewChangeValue(d *Differ, c Change, target interface{}) (ret *ChangeValue) //renderChangeValue applies 'path' in change to target. nil check is foregone // here as we control usage func (d *Differ) renderChangeTarget(c *ChangeValue) { - //This particular change element may potentially have the immutable flag if c.HasFlag(OptionImmutable) { c.AddError(NewError("Option immutable set, cannot apply change")) @@ -181,7 +180,7 @@ func (d *Differ) renderChangeTarget(c *ChangeValue) { //map prior to editing the value, it will fail to stick. To fix this, we //defer the safe until the stack unwinds m, k, v := d.renderMap(c) - defer d.deleteMapEntry(c, m, k, v) + defer d.updateMapEntry(c, m, k, v) //path element that is a slice case reflect.Slice: diff --git a/patch_map.go b/patch_map.go index a5352a8..16de66a 100644 --- a/patch_map.go +++ b/patch_map.go @@ -9,7 +9,6 @@ import ( //renderMap - handle map rendering for patch func (d *Differ) renderMap(c *ChangeValue) (m, k, v *reflect.Value) { - //we must tease out the type of the key, we use the msgpack from diff to recreate the key kt := c.target.Type().Key() field := reflect.New(kt) @@ -68,34 +67,41 @@ func (d *Differ) renderMap(c *ChangeValue) (m, k, v *reflect.Value) { } -//deleteMapEntry - deletes are special, they are handled differently based on options +// updateMapEntry - deletes are special, they are handled differently based on options // container type etc. We have to have special handling for each // type. Set values are more generic even if they must be instanced -func (d *Differ) deleteMapEntry(c *ChangeValue, m, k, v *reflect.Value) { - if k == nil { +func (d *Differ) updateMapEntry(c *ChangeValue, m, k, v *reflect.Value) { + if k == nil || m == nil { return } - if m != nil && m.CanSet() && v.IsValid() && v.Kind() == reflect.Struct { - for x := 0; x < v.NumField(); x++ { - if !v.Field(x).IsZero() { - m.SetMapIndex(*k, *v) - return - } - } //if all the fields are zero, remove from map + switch c.change.Type { + case DELETE: + if c.HasFlag(FlagDeleted) { + return + } + + if !m.CanSet() && v.IsValid() && v.Kind() == reflect.Struct { + for x := 0; x < v.NumField(); x++ { + if !v.Field(x).IsZero() { + m.SetMapIndex(*k, *v) + return + } + } //if all the fields are zero, remove from map + } + m.SetMapIndex(*k, reflect.Value{}) c.SetFlag(FlagDeleted) - } else { - switch c.change.Type { - case DELETE: - m.SetMapIndex(*k, reflect.Value{}) - c.SetFlag(FlagDeleted) - case CREATE: - m.SetMapIndex(*k, *v) - c.SetFlag(FlagCreated) - case UPDATE: - m.SetMapIndex(*k, *v) - c.SetFlag(FlagUpdated) - } + + case CREATE: + m.SetMapIndex(*k, *v) + c.SetFlag(FlagCreated) + + case UPDATE: + m.SetMapIndex(*k, *v) + c.SetFlag(FlagUpdated) + + default: + panic("NO TYPE SPECIFIED") } } diff --git a/patch_test.go b/patch_test.go index 706449f..a3958a7 100644 --- a/patch_test.go +++ b/patch_test.go @@ -193,7 +193,6 @@ func TestPatch(t *testing.T) { }, nil, }, - { "map", map[string]interface{}{"1": "one", "3": "three"}, @@ -205,6 +204,33 @@ func TestPatch(t *testing.T) { }, nil, }, + { + "map-nested-create", + map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active", "attributes": map[string]interface{}{"attrA": "A", "attrB": "B"}}}, + map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active", "attributes": map[string]interface{}{"attrA": "A", "attrB": "B"}, "secondary-attributes": map[string]interface{}{"attrA": "A", "attrB": "B"}}}, + Changelog{ + Change{Type: "delete", Path: []string{"details", "secondary-attributes"}, From: nil, To: map[string]interface{}{"attrA": "A", "attrB": "B"}}, + }, + nil, + }, + { + "map-nested-update", + map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active", "attributes": map[string]interface{}{"attrA": "A", "attrB": "B"}}}, + map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active", "attributes": map[string]interface{}{"attrA": "C", "attrD": "X"}}}, + Changelog{ + Change{Type: "delete", Path: []string{"details", "attributes"}, From: map[string]interface{}{"attrA": "A", "attrB": "B"}, To: map[string]interface{}{"attrA": "C", "attrD": "X"}}, + }, + nil, + }, + { + "map-nested-delete", + map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active", "attributes": map[string]interface{}{"attrA": "A", "attrB": "B"}}}, + map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active"}}, + Changelog{ + Change{Type: "delete", Path: []string{"details", "attributes"}, From: map[string]interface{}{"attrA": "A", "attrB": "B"}, To: nil}, + }, + nil, + }, } for _, tc := range cases { From 3113f40d0096a6b897b194185c41fbb97436c180 Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Wed, 30 Jun 2021 15:11:12 +0100 Subject: [PATCH 35/56] fix tests --- patch_map.go | 2 -- patch_test.go | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/patch_map.go b/patch_map.go index 16de66a..8a3906a 100644 --- a/patch_map.go +++ b/patch_map.go @@ -101,7 +101,5 @@ func (d *Differ) updateMapEntry(c *ChangeValue, m, k, v *reflect.Value) { m.SetMapIndex(*k, *v) c.SetFlag(FlagUpdated) - default: - panic("NO TYPE SPECIFIED") } } diff --git a/patch_test.go b/patch_test.go index a3958a7..3e8753b 100644 --- a/patch_test.go +++ b/patch_test.go @@ -209,7 +209,7 @@ func TestPatch(t *testing.T) { map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active", "attributes": map[string]interface{}{"attrA": "A", "attrB": "B"}}}, map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active", "attributes": map[string]interface{}{"attrA": "A", "attrB": "B"}, "secondary-attributes": map[string]interface{}{"attrA": "A", "attrB": "B"}}}, Changelog{ - Change{Type: "delete", Path: []string{"details", "secondary-attributes"}, From: nil, To: map[string]interface{}{"attrA": "A", "attrB": "B"}}, + Change{Type: "create", Path: []string{"details", "secondary-attributes"}, From: nil, To: map[string]interface{}{"attrA": "A", "attrB": "B"}}, }, nil, }, @@ -218,7 +218,7 @@ func TestPatch(t *testing.T) { map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active", "attributes": map[string]interface{}{"attrA": "A", "attrB": "B"}}}, map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active", "attributes": map[string]interface{}{"attrA": "C", "attrD": "X"}}}, Changelog{ - Change{Type: "delete", Path: []string{"details", "attributes"}, From: map[string]interface{}{"attrA": "A", "attrB": "B"}, To: map[string]interface{}{"attrA": "C", "attrD": "X"}}, + Change{Type: "update", Path: []string{"details", "attributes"}, From: map[string]interface{}{"attrA": "A", "attrB": "B"}, To: map[string]interface{}{"attrA": "C", "attrD": "X"}}, }, nil, }, From 5054057acdcbf805eb24fc01521728c9bcc7bdd4 Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Mon, 12 Jul 2021 17:21:31 +0100 Subject: [PATCH 36/56] assign an empty type when patch value is nil --- change_value.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/change_value.go b/change_value.go index 7d7ca08..4d721e9 100644 --- a/change_value.go +++ b/change_value.go @@ -123,7 +123,12 @@ func (c *ChangeValue) Set(value reflect.Value, convertCompatibleTypes bool) { } c.target.Set(value.Convert(c.target.Type())) } else { - c.target.Set(value) + if value.IsValid() { + c.target.Set(value) + } else { + t := c.target.Elem() + t.Set(reflect.Zero(t.Type())) + } } c.SetFlag(FlagApplied) } From 5f0fcde00ed846c3c1fe0b31335682743d9729a3 Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Mon, 12 Jul 2021 17:30:20 +0100 Subject: [PATCH 37/56] fix travis-ci badges --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5d0a549..ef16bc5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Diff [![PkgGoDev](https://pkg.go.dev/badge/github.com/r3labs/diff)](https://pkg.go.dev/github.com/r3labs/diff) [![Go Report Card](https://goreportcard.com/badge/github.com/r3labs/diff)](https://goreportcard.com/report/github.com/r3labs/diff) [![Build Status](https://travis-ci.org/r3labs/diff.svg?branch=master)](https://travis-ci.org/r3labs/diff) +# Diff [![PkgGoDev](https://pkg.go.dev/badge/github.com/r3labs/diff)](https://pkg.go.dev/github.com/r3labs/diff) [![Go Report Card](https://goreportcard.com/badge/github.com/r3labs/diff)](https://goreportcard.com/report/github.com/r3labs/diff) [![Build Status](https://travis-ci.com/r3labs/diff.svg?branch=master)](https://travis-ci.com/r3labs/diff) A library for diffing golang structures and values. From b0d6a43698f6e80056eaf10f72bb61a53cfa8146 Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Tue, 13 Jul 2021 09:52:58 +0100 Subject: [PATCH 38/56] fix struct assignment when value is zero --- change_value.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/change_value.go b/change_value.go index 4d721e9..2f8df91 100644 --- a/change_value.go +++ b/change_value.go @@ -125,7 +125,7 @@ func (c *ChangeValue) Set(value reflect.Value, convertCompatibleTypes bool) { } else { if value.IsValid() { c.target.Set(value) - } else { + } else if !c.target.IsZero() { t := c.target.Elem() t.Set(reflect.Zero(t.Type())) } From 514e132ff74525f0adaca8f73e28fad9aa464e91 Mon Sep 17 00:00:00 2001 From: Farhan Sajid <46803536+FarhanSajid1@users.noreply.github.com> Date: Mon, 6 Sep 2021 06:29:16 -0400 Subject: [PATCH 39/56] Add arrays to diff (#66) * add support for arrays --- diff.go | 2 + diff_test.go | 123 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/diff.go b/diff.go index fe0b415..e57abd3 100644 --- a/diff.go +++ b/diff.go @@ -166,6 +166,8 @@ func (d *Differ) diff(path []string, a, b reflect.Value, parent interface{}) err return d.diffStruct(path, a, b) case are(a, b, reflect.Slice, reflect.Invalid): return d.diffSlice(path, a, b) + case are(a, b, reflect.Array, reflect.Invalid): + return d.diffSlice(path, a, b) case are(a, b, reflect.String, reflect.Invalid): return d.diffString(path, a, b, parent) case are(a, b, reflect.Bool, reflect.Invalid): diff --git a/diff_test.go b/diff_test.go index a0427c2..6a9b76a 100644 --- a/diff_test.go +++ b/diff_test.go @@ -98,6 +98,13 @@ func TestDiff(t *testing.T) { }, nil, }, + { + "uint-array-insert", [3]uint{1, 2, 3}, [4]uint{1, 2, 3, 4}, + Changelog{ + Change{Type: CREATE, Path: []string{"3"}, To: uint(4)}, + }, + nil, + }, { "int-slice-insert", []int{1, 2, 3}, []int{1, 2, 3, 4}, Changelog{ @@ -105,6 +112,13 @@ func TestDiff(t *testing.T) { }, nil, }, + { + "int-array-insert", [3]int{1, 2, 3}, [4]int{1, 2, 3, 4}, + Changelog{ + Change{Type: CREATE, Path: []string{"3"}, To: 4}, + }, + nil, + }, { "uint-slice-delete", []uint{1, 2, 3}, []uint{1, 3}, Changelog{ @@ -112,6 +126,13 @@ func TestDiff(t *testing.T) { }, nil, }, + { + "uint-array-delete", [3]uint{1, 2, 3}, [2]uint{1, 3}, + Changelog{ + Change{Type: DELETE, Path: []string{"1"}, From: uint(2)}, + }, + nil, + }, { "int-slice-delete", []int{1, 2, 3}, []int{1, 3}, Changelog{ @@ -127,6 +148,14 @@ func TestDiff(t *testing.T) { }, nil, }, + { + "uint-slice-array-delete", [3]uint{1, 2, 3}, [3]uint{1, 3, 4}, + Changelog{ + Change{Type: DELETE, Path: []string{"1"}, From: uint(2)}, + Change{Type: CREATE, Path: []string{"2"}, To: uint(4)}, + }, + nil, + }, { "int-slice-insert-delete", []int{1, 2, 3}, []int{1, 3, 4}, Changelog{ @@ -142,6 +171,13 @@ func TestDiff(t *testing.T) { }, nil, }, + { + "string-array-insert", [3]string{"1", "2", "3"}, [4]string{"1", "2", "3", "4"}, + Changelog{ + Change{Type: CREATE, Path: []string{"3"}, To: "4"}, + }, + nil, + }, { "string-slice-delete", []string{"1", "2", "3"}, []string{"1", "3"}, Changelog{ @@ -149,6 +185,13 @@ func TestDiff(t *testing.T) { }, nil, }, + { + "string-slice-delete", [3]string{"1", "2", "3"}, [2]string{"1", "3"}, + Changelog{ + Change{Type: DELETE, Path: []string{"1"}, From: "2"}, + }, + nil, + }, { "string-slice-insert-delete", []string{"1", "2", "3"}, []string{"1", "3", "4"}, Changelog{ @@ -157,6 +200,14 @@ func TestDiff(t *testing.T) { }, nil, }, + { + "string-array-insert-delete", [3]string{"1", "2", "3"}, [3]string{"1", "3", "4"}, + Changelog{ + Change{Type: DELETE, Path: []string{"1"}, From: "2"}, + Change{Type: CREATE, Path: []string{"2"}, To: "4"}, + }, + nil, + }, { "comparable-slice-insert", []tistruct{{"one", 1}}, []tistruct{{"one", 1}, {"two", 2}}, Changelog{ @@ -165,6 +216,14 @@ func TestDiff(t *testing.T) { }, nil, }, + { + "comparable-array-insert", [1]tistruct{{"one", 1}}, [2]tistruct{{"one", 1}, {"two", 2}}, + Changelog{ + Change{Type: CREATE, Path: []string{"two", "name"}, To: "two"}, + Change{Type: CREATE, Path: []string{"two", "value"}, To: 2}, + }, + nil, + }, { "comparable-slice-delete", []tistruct{{"one", 1}, {"two", 2}}, []tistruct{{"one", 1}}, Changelog{ @@ -173,6 +232,14 @@ func TestDiff(t *testing.T) { }, nil, }, + { + "comparable-array-delete", [2]tistruct{{"one", 1}, {"two", 2}}, [1]tistruct{{"one", 1}}, + Changelog{ + Change{Type: DELETE, Path: []string{"two", "name"}, From: "two"}, + Change{Type: DELETE, Path: []string{"two", "value"}, From: 2}, + }, + nil, + }, { "comparable-slice-update", []tistruct{{"one", 1}}, []tistruct{{"one", 50}}, Changelog{ @@ -180,6 +247,13 @@ func TestDiff(t *testing.T) { }, nil, }, + { + "comparable-array-update", [1]tistruct{{"one", 1}}, [1]tistruct{{"one", 50}}, + Changelog{ + Change{Type: UPDATE, Path: []string{"one", "value"}, From: 1, To: 50}, + }, + nil, + }, { "map-slice-insert", []map[string]string{{"test": "123"}}, []map[string]string{{"test": "123", "tset": "456"}}, Changelog{ @@ -187,6 +261,13 @@ func TestDiff(t *testing.T) { }, nil, }, + { + "map-array-insert", [1]map[string]string{{"test": "123"}}, [1]map[string]string{{"test": "123", "tset": "456"}}, + Changelog{ + Change{Type: CREATE, Path: []string{"0", "tset"}, To: "456"}, + }, + nil, + }, { "map-slice-update", []map[string]string{{"test": "123"}}, []map[string]string{{"test": "456"}}, Changelog{ @@ -194,6 +275,13 @@ func TestDiff(t *testing.T) { }, nil, }, + { + "map-array-update", [1]map[string]string{{"test": "123"}}, [1]map[string]string{{"test": "456"}}, + Changelog{ + Change{Type: UPDATE, Path: []string{"0", "test"}, From: "123", To: "456"}, + }, + nil, + }, { "map-slice-delete", []map[string]string{{"test": "123", "tset": "456"}}, []map[string]string{{"test": "123"}}, Changelog{ @@ -201,6 +289,13 @@ func TestDiff(t *testing.T) { }, nil, }, + { + "map-array-delete", [1]map[string]string{{"test": "123", "tset": "456"}}, [1]map[string]string{{"test": "123"}}, + Changelog{ + Change{Type: DELETE, Path: []string{"0", "tset"}, From: "456"}, + }, + nil, + }, { "map-interface-slice-update", []map[string]interface{}{{"test": nil}}, []map[string]interface{}{{"test": "456"}}, Changelog{ @@ -208,6 +303,13 @@ func TestDiff(t *testing.T) { }, nil, }, + { + "map-interface-array-update", [1]map[string]interface{}{{"test": nil}}, [1]map[string]interface{}{{"test": "456"}}, + Changelog{ + Change{Type: UPDATE, Path: []string{"0", "test"}, From: nil, To: "456"}, + }, + nil, + }, { "map-nil", map[string]string{"one": "test"}, nil, Changelog{ @@ -257,6 +359,13 @@ func TestDiff(t *testing.T) { }, nil, }, + { + "nested-array-insert", map[string][3]int{"a": {1, 2, 3}}, map[string][4]int{"a": {1, 2, 3, 4}}, + Changelog{ + Change{Type: CREATE, Path: []string{"a", "3"}, To: 4}, + }, + nil, + }, { "nested-slice-update", map[string][]int{"a": {1, 2, 3}}, map[string][]int{"a": {1, 4, 3}}, Changelog{ @@ -264,6 +373,13 @@ func TestDiff(t *testing.T) { }, nil, }, + { + "nested-array-update", map[string][3]int{"a": {1, 2, 3}}, map[string][3]int{"a": {1, 4, 3}}, + Changelog{ + Change{Type: UPDATE, Path: []string{"a", "1"}, From: 2, To: 4}, + }, + nil, + }, { "nested-slice-delete", map[string][]int{"a": {1, 2, 3}}, map[string][]int{"a": {1, 3}}, Changelog{ @@ -271,6 +387,13 @@ func TestDiff(t *testing.T) { }, nil, }, + { + "nested-array-delete", map[string][3]int{"a": {1, 2, 3}}, map[string][2]int{"a": {1, 3}}, + Changelog{ + Change{Type: DELETE, Path: []string{"a", "1"}, From: 2, To: nil}, + }, + nil, + }, { "struct-string-update", tstruct{Name: "one"}, tstruct{Name: "two"}, Changelog{ From 0773ab31df74505044e9fee81ef5a6ba01dbfc58 Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Wed, 20 Oct 2021 15:03:44 +0100 Subject: [PATCH 40/56] Fix private map (#68) * move tests to their own package * fix private maps * access private struct interface values --- diff_comparative.go | 2 +- diff_examples_test.go | 63 +++--- diff_interface.go | 8 +- diff_map.go | 6 +- diff_struct.go | 4 +- diff_test.go | 501 ++++++++++++++++++++++-------------------- patch_test.go | 159 +++++++------- 7 files changed, 390 insertions(+), 353 deletions(-) diff --git a/diff_comparative.go b/diff_comparative.go index 878e28f..7359d17 100644 --- a/diff_comparative.go +++ b/diff_comparative.go @@ -14,8 +14,8 @@ func (d *Differ) diffComparative(path []string, c *ComparativeList, parent inter if d.StructMapKeys { id = idComplex(k) } - fpath := copyAppend(path, id) + fpath := copyAppend(path, id) nv := reflect.ValueOf(nil) if c.m[k].A == nil { diff --git a/diff_examples_test.go b/diff_examples_test.go index f89b321..2885eb7 100644 --- a/diff_examples_test.go +++ b/diff_examples_test.go @@ -1,9 +1,11 @@ -package diff +package diff_test import ( "fmt" "math/big" "reflect" + + "github.com/r3labs/diff/v2" ) //Try to do a bunch of stuff that will result in some or all failures @@ -68,20 +70,20 @@ func ExamplePatchWithErrors() { Name: "second", } - changelog, err := Diff(a, b) + changelog, err := diff.Diff(a, b) if err != nil { panic(err) } //This fails in total because c is not assignable (passed by Value) - patchLog := Patch(changelog, c) + patchLog := diff.Patch(changelog, c) //this also demonstrated the nested errors with 'next' - errors := patchLog[0].Errors.(*DiffError) + errors := patchLog[0].Errors.(*diff.DiffError) //we can also continue to nest errors if we like - message := errors.WithCause(NewError("This is a custom message")). + message := errors.WithCause(diff.NewError("This is a custom message")). WithCause(fmt.Errorf("this is an error from somewhere else but still compatible")). Error() @@ -89,17 +91,17 @@ func ExamplePatchWithErrors() { changelog[2].Path[1] = "bad index" changelog[3].Path[0] = "bad struct field" - patchLog = Patch(changelog, &c) + patchLog = diff.Patch(changelog, &c) - patchLog, _ = Merge(a, nil, &c) + patchLog, _ = diff.Merge(a, nil, &c) - patchLog, _ = Merge(a, d, &c) + patchLog, _ = diff.Merge(a, d, &c) //try patching a string - patchLog = Patch(changelog, message) + patchLog = diff.Patch(changelog, message) //test an invalid change Value - var bad *ChangeValue + var bad *diff.ChangeValue if bad.IsValid() { fmt.Print("this should never happen") } @@ -158,7 +160,7 @@ func ExampleMerge() { c.Labels["colors"] = 42 //the only error that can happen here comes from the diff step - patchLog, _ := Merge(a, b, &c) + patchLog, _ := diff.Merge(a, b, &c) //Note that unlike our patch version we've not included 'create' in the //tag for nutrients. This will omit "vitamin e" from ending up in c @@ -169,7 +171,6 @@ func ExampleMerge() { //ExamplePrimitiveSlice demonstrates working with arrays and primitive values func ExamplePrimitiveSlice() { - sla := []string{ "this", "is", @@ -189,11 +190,11 @@ func ExamplePrimitiveSlice() { "ok", } - patch, err := Diff(sla, slb, StructMapKeySupport()) + patch, err := diff.Diff(sla, slb, diff.StructMapKeySupport()) if err != nil { fmt.Print("failed to diff sla and slb") } - cl := Patch(patch, &slc) + cl := diff.Patch(patch, &slc) //now the other way, round sla = []string{ @@ -210,11 +211,11 @@ func ExamplePrimitiveSlice() { "simple", } - patch, err = Diff(sla, slb) + patch, err = diff.Diff(sla, slb) if err != nil { fmt.Print("failed to diff sla and slb") } - cl = Patch(patch, &slc) + cl = diff.Patch(patch, &slc) //and finally a clean view sla = []string{ @@ -226,11 +227,11 @@ func ExamplePrimitiveSlice() { } slb = []string{} - patch, err = Diff(sla, slb) + patch, err = diff.Diff(sla, slb) if err != nil { fmt.Print("failed to diff sla and slb") } - cl = Patch(patch, &slc) + cl = diff.Patch(patch, &slc) fmt.Printf("%d changes made to string array; %v", len(cl), slc) @@ -302,12 +303,12 @@ func ExampleComplexSlicePatch() { } c := Attributes{} - changelog, err := Diff(a, b, DiscardComplexOrigin(), StructMapKeySupport()) + changelog, err := diff.Diff(a, b, diff.DiscardComplexOrigin(), diff.StructMapKeySupport()) if err != nil { panic(err) } - patchLog := Patch(changelog, &c) + patchLog := diff.Patch(changelog, &c) fmt.Printf("Patched %d entries and encountered %d errors", len(patchLog), patchLog.ErrorCount()) @@ -367,12 +368,12 @@ func ExampleComplexMapPatch() { Number: 23.4453, } - changelog, err := Diff(a, b) + changelog, err := diff.Diff(a, b) if err != nil { panic(err) } - patchLog := Patch(changelog, &c) + patchLog := diff.Patch(changelog, &c) fmt.Printf("%#v", len(patchLog)) @@ -466,15 +467,15 @@ func ExamplePatch() { } d.Nutrients = append(d.Nutrients, "minerals") - changelog, err := Diff(a, b) + changelog, err := diff.Diff(a, b) if err != nil { panic(err) } - patchLog := Patch(changelog, &c) + patchLog := diff.Patch(changelog, &c) - changelog, _ = Diff(a, d) - patchLog = Patch(changelog, &c) + changelog, _ = diff.Diff(a, d) + patchLog = diff.Patch(changelog, &c) fmt.Printf("%#v", len(patchLog)) @@ -532,7 +533,7 @@ func ExampleDiff() { }, } - changelog, err := Diff(a, b) + changelog, err := diff.Diff(a, b) if err != nil { panic(err) } @@ -576,7 +577,7 @@ func ExampleFilter() { }, } - d, err := NewDiffer(Filter(func(path []string, parent reflect.Type, field reflect.StructField) bool { + d, err := diff.NewDiffer(diff.Filter(func(path []string, parent reflect.Type, field reflect.StructField) bool { return field.Name != "Name" })) if err != nil { @@ -589,7 +590,7 @@ func ExampleFilter() { } fmt.Printf("%#v", changelog) - // Output: diff.Changelog{diff.Change{Type:"update", Path:[]string{"id"}, From:1, To:2, parent:diff.Fruit{ID:1, Name:"Green Apple", Healthy:true, Nutrients:[]string{"vitamin c", "vitamin d"}, Tags:[]diff.Tag(nil)}}, diff.Change{Type:"create", Path:[]string{"nutrients", "2"}, From:interface {}(nil), To:"vitamin e", parent:interface {}(nil)}} + // Output: diff.Changelog{diff.Change{Type:"update", Path:[]string{"id"}, From:1, To:2, parent:diff_test.Fruit{ID:1, Name:"Green Apple", Healthy:true, Nutrients:[]string{"vitamin c", "vitamin d"}, Tags:[]diff_test.Tag(nil)}}, diff.Change{Type:"create", Path:[]string{"nutrients", "2"}, From:interface {}(nil), To:"vitamin e", parent:interface {}(nil)}} } func ExamplePrivatePtr() { @@ -600,11 +601,11 @@ func ExamplePrivatePtr() { a := number{} b := number{value: big.NewInt(111)} - changelog, err := Diff(a, b) + changelog, err := diff.Diff(a, b) if err != nil { panic(err) } fmt.Printf("%#v", changelog) - // Output: diff.Changelog{diff.Change{Type:"update", Path:[]string{"value"}, From:interface {}(nil), To:111, parent:diff.number{value:(*big.Int)(nil), exp:0}}} + // Output: diff.Changelog{diff.Change{Type:"update", Path:[]string{"value"}, From:interface {}(nil), To:111, parent:diff_test.number{value:(*big.Int)(nil), exp:0}}} } diff --git a/diff_interface.go b/diff_interface.go index 821a275..ef6cde8 100644 --- a/diff_interface.go +++ b/diff_interface.go @@ -8,12 +8,12 @@ import "reflect" func (d *Differ) diffInterface(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { - d.cl.Add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, exportInterface(b)) return nil } if b.Kind() == reflect.Invalid { - d.cl.Add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, exportInterface(a), nil) return nil } @@ -26,12 +26,12 @@ func (d *Differ) diffInterface(path []string, a, b reflect.Value, parent interfa } if a.IsNil() { - d.cl.Add(UPDATE, path, nil, b.Interface(), parent) + d.cl.Add(UPDATE, path, nil, exportInterface(b), parent) return nil } if b.IsNil() { - d.cl.Add(UPDATE, path, a.Interface(), nil, parent) + d.cl.Add(UPDATE, path, exportInterface(a), nil, parent) return nil } diff --git a/diff_map.go b/diff_map.go index 11a17f3..dcb8efb 100644 --- a/diff_map.go +++ b/diff_map.go @@ -24,15 +24,15 @@ func (d *Differ) diffMap(path []string, a, b reflect.Value) error { for _, k := range a.MapKeys() { ae := a.MapIndex(k) - c.addA(k.Interface(), &ae) + c.addA(exportInterface(k), &ae) } for _, k := range b.MapKeys() { be := b.MapIndex(k) - c.addB(k.Interface(), &be) + c.addB(exportInterface(k), &be) } - return d.diffComparative(path, c, a.Interface()) + return d.diffComparative(path, c, exportInterface(a)) } func (d *Differ) mapValues(t string, path []string, a reflect.Value) error { diff --git a/diff_struct.go b/diff_struct.go index e1dcb5c..a16e34c 100644 --- a/diff_struct.go +++ b/diff_struct.go @@ -16,7 +16,7 @@ func (d *Differ) diffStruct(path []string, a, b reflect.Value) error { if a.Kind() == reflect.Invalid { if d.DisableStructValues { - d.cl.Add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, exportInterface(b)) return nil } return d.structValues(CREATE, path, b) @@ -24,7 +24,7 @@ func (d *Differ) diffStruct(path []string, a, b reflect.Value) error { if b.Kind() == reflect.Invalid { if d.DisableStructValues { - d.cl.Add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, exportInterface(a), nil) return nil } return d.structValues(DELETE, path, a) diff --git a/diff_test.go b/diff_test.go index 6a9b76a..938393f 100644 --- a/diff_test.go +++ b/diff_test.go @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package diff +package diff_test import ( "reflect" @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/r3labs/diff/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -57,6 +58,10 @@ type privateValueStruct struct { Private *sync.RWMutex } +type privateMapStruct struct { + set map[string]interface{} +} + type CustomStringType string type CustomIntType int type customTypeStruct struct { @@ -88,439 +93,439 @@ func TestDiff(t *testing.T) { cases := []struct { Name string A, B interface{} - Changelog Changelog + Changelog diff.Changelog Error error }{ { "uint-slice-insert", []uint{1, 2, 3}, []uint{1, 2, 3, 4}, - Changelog{ - Change{Type: CREATE, Path: []string{"3"}, To: uint(4)}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: uint(4)}, }, nil, }, { "uint-array-insert", [3]uint{1, 2, 3}, [4]uint{1, 2, 3, 4}, - Changelog{ - Change{Type: CREATE, Path: []string{"3"}, To: uint(4)}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: uint(4)}, }, nil, }, { "int-slice-insert", []int{1, 2, 3}, []int{1, 2, 3, 4}, - Changelog{ - Change{Type: CREATE, Path: []string{"3"}, To: 4}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: 4}, }, nil, }, { "int-array-insert", [3]int{1, 2, 3}, [4]int{1, 2, 3, 4}, - Changelog{ - Change{Type: CREATE, Path: []string{"3"}, To: 4}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: 4}, }, nil, }, { "uint-slice-delete", []uint{1, 2, 3}, []uint{1, 3}, - Changelog{ - Change{Type: DELETE, Path: []string{"1"}, From: uint(2)}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: uint(2)}, }, nil, }, { "uint-array-delete", [3]uint{1, 2, 3}, [2]uint{1, 3}, - Changelog{ - Change{Type: DELETE, Path: []string{"1"}, From: uint(2)}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: uint(2)}, }, nil, }, { "int-slice-delete", []int{1, 2, 3}, []int{1, 3}, - Changelog{ - Change{Type: DELETE, Path: []string{"1"}, From: 2}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: 2}, }, nil, }, { "uint-slice-insert-delete", []uint{1, 2, 3}, []uint{1, 3, 4}, - Changelog{ - Change{Type: DELETE, Path: []string{"1"}, From: uint(2)}, - Change{Type: CREATE, Path: []string{"2"}, To: uint(4)}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: uint(2)}, + diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: uint(4)}, }, nil, }, { "uint-slice-array-delete", [3]uint{1, 2, 3}, [3]uint{1, 3, 4}, - Changelog{ - Change{Type: DELETE, Path: []string{"1"}, From: uint(2)}, - Change{Type: CREATE, Path: []string{"2"}, To: uint(4)}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: uint(2)}, + diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: uint(4)}, }, nil, }, { "int-slice-insert-delete", []int{1, 2, 3}, []int{1, 3, 4}, - Changelog{ - Change{Type: DELETE, Path: []string{"1"}, From: 2}, - Change{Type: CREATE, Path: []string{"2"}, To: 4}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: 2}, + diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: 4}, }, nil, }, { "string-slice-insert", []string{"1", "2", "3"}, []string{"1", "2", "3", "4"}, - Changelog{ - Change{Type: CREATE, Path: []string{"3"}, To: "4"}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: "4"}, }, nil, }, { "string-array-insert", [3]string{"1", "2", "3"}, [4]string{"1", "2", "3", "4"}, - Changelog{ - Change{Type: CREATE, Path: []string{"3"}, To: "4"}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: "4"}, }, nil, }, { "string-slice-delete", []string{"1", "2", "3"}, []string{"1", "3"}, - Changelog{ - Change{Type: DELETE, Path: []string{"1"}, From: "2"}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"}, }, nil, }, { "string-slice-delete", [3]string{"1", "2", "3"}, [2]string{"1", "3"}, - Changelog{ - Change{Type: DELETE, Path: []string{"1"}, From: "2"}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"}, }, nil, }, { "string-slice-insert-delete", []string{"1", "2", "3"}, []string{"1", "3", "4"}, - Changelog{ - Change{Type: DELETE, Path: []string{"1"}, From: "2"}, - Change{Type: CREATE, Path: []string{"2"}, To: "4"}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"}, + diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: "4"}, }, nil, }, { "string-array-insert-delete", [3]string{"1", "2", "3"}, [3]string{"1", "3", "4"}, - Changelog{ - Change{Type: DELETE, Path: []string{"1"}, From: "2"}, - Change{Type: CREATE, Path: []string{"2"}, To: "4"}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"}, + diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: "4"}, }, nil, }, { "comparable-slice-insert", []tistruct{{"one", 1}}, []tistruct{{"one", 1}, {"two", 2}}, - Changelog{ - Change{Type: CREATE, Path: []string{"two", "name"}, To: "two"}, - Change{Type: CREATE, Path: []string{"two", "value"}, To: 2}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"two", "name"}, To: "two"}, + diff.Change{Type: diff.CREATE, Path: []string{"two", "value"}, To: 2}, }, nil, }, { "comparable-array-insert", [1]tistruct{{"one", 1}}, [2]tistruct{{"one", 1}, {"two", 2}}, - Changelog{ - Change{Type: CREATE, Path: []string{"two", "name"}, To: "two"}, - Change{Type: CREATE, Path: []string{"two", "value"}, To: 2}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"two", "name"}, To: "two"}, + diff.Change{Type: diff.CREATE, Path: []string{"two", "value"}, To: 2}, }, nil, }, { "comparable-slice-delete", []tistruct{{"one", 1}, {"two", 2}}, []tistruct{{"one", 1}}, - Changelog{ - Change{Type: DELETE, Path: []string{"two", "name"}, From: "two"}, - Change{Type: DELETE, Path: []string{"two", "value"}, From: 2}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"two", "name"}, From: "two"}, + diff.Change{Type: diff.DELETE, Path: []string{"two", "value"}, From: 2}, }, nil, }, { "comparable-array-delete", [2]tistruct{{"one", 1}, {"two", 2}}, [1]tistruct{{"one", 1}}, - Changelog{ - Change{Type: DELETE, Path: []string{"two", "name"}, From: "two"}, - Change{Type: DELETE, Path: []string{"two", "value"}, From: 2}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"two", "name"}, From: "two"}, + diff.Change{Type: diff.DELETE, Path: []string{"two", "value"}, From: 2}, }, nil, }, { "comparable-slice-update", []tistruct{{"one", 1}}, []tistruct{{"one", 50}}, - Changelog{ - Change{Type: UPDATE, Path: []string{"one", "value"}, From: 1, To: 50}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"one", "value"}, From: 1, To: 50}, }, nil, }, { "comparable-array-update", [1]tistruct{{"one", 1}}, [1]tistruct{{"one", 50}}, - Changelog{ - Change{Type: UPDATE, Path: []string{"one", "value"}, From: 1, To: 50}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"one", "value"}, From: 1, To: 50}, }, nil, }, { "map-slice-insert", []map[string]string{{"test": "123"}}, []map[string]string{{"test": "123", "tset": "456"}}, - Changelog{ - Change{Type: CREATE, Path: []string{"0", "tset"}, To: "456"}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"0", "tset"}, To: "456"}, }, nil, }, { "map-array-insert", [1]map[string]string{{"test": "123"}}, [1]map[string]string{{"test": "123", "tset": "456"}}, - Changelog{ - Change{Type: CREATE, Path: []string{"0", "tset"}, To: "456"}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"0", "tset"}, To: "456"}, }, nil, }, { "map-slice-update", []map[string]string{{"test": "123"}}, []map[string]string{{"test": "456"}}, - Changelog{ - Change{Type: UPDATE, Path: []string{"0", "test"}, From: "123", To: "456"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"0", "test"}, From: "123", To: "456"}, }, nil, }, { "map-array-update", [1]map[string]string{{"test": "123"}}, [1]map[string]string{{"test": "456"}}, - Changelog{ - Change{Type: UPDATE, Path: []string{"0", "test"}, From: "123", To: "456"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"0", "test"}, From: "123", To: "456"}, }, nil, }, { "map-slice-delete", []map[string]string{{"test": "123", "tset": "456"}}, []map[string]string{{"test": "123"}}, - Changelog{ - Change{Type: DELETE, Path: []string{"0", "tset"}, From: "456"}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"0", "tset"}, From: "456"}, }, nil, }, { "map-array-delete", [1]map[string]string{{"test": "123", "tset": "456"}}, [1]map[string]string{{"test": "123"}}, - Changelog{ - Change{Type: DELETE, Path: []string{"0", "tset"}, From: "456"}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"0", "tset"}, From: "456"}, }, nil, }, { "map-interface-slice-update", []map[string]interface{}{{"test": nil}}, []map[string]interface{}{{"test": "456"}}, - Changelog{ - Change{Type: UPDATE, Path: []string{"0", "test"}, From: nil, To: "456"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"0", "test"}, From: nil, To: "456"}, }, nil, }, { "map-interface-array-update", [1]map[string]interface{}{{"test": nil}}, [1]map[string]interface{}{{"test": "456"}}, - Changelog{ - Change{Type: UPDATE, Path: []string{"0", "test"}, From: nil, To: "456"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"0", "test"}, From: nil, To: "456"}, }, nil, }, { "map-nil", map[string]string{"one": "test"}, nil, - Changelog{ - Change{Type: DELETE, Path: []string{"\xa3one"}, From: "test", To: nil}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"\xa3one"}, From: "test", To: nil}, }, nil, }, { "nil-map", nil, map[string]string{"one": "test"}, - Changelog{ - Change{Type: CREATE, Path: []string{"\xa3one"}, From: nil, To: "test"}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"\xa3one"}, From: nil, To: "test"}, }, nil, }, { "nested-map-insert", map[string]map[string]string{"a": {"test": "123"}}, map[string]map[string]string{"a": {"test": "123", "tset": "456"}}, - Changelog{ - Change{Type: CREATE, Path: []string{"a", "tset"}, To: "456"}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"a", "tset"}, To: "456"}, }, nil, }, { "nested-map-interface-insert", map[string]map[string]interface{}{"a": {"test": "123"}}, map[string]map[string]interface{}{"a": {"test": "123", "tset": "456"}}, - Changelog{ - Change{Type: CREATE, Path: []string{"a", "tset"}, To: "456"}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"a", "tset"}, To: "456"}, }, nil, }, { "nested-map-update", map[string]map[string]string{"a": {"test": "123"}}, map[string]map[string]string{"a": {"test": "456"}}, - Changelog{ - Change{Type: UPDATE, Path: []string{"a", "test"}, From: "123", To: "456"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"a", "test"}, From: "123", To: "456"}, }, nil, }, { "nested-map-delete", map[string]map[string]string{"a": {"test": "123"}}, map[string]map[string]string{"a": {}}, - Changelog{ - Change{Type: DELETE, Path: []string{"a", "test"}, From: "123", To: nil}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"a", "test"}, From: "123", To: nil}, }, nil, }, { "nested-slice-insert", map[string][]int{"a": {1, 2, 3}}, map[string][]int{"a": {1, 2, 3, 4}}, - Changelog{ - Change{Type: CREATE, Path: []string{"a", "3"}, To: 4}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"a", "3"}, To: 4}, }, nil, }, { "nested-array-insert", map[string][3]int{"a": {1, 2, 3}}, map[string][4]int{"a": {1, 2, 3, 4}}, - Changelog{ - Change{Type: CREATE, Path: []string{"a", "3"}, To: 4}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"a", "3"}, To: 4}, }, nil, }, { "nested-slice-update", map[string][]int{"a": {1, 2, 3}}, map[string][]int{"a": {1, 4, 3}}, - Changelog{ - Change{Type: UPDATE, Path: []string{"a", "1"}, From: 2, To: 4}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"a", "1"}, From: 2, To: 4}, }, nil, }, { "nested-array-update", map[string][3]int{"a": {1, 2, 3}}, map[string][3]int{"a": {1, 4, 3}}, - Changelog{ - Change{Type: UPDATE, Path: []string{"a", "1"}, From: 2, To: 4}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"a", "1"}, From: 2, To: 4}, }, nil, }, { "nested-slice-delete", map[string][]int{"a": {1, 2, 3}}, map[string][]int{"a": {1, 3}}, - Changelog{ - Change{Type: DELETE, Path: []string{"a", "1"}, From: 2, To: nil}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"a", "1"}, From: 2, To: nil}, }, nil, }, { "nested-array-delete", map[string][3]int{"a": {1, 2, 3}}, map[string][2]int{"a": {1, 3}}, - Changelog{ - Change{Type: DELETE, Path: []string{"a", "1"}, From: 2, To: nil}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"a", "1"}, From: 2, To: nil}, }, nil, }, { "struct-string-update", tstruct{Name: "one"}, tstruct{Name: "two"}, - Changelog{ - Change{Type: UPDATE, Path: []string{"name"}, From: "one", To: "two"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"name"}, From: "one", To: "two"}, }, nil, }, { "struct-int-update", tstruct{Value: 1}, tstruct{Value: 50}, - Changelog{ - Change{Type: UPDATE, Path: []string{"value"}, From: 1, To: 50}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"value"}, From: 1, To: 50}, }, nil, }, { "struct-bool-update", tstruct{Bool: true}, tstruct{Bool: false}, - Changelog{ - Change{Type: UPDATE, Path: []string{"bool"}, From: true, To: false}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"bool"}, From: true, To: false}, }, nil, }, { "struct-time-update", tstruct{}, tstruct{Time: currentTime}, - Changelog{ - Change{Type: UPDATE, Path: []string{"time"}, From: time.Time{}, To: currentTime}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"time"}, From: time.Time{}, To: currentTime}, }, nil, }, { "struct-map-update", tstruct{Map: map[string]string{"test": "123"}}, tstruct{Map: map[string]string{"test": "456"}}, - Changelog{ - Change{Type: UPDATE, Path: []string{"map", "test"}, From: "123", To: "456"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"map", "test"}, From: "123", To: "456"}, }, nil, }, { "struct-string-pointer-update", tstruct{Pointer: sptr("test")}, tstruct{Pointer: sptr("test2")}, - Changelog{ - Change{Type: UPDATE, Path: []string{"pointer"}, From: "test", To: "test2"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"pointer"}, From: "test", To: "test2"}, }, nil, }, { "struct-nil-string-pointer-update", tstruct{Pointer: nil}, tstruct{Pointer: sptr("test")}, - Changelog{ - Change{Type: UPDATE, Path: []string{"pointer"}, From: nil, To: sptr("test")}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"pointer"}, From: nil, To: sptr("test")}, }, nil, }, { "struct-generic-slice-insert", tstruct{Values: []string{"one"}}, tstruct{Values: []string{"one", "two"}}, - Changelog{ - Change{Type: CREATE, Path: []string{"values", "1"}, From: nil, To: "two"}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"values", "1"}, From: nil, To: "two"}, }, nil, }, { "struct-identifiable-slice-insert", tstruct{Identifiables: []tistruct{{"one", 1}}}, tstruct{Identifiables: []tistruct{{"one", 1}, {"two", 2}}}, - Changelog{ - Change{Type: CREATE, Path: []string{"identifiables", "two", "name"}, From: nil, To: "two"}, - Change{Type: CREATE, Path: []string{"identifiables", "two", "value"}, From: nil, To: 2}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"identifiables", "two", "name"}, From: nil, To: "two"}, + diff.Change{Type: diff.CREATE, Path: []string{"identifiables", "two", "value"}, From: nil, To: 2}, }, nil, }, { "struct-generic-slice-delete", tstruct{Values: []string{"one", "two"}}, tstruct{Values: []string{"one"}}, - Changelog{ - Change{Type: DELETE, Path: []string{"values", "1"}, From: "two", To: nil}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"values", "1"}, From: "two", To: nil}, }, nil, }, { "struct-identifiable-slice-delete", tstruct{Identifiables: []tistruct{{"one", 1}, {"two", 2}}}, tstruct{Identifiables: []tistruct{{"one", 1}}}, - Changelog{ - Change{Type: DELETE, Path: []string{"identifiables", "two", "name"}, From: "two", To: nil}, - Change{Type: DELETE, Path: []string{"identifiables", "two", "value"}, From: 2, To: nil}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"identifiables", "two", "name"}, From: "two", To: nil}, + diff.Change{Type: diff.DELETE, Path: []string{"identifiables", "two", "value"}, From: 2, To: nil}, }, nil, }, { "struct-unidentifiable-slice-insert-delete", tstruct{Unidentifiables: []tuistruct{{1}, {2}, {3}}}, tstruct{Unidentifiables: []tuistruct{{5}, {2}, {3}, {4}}}, - Changelog{ - Change{Type: UPDATE, Path: []string{"unidentifiables", "0", "value"}, From: 1, To: 5}, - Change{Type: CREATE, Path: []string{"unidentifiables", "3", "value"}, From: nil, To: 4}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"unidentifiables", "0", "value"}, From: 1, To: 5}, + diff.Change{Type: diff.CREATE, Path: []string{"unidentifiables", "3", "value"}, From: nil, To: 4}, }, nil, }, { "struct-with-private-value", privateValueStruct{Public: "one", Private: new(sync.RWMutex)}, privateValueStruct{Public: "two", Private: new(sync.RWMutex)}, - Changelog{ - Change{Type: UPDATE, Path: []string{"Public"}, From: "one", To: "two"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"Public"}, From: "one", To: "two"}, }, nil, }, { "mismatched-values-struct-map", map[string]string{"test": "one"}, &tstruct{Identifiables: []tistruct{{"one", 1}}}, - Changelog{}, - ErrTypeMismatch, + diff.Changelog{}, + diff.ErrTypeMismatch, }, { "omittable", tstruct{Ignored: false}, tstruct{Ignored: true}, - Changelog{}, + diff.Changelog{}, nil, }, { "slice", &tstruct{}, &tstruct{Nested: tnstruct{Slice: []tmstruct{{"one", 1}, {"two", 2}}}}, - Changelog{ - Change{Type: CREATE, Path: []string{"nested", "slice", "0", "foo"}, From: nil, To: "one"}, - Change{Type: CREATE, Path: []string{"nested", "slice", "0", "bar"}, From: nil, To: 1}, - Change{Type: CREATE, Path: []string{"nested", "slice", "1", "foo"}, From: nil, To: "two"}, - Change{Type: CREATE, Path: []string{"nested", "slice", "1", "bar"}, From: nil, To: 2}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "0", "foo"}, From: nil, To: "one"}, + diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "0", "bar"}, From: nil, To: 1}, + diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "1", "foo"}, From: nil, To: "two"}, + diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "1", "bar"}, From: nil, To: 2}, }, nil, }, { "slice-duplicate-items", []int{1}, []int{1, 1}, - Changelog{ - Change{Type: CREATE, Path: []string{"1"}, From: nil, To: 1}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"1"}, From: nil, To: 1}, }, nil, }, { "mixed-slice-map", []map[string]interface{}{{"name": "name1", "type": []string{"null", "string"}}}, []map[string]interface{}{{"name": "name1", "type": []string{"null", "int"}}, {"name": "name2", "type": []string{"null", "string"}}}, - Changelog{ - Change{Type: UPDATE, Path: []string{"0", "type", "1"}, From: "string", To: "int"}, - Change{Type: CREATE, Path: []string{"1", "\xa4name"}, From: nil, To: "name2"}, - Change{Type: CREATE, Path: []string{"1", "\xa4type"}, From: nil, To: []string{"null", "string"}}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"0", "type", "1"}, From: "string", To: "int"}, + diff.Change{Type: diff.CREATE, Path: []string{"1", "\xa4name"}, From: nil, To: "name2"}, + diff.Change{Type: diff.CREATE, Path: []string{"1", "\xa4type"}, From: nil, To: []string{"null", "string"}}, }, nil, }, @@ -528,9 +533,9 @@ func TestDiff(t *testing.T) { "map-string-pointer-create", map[string]*tmstruct{"one": &struct1}, map[string]*tmstruct{"one": &struct1, "two": &struct2}, - Changelog{ - Change{Type: CREATE, Path: []string{"two", "foo"}, From: nil, To: "two"}, - Change{Type: CREATE, Path: []string{"two", "bar"}, From: nil, To: 2}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"two", "foo"}, From: nil, To: "two"}, + diff.Change{Type: diff.CREATE, Path: []string{"two", "bar"}, From: nil, To: 2}, }, nil, }, @@ -538,9 +543,9 @@ func TestDiff(t *testing.T) { "map-string-pointer-delete", map[string]*tmstruct{"one": &struct1, "two": &struct2}, map[string]*tmstruct{"one": &struct1}, - Changelog{ - Change{Type: DELETE, Path: []string{"two", "foo"}, From: "two", To: nil}, - Change{Type: DELETE, Path: []string{"two", "bar"}, From: 2, To: nil}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"two", "foo"}, From: "two", To: nil}, + diff.Change{Type: diff.DELETE, Path: []string{"two", "bar"}, From: 2, To: nil}, }, nil, }, @@ -548,8 +553,8 @@ func TestDiff(t *testing.T) { "private-struct-field", tstruct{private: 1}, tstruct{private: 4}, - Changelog{ - Change{Type: UPDATE, Path: []string{"private"}, From: int64(1), To: int64(4)}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"private"}, From: int64(1), To: int64(4)}, }, nil, }, @@ -557,10 +562,10 @@ func TestDiff(t *testing.T) { "embedded-struct-field", embedstruct{Embedded{Foo: "a", Bar: 2}, true}, embedstruct{Embedded{Foo: "b", Bar: 3}, false}, - Changelog{ - Change{Type: UPDATE, Path: []string{"foo"}, From: "a", To: "b"}, - Change{Type: UPDATE, Path: []string{"bar"}, From: 2, To: 3}, - Change{Type: UPDATE, Path: []string{"baz"}, From: true, To: false}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: "a", To: "b"}, + diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: 2, To: 3}, + diff.Change{Type: diff.UPDATE, Path: []string{"baz"}, From: true, To: false}, }, nil, }, @@ -568,9 +573,9 @@ func TestDiff(t *testing.T) { "custom-tags", customTagStruct{Foo: "abc", Bar: 3}, customTagStruct{Foo: "def", Bar: 4}, - Changelog{ - Change{Type: UPDATE, Path: []string{"foo"}, From: "abc", To: "def"}, - Change{Type: UPDATE, Path: []string{"bar"}, From: 3, To: 4}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: "abc", To: "def"}, + diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: 3, To: 4}, }, nil, }, @@ -578,9 +583,30 @@ func TestDiff(t *testing.T) { "custom-types", customTypeStruct{Foo: "a", Bar: 1}, customTypeStruct{Foo: "b", Bar: 2}, - Changelog{ - Change{Type: UPDATE, Path: []string{"foo"}, From: CustomStringType("a"), To: CustomStringType("b")}, - Change{Type: UPDATE, Path: []string{"bar"}, From: CustomIntType(1), To: CustomIntType(2)}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: CustomStringType("a"), To: CustomStringType("b")}, + diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: CustomIntType(1), To: CustomIntType(2)}, + }, + nil, + }, + { + "struct-private-map-create", privateMapStruct{set: map[string]interface{}{"1": struct{}{}, "2": struct{}{}}}, privateMapStruct{set: map[string]interface{}{"1": struct{}{}, "2": struct{}{}, "3": struct{}{}}}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"set", "3"}, From: nil, To: struct{}{}}, + }, + nil, + }, + { + "struct-private-map-delete", privateMapStruct{set: map[string]interface{}{"1": struct{}{}, "2": struct{}{}, "3": struct{}{}}}, privateMapStruct{set: map[string]interface{}{"1": struct{}{}, "2": struct{}{}}}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"set", "3"}, From: struct{}{}, To: nil}, + }, + nil, + }, + { + "struct-private-map-nil-values", privateMapStruct{set: map[string]interface{}{"1": nil, "2": nil}}, privateMapStruct{set: map[string]interface{}{"1": nil, "2": nil, "3": nil}}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"set", "3"}, From: nil, To: nil}, }, nil, }, @@ -589,16 +615,16 @@ func TestDiff(t *testing.T) { for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { - var options []func(d *Differ) error + var options []func(d *diff.Differ) error switch tc.Name { case "mixed-slice-map", "nil-map", "map-nil": - options = append(options, StructMapKeySupport()) + options = append(options, diff.StructMapKeySupport()) case "embedded-struct-field": - options = append(options, FlattenEmbeddedStructs()) + options = append(options, diff.FlattenEmbeddedStructs()) case "custom-tags": - options = append(options, TagName("json")) + options = append(options, diff.TagName("json")) } - cl, err := Diff(tc.A, tc.B, options...) + cl, err := diff.Diff(tc.A, tc.B, options...) assert.Equal(t, tc.Error, err) require.Equal(t, len(tc.Changelog), len(cl)) @@ -617,70 +643,70 @@ func TestDiffSliceOrdering(t *testing.T) { cases := []struct { Name string A, B interface{} - Changelog Changelog + Changelog diff.Changelog Error error }{ { "int-slice-insert-in-middle", []int{1, 2, 4}, []int{1, 2, 3, 4}, - Changelog{ - Change{Type: UPDATE, Path: []string{"2"}, From: 4, To: 3}, - Change{Type: CREATE, Path: []string{"3"}, To: 4}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"2"}, From: 4, To: 3}, + diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: 4}, }, nil, }, { "int-slice-delete", []int{1, 2, 3}, []int{1, 3}, - Changelog{ - Change{Type: UPDATE, Path: []string{"1"}, From: 2, To: 3}, - Change{Type: DELETE, Path: []string{"2"}, From: 3}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"1"}, From: 2, To: 3}, + diff.Change{Type: diff.DELETE, Path: []string{"2"}, From: 3}, }, nil, }, { "int-slice-insert-delete", []int{1, 2, 3}, []int{1, 3, 4}, - Changelog{ - Change{Type: UPDATE, Path: []string{"1"}, From: 2, To: 3}, - Change{Type: UPDATE, Path: []string{"2"}, From: 3, To: 4}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"1"}, From: 2, To: 3}, + diff.Change{Type: diff.UPDATE, Path: []string{"2"}, From: 3, To: 4}, }, nil, }, { "int-slice-reorder", []int{1, 2, 3}, []int{1, 3, 2}, - Changelog{ - Change{Type: UPDATE, Path: []string{"1"}, From: 2, To: 3}, - Change{Type: UPDATE, Path: []string{"2"}, From: 3, To: 2}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"1"}, From: 2, To: 3}, + diff.Change{Type: diff.UPDATE, Path: []string{"2"}, From: 3, To: 2}, }, nil, }, { "string-slice-delete", []string{"1", "2", "3"}, []string{"1", "3"}, - Changelog{ - Change{Type: UPDATE, Path: []string{"1"}, From: "2", To: "3"}, - Change{Type: DELETE, Path: []string{"2"}, From: "3"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"1"}, From: "2", To: "3"}, + diff.Change{Type: diff.DELETE, Path: []string{"2"}, From: "3"}, }, nil, }, { "string-slice-insert-delete", []string{"1", "2", "3"}, []string{"1", "3", "4"}, - Changelog{ - Change{Type: UPDATE, Path: []string{"1"}, From: "2", To: "3"}, - Change{Type: UPDATE, Path: []string{"2"}, From: "3", To: "4"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"1"}, From: "2", To: "3"}, + diff.Change{Type: diff.UPDATE, Path: []string{"2"}, From: "3", To: "4"}, }, nil, }, { "string-slice-reorder", []string{"1", "2", "3"}, []string{"1", "3", "2"}, - Changelog{ - Change{Type: UPDATE, Path: []string{"1"}, From: "2", To: "3"}, - Change{Type: UPDATE, Path: []string{"2"}, From: "3", To: "2"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"1"}, From: "2", To: "3"}, + diff.Change{Type: diff.UPDATE, Path: []string{"2"}, From: "3", To: "2"}, }, nil, }, { "nested-slice-delete", map[string][]int{"a": {1, 2, 3}}, map[string][]int{"a": {1, 3}}, - Changelog{ - Change{Type: UPDATE, Path: []string{"a", "1"}, From: 2, To: 3}, - Change{Type: DELETE, Path: []string{"a", "2"}, From: 3}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"a", "1"}, From: 2, To: 3}, + diff.Change{Type: diff.DELETE, Path: []string{"a", "2"}, From: 3}, }, nil, }, @@ -688,7 +714,7 @@ func TestDiffSliceOrdering(t *testing.T) { for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { - d, err := NewDiffer(SliceOrdering(true)) + d, err := diff.NewDiffer(diff.SliceOrdering(true)) require.Nil(t, err) cl, err := d.Diff(tc.A, tc.B) @@ -716,7 +742,7 @@ func TestFilter(t *testing.T) { {"regex", []string{"item-*"}, [][]string{{"item-1", "subitem"}, {"item-2", "subitem"}}}, } - cl := Changelog{ + cl := diff.Changelog{ {Path: []string{"item-1", "subitem"}}, {Path: []string{"item-2", "subitem"}}, } @@ -737,30 +763,30 @@ func TestStructValues(t *testing.T) { Name string ChangeType string X interface{} - Changelog Changelog + Changelog diff.Changelog Error error }{ { - "struct-create", CREATE, tstruct{ID: "xxxxx", Name: "something", Value: 1, Values: []string{"one", "two", "three"}}, - Changelog{ - Change{Type: CREATE, Path: []string{"id"}, From: nil, To: "xxxxx"}, - Change{Type: CREATE, Path: []string{"name"}, From: nil, To: "something"}, - Change{Type: CREATE, Path: []string{"value"}, From: nil, To: 1}, - Change{Type: CREATE, Path: []string{"values", "0"}, From: nil, To: "one"}, - Change{Type: CREATE, Path: []string{"values", "1"}, From: nil, To: "two"}, - Change{Type: CREATE, Path: []string{"values", "2"}, From: nil, To: "three"}, + "struct-create", diff.CREATE, tstruct{ID: "xxxxx", Name: "something", Value: 1, Values: []string{"one", "two", "three"}}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"id"}, From: nil, To: "xxxxx"}, + diff.Change{Type: diff.CREATE, Path: []string{"name"}, From: nil, To: "something"}, + diff.Change{Type: diff.CREATE, Path: []string{"value"}, From: nil, To: 1}, + diff.Change{Type: diff.CREATE, Path: []string{"values", "0"}, From: nil, To: "one"}, + diff.Change{Type: diff.CREATE, Path: []string{"values", "1"}, From: nil, To: "two"}, + diff.Change{Type: diff.CREATE, Path: []string{"values", "2"}, From: nil, To: "three"}, }, nil, }, { - "struct-delete", DELETE, tstruct{ID: "xxxxx", Name: "something", Value: 1, Values: []string{"one", "two", "three"}}, - Changelog{ - Change{Type: DELETE, Path: []string{"id"}, From: "xxxxx", To: nil}, - Change{Type: DELETE, Path: []string{"name"}, From: "something", To: nil}, - Change{Type: DELETE, Path: []string{"value"}, From: 1, To: nil}, - Change{Type: DELETE, Path: []string{"values", "0"}, From: "one", To: nil}, - Change{Type: DELETE, Path: []string{"values", "1"}, From: "two", To: nil}, - Change{Type: DELETE, Path: []string{"values", "2"}, From: "three", To: nil}, + "struct-delete", diff.DELETE, tstruct{ID: "xxxxx", Name: "something", Value: 1, Values: []string{"one", "two", "three"}}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"id"}, From: "xxxxx", To: nil}, + diff.Change{Type: diff.DELETE, Path: []string{"name"}, From: "something", To: nil}, + diff.Change{Type: diff.DELETE, Path: []string{"value"}, From: 1, To: nil}, + diff.Change{Type: diff.DELETE, Path: []string{"values", "0"}, From: "one", To: nil}, + diff.Change{Type: diff.DELETE, Path: []string{"values", "1"}, From: "two", To: nil}, + diff.Change{Type: diff.DELETE, Path: []string{"values", "2"}, From: "three", To: nil}, }, nil, }, @@ -768,7 +794,7 @@ func TestStructValues(t *testing.T) { for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { - cl, err := StructValues(tc.ChangeType, []string{}, tc.X) + cl, err := diff.StructValues(tc.ChangeType, []string{}, tc.X) assert.Equal(t, tc.Error, err) assert.Equal(t, len(tc.Changelog), len(cl)) @@ -784,7 +810,7 @@ func TestStructValues(t *testing.T) { } func TestDifferReuse(t *testing.T) { - d, err := NewDiffer() + d, err := diff.NewDiffer() require.Nil(t, err) cl, err := d.Diff([]string{"1", "2", "3"}, []string{"1"}) @@ -807,7 +833,7 @@ func TestDifferReuse(t *testing.T) { } func TestDiffingOptions(t *testing.T) { - d, err := NewDiffer(SliceOrdering(false)) + d, err := diff.NewDiffer(diff.SliceOrdering(false)) require.Nil(t, err) assert.False(t, d.SliceOrdering) @@ -817,7 +843,7 @@ func TestDiffingOptions(t *testing.T) { assert.Len(t, cl, 0) - d, err = NewDiffer(SliceOrdering(true)) + d, err = diff.NewDiffer(diff.SliceOrdering(true)) require.Nil(t, err) assert.True(t, d.SliceOrdering) @@ -831,7 +857,7 @@ func TestDiffingOptions(t *testing.T) { } func TestDiffPrivateField(t *testing.T) { - cl, err := Diff(tstruct{private: 1}, tstruct{private: 3}) + cl, err := diff.Diff(tstruct{private: 1}, tstruct{private: 3}) require.Nil(t, err) assert.Len(t, cl, 1) } @@ -846,11 +872,11 @@ func (o *testTypeDiffer) InsertParentDiffer(dfunc func(path []string, a, b refle } func (o *testTypeDiffer) Match(a, b reflect.Value) bool { - return AreType(a, b, reflect.TypeOf(testType(""))) + return diff.AreType(a, b, reflect.TypeOf(testType(""))) } -func (o *testTypeDiffer) Diff(cl *Changelog, path []string, a, b reflect.Value) error { +func (o *testTypeDiffer) Diff(cl *diff.Changelog, path []string, a, b reflect.Value) error { if a.String() != "custom" && b.String() != "match" { - cl.Add(UPDATE, path, a.Interface(), b.Interface()) + cl.Add(diff.UPDATE, path, a.Interface(), b.Interface()) } return nil } @@ -860,8 +886,8 @@ func TestCustomDiffer(t *testing.T) { T testType } - d, err := NewDiffer( - CustomValueDiffers( + d, err := diff.NewDiffer( + diff.CustomValueDiffers( &testTypeDiffer{}, ), ) @@ -872,8 +898,8 @@ func TestCustomDiffer(t *testing.T) { assert.Len(t, cl, 0) - d, err = NewDiffer( - CustomValueDiffers( + d, err = diff.NewDiffer( + diff.CustomValueDiffers( &testTypeDiffer{}, ), ) @@ -899,23 +925,23 @@ func (o *recursiveTestStructDiffer) InsertParentDiffer(dfunc func(path []string, } func (o *recursiveTestStructDiffer) Match(a, b reflect.Value) bool { - return AreType(a, b, reflect.TypeOf(RecursiveTestStruct{})) + return diff.AreType(a, b, reflect.TypeOf(RecursiveTestStruct{})) } -func (o *recursiveTestStructDiffer) Diff(cl *Changelog, path []string, a, b reflect.Value) error { +func (o *recursiveTestStructDiffer) Diff(cl *diff.Changelog, path []string, a, b reflect.Value) error { if a.Kind() == reflect.Invalid { - cl.Add(CREATE, path, nil, b.Interface()) + cl.Add(diff.CREATE, path, nil, b.Interface()) return nil } if b.Kind() == reflect.Invalid { - cl.Add(DELETE, path, a.Interface(), nil) + cl.Add(diff.DELETE, path, a.Interface(), nil) return nil } var awt, bwt RecursiveTestStruct awt, _ = a.Interface().(RecursiveTestStruct) bwt, _ = b.Interface().(RecursiveTestStruct) if awt.Id != bwt.Id { - cl.Add(UPDATE, path, a.Interface(), b.Interface()) + cl.Add(diff.UPDATE, path, a.Interface(), b.Interface()) } for i := 0; i < a.NumField(); i++ { field := a.Type().Field(i) @@ -949,8 +975,8 @@ func TestRecursiveCustomDiffer(t *testing.T) { }, }, } - d, err := NewDiffer( - CustomValueDiffers( + d, err := diff.NewDiffer( + diff.CustomValueDiffers( &recursiveTestStructDiffer{}, ), ) @@ -964,7 +990,7 @@ func TestHandleDifferentTypes(t *testing.T) { cases := []struct { Name string A, B interface{} - Changelog Changelog + Changelog diff.Changelog Error error HandleTypeMismatch bool }{ @@ -972,7 +998,7 @@ func TestHandleDifferentTypes(t *testing.T) { "type-change-not-allowed-error", 1, "1", nil, - ErrTypeMismatch, + diff.ErrTypeMismatch, false, }, { @@ -986,14 +1012,14 @@ func TestHandleDifferentTypes(t *testing.T) { p2 string }{"1", "1"}, nil, - ErrTypeMismatch, + diff.ErrTypeMismatch, false, }, { "type-change-allowed", 1, "1", - Changelog{ - Change{Type: UPDATE, Path: []string{}, From: 1, To: "1"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{}, From: 1, To: "1"}, }, nil, true, @@ -1010,9 +1036,9 @@ func TestHandleDifferentTypes(t *testing.T) { P2 string P3 string }{"1", "1", "1"}, - Changelog{ - Change{Type: UPDATE, Path: []string{"P2"}, From: 1, To: "1"}, - Change{Type: UPDATE, Path: []string{"P3"}, From: map[string]string{"1": "1"}, To: "1"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"P2"}, From: 1, To: "1"}, + diff.Change{Type: diff.UPDATE, Path: []string{"P3"}, From: map[string]string{"1": "1"}, To: "1"}, }, nil, true, @@ -1021,7 +1047,7 @@ func TestHandleDifferentTypes(t *testing.T) { for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { - d, err := NewDiffer(AllowTypeMismatch(tc.HandleTypeMismatch)) + d, err := diff.NewDiffer(diff.AllowTypeMismatch(tc.HandleTypeMismatch)) require.Nil(t, err) cl, err := d.Diff(tc.A, tc.B) @@ -1037,3 +1063,12 @@ func TestHandleDifferentTypes(t *testing.T) { }) } } + +func copyAppend(src []string, elems ...string) []string { + dst := make([]string, len(src)+len(elems)) + copy(dst, src) + for i := len(src); i < len(src)+len(elems); i++ { + dst[i] = elems[i-len(src)] + } + return dst +} diff --git a/patch_test.go b/patch_test.go index 3e8753b..80ca1d1 100644 --- a/patch_test.go +++ b/patch_test.go @@ -1,9 +1,10 @@ -package diff +package diff_test import ( "testing" "time" + "github.com/r3labs/diff/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -12,153 +13,153 @@ func TestPatch(t *testing.T) { cases := []struct { Name string A, B interface{} - Changelog Changelog + Changelog diff.Changelog Error error }{ { "uint-slice-insert", &[]uint{1, 2, 3}, &[]uint{1, 2, 3, 4}, - Changelog{ - Change{Type: CREATE, Path: []string{"3"}, To: uint(4)}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: uint(4)}, }, nil, }, { "int-slice-insert", &[]int{1, 2, 3}, &[]int{1, 2, 3, 4}, - Changelog{ - Change{Type: CREATE, Path: []string{"3"}, To: 4}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: 4}, }, nil, }, { "uint-slice-delete", &[]uint{1, 2, 3}, &[]uint{1, 3}, - Changelog{ - Change{Type: DELETE, Path: []string{"1"}, From: uint(2)}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: uint(2)}, }, nil, }, { "int-slice-delete", &[]int{1, 2, 3}, &[]int{1, 3}, - Changelog{ - Change{Type: DELETE, Path: []string{"1"}, From: 2}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: 2}, }, nil, }, { "uint-slice-insert-delete", &[]uint{1, 2, 3}, &[]uint{1, 3, 4}, - Changelog{ - Change{Type: DELETE, Path: []string{"1"}, From: uint(2)}, - Change{Type: CREATE, Path: []string{"2"}, To: uint(4)}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: uint(2)}, + diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: uint(4)}, }, nil, }, { "int-slice-insert-delete", &[]int{1, 2, 3}, &[]int{1, 3, 4}, - Changelog{ - Change{Type: DELETE, Path: []string{"1"}, From: 2}, - Change{Type: CREATE, Path: []string{"2"}, To: 4}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: 2}, + diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: 4}, }, nil, }, { "string-slice-insert", &[]string{"1", "2", "3"}, &[]string{"1", "2", "3", "4"}, - Changelog{ - Change{Type: CREATE, Path: []string{"3"}, To: "4"}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: "4"}, }, nil, }, { "string-slice-delete", &[]string{"1", "2", "3"}, &[]string{"1", "3"}, - Changelog{ - Change{Type: DELETE, Path: []string{"1"}, From: "2"}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"}, }, nil, }, { "string-slice-insert-delete", &[]string{"1", "2", "3"}, &[]string{"1", "3", "4"}, - Changelog{ - Change{Type: DELETE, Path: []string{"1"}, From: "2"}, - Change{Type: CREATE, Path: []string{"2"}, To: "4"}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"}, + diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: "4"}, }, nil, }, { "comparable-slice-update", &[]tistruct{{"one", 1}}, &[]tistruct{{"one", 50}}, - Changelog{ - Change{Type: UPDATE, Path: []string{"one", "value"}, From: 1, To: 50}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"one", "value"}, From: 1, To: 50}, }, nil, }, { "struct-string-update", &tstruct{Name: "one"}, &tstruct{Name: "two"}, - Changelog{ - Change{Type: UPDATE, Path: []string{"name"}, From: "one", To: "two"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"name"}, From: "one", To: "two"}, }, nil, }, { "struct-int-update", &tstruct{Value: 1}, &tstruct{Value: 50}, - Changelog{ - Change{Type: UPDATE, Path: []string{"value"}, From: 1, To: 50}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"value"}, From: 1, To: 50}, }, nil, }, { "struct-bool-update", &tstruct{Bool: true}, &tstruct{Bool: false}, - Changelog{ - Change{Type: UPDATE, Path: []string{"bool"}, From: true, To: false}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"bool"}, From: true, To: false}, }, nil, }, { "struct-time-update", &tstruct{}, &tstruct{Time: currentTime}, - Changelog{ - Change{Type: UPDATE, Path: []string{"time"}, From: time.Time{}, To: currentTime}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"time"}, From: time.Time{}, To: currentTime}, }, nil, }, { "struct-nil-string-pointer-update", &tstruct{Pointer: nil}, &tstruct{Pointer: sptr("test")}, - Changelog{ - Change{Type: UPDATE, Path: []string{"pointer"}, From: nil, To: sptr("test")}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"pointer"}, From: nil, To: sptr("test")}, }, nil, }, { "struct-generic-slice-insert", &tstruct{Values: []string{"one"}}, &tstruct{Values: []string{"one", "two"}}, - Changelog{ - Change{Type: CREATE, Path: []string{"values", "1"}, From: nil, To: "two"}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"values", "1"}, From: nil, To: "two"}, }, nil, }, { "struct-generic-slice-delete", &tstruct{Values: []string{"one", "two"}}, &tstruct{Values: []string{"one"}}, - Changelog{ - Change{Type: DELETE, Path: []string{"values", "1"}, From: "two", To: nil}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"values", "1"}, From: "two", To: nil}, }, nil, }, { "struct-unidentifiable-slice-insert-delete", &tstruct{Unidentifiables: []tuistruct{{1}, {2}, {3}}}, &tstruct{Unidentifiables: []tuistruct{{5}, {2}, {3}, {4}}}, - Changelog{ - Change{Type: UPDATE, Path: []string{"unidentifiables", "0", "value"}, From: 1, To: 5}, - Change{Type: CREATE, Path: []string{"unidentifiables", "3", "value"}, From: nil, To: 4}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"unidentifiables", "0", "value"}, From: 1, To: 5}, + diff.Change{Type: diff.CREATE, Path: []string{"unidentifiables", "3", "value"}, From: nil, To: 4}, }, nil, }, { "slice", &tstruct{}, &tstruct{Nested: tnstruct{Slice: []tmstruct{{"one", 1}, {"two", 2}}}}, - Changelog{ - Change{Type: CREATE, Path: []string{"nested", "slice", "0", "foo"}, From: nil, To: "one"}, - Change{Type: CREATE, Path: []string{"nested", "slice", "0", "bar"}, From: nil, To: 1}, - Change{Type: CREATE, Path: []string{"nested", "slice", "1", "foo"}, From: nil, To: "two"}, - Change{Type: CREATE, Path: []string{"nested", "slice", "1", "bar"}, From: nil, To: 2}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "0", "foo"}, From: nil, To: "one"}, + diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "0", "bar"}, From: nil, To: 1}, + diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "1", "foo"}, From: nil, To: "two"}, + diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "1", "bar"}, From: nil, To: 2}, }, nil, }, { "slice-duplicate-items", &[]int{1}, &[]int{1, 1}, - Changelog{ - Change{Type: CREATE, Path: []string{"1"}, From: nil, To: 1}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"1"}, From: nil, To: 1}, }, nil, }, @@ -166,10 +167,10 @@ func TestPatch(t *testing.T) { "embedded-struct-field", &embedstruct{Embedded{Foo: "a", Bar: 2}, true}, &embedstruct{Embedded{Foo: "b", Bar: 3}, false}, - Changelog{ - Change{Type: UPDATE, Path: []string{"foo"}, From: "a", To: "b"}, - Change{Type: UPDATE, Path: []string{"bar"}, From: 2, To: 3}, - Change{Type: UPDATE, Path: []string{"baz"}, From: true, To: false}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: "a", To: "b"}, + diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: 2, To: 3}, + diff.Change{Type: diff.UPDATE, Path: []string{"baz"}, From: true, To: false}, }, nil, }, @@ -177,9 +178,9 @@ func TestPatch(t *testing.T) { "custom-tags", &customTagStruct{Foo: "abc", Bar: 3}, &customTagStruct{Foo: "def", Bar: 4}, - Changelog{ - Change{Type: UPDATE, Path: []string{"foo"}, From: "abc", To: "def"}, - Change{Type: UPDATE, Path: []string{"bar"}, From: 3, To: 4}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: "abc", To: "def"}, + diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: 3, To: 4}, }, nil, }, @@ -187,9 +188,9 @@ func TestPatch(t *testing.T) { "custom-types", &customTypeStruct{Foo: "a", Bar: 1}, &customTypeStruct{Foo: "b", Bar: 2}, - Changelog{ - Change{Type: UPDATE, Path: []string{"foo"}, From: CustomStringType("a"), To: CustomStringType("b")}, - Change{Type: UPDATE, Path: []string{"bar"}, From: CustomIntType(1), To: CustomIntType(2)}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: CustomStringType("a"), To: CustomStringType("b")}, + diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: CustomIntType(1), To: CustomIntType(2)}, }, nil, }, @@ -197,10 +198,10 @@ func TestPatch(t *testing.T) { "map", map[string]interface{}{"1": "one", "3": "three"}, map[string]interface{}{"2": "two", "3": "tres"}, - Changelog{ - Change{Type: DELETE, Path: []string{"1"}, From: "one", To: nil}, - Change{Type: CREATE, Path: []string{"2"}, From: nil, To: "two"}, - Change{Type: UPDATE, Path: []string{"3"}, From: "three", To: "tres"}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "one", To: nil}, + diff.Change{Type: diff.CREATE, Path: []string{"2"}, From: nil, To: "two"}, + diff.Change{Type: diff.UPDATE, Path: []string{"3"}, From: "three", To: "tres"}, }, nil, }, @@ -208,8 +209,8 @@ func TestPatch(t *testing.T) { "map-nested-create", map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active", "attributes": map[string]interface{}{"attrA": "A", "attrB": "B"}}}, map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active", "attributes": map[string]interface{}{"attrA": "A", "attrB": "B"}, "secondary-attributes": map[string]interface{}{"attrA": "A", "attrB": "B"}}}, - Changelog{ - Change{Type: "create", Path: []string{"details", "secondary-attributes"}, From: nil, To: map[string]interface{}{"attrA": "A", "attrB": "B"}}, + diff.Changelog{ + diff.Change{Type: "create", Path: []string{"details", "secondary-attributes"}, From: nil, To: map[string]interface{}{"attrA": "A", "attrB": "B"}}, }, nil, }, @@ -217,8 +218,8 @@ func TestPatch(t *testing.T) { "map-nested-update", map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active", "attributes": map[string]interface{}{"attrA": "A", "attrB": "B"}}}, map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active", "attributes": map[string]interface{}{"attrA": "C", "attrD": "X"}}}, - Changelog{ - Change{Type: "update", Path: []string{"details", "attributes"}, From: map[string]interface{}{"attrA": "A", "attrB": "B"}, To: map[string]interface{}{"attrA": "C", "attrD": "X"}}, + diff.Changelog{ + diff.Change{Type: "update", Path: []string{"details", "attributes"}, From: map[string]interface{}{"attrA": "A", "attrB": "B"}, To: map[string]interface{}{"attrA": "C", "attrD": "X"}}, }, nil, }, @@ -226,8 +227,8 @@ func TestPatch(t *testing.T) { "map-nested-delete", map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active", "attributes": map[string]interface{}{"attrA": "A", "attrB": "B"}}}, map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active"}}, - Changelog{ - Change{Type: "delete", Path: []string{"details", "attributes"}, From: map[string]interface{}{"attrA": "A", "attrB": "B"}, To: nil}, + diff.Changelog{ + diff.Change{Type: "delete", Path: []string{"details", "attributes"}, From: map[string]interface{}{"attrA": "A", "attrB": "B"}, To: nil}, }, nil, }, @@ -236,16 +237,16 @@ func TestPatch(t *testing.T) { for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { - var options []func(d *Differ) error + var options []func(d *diff.Differ) error switch tc.Name { case "mixed-slice-map", "nil-map", "map-nil": - options = append(options, StructMapKeySupport()) + options = append(options, diff.StructMapKeySupport()) case "embedded-struct-field": - options = append(options, FlattenEmbeddedStructs()) + options = append(options, diff.FlattenEmbeddedStructs()) case "custom-tags": - options = append(options, TagName("json")) + options = append(options, diff.TagName("json")) } - d, err := NewDiffer(options...) + d, err := diff.NewDiffer(options...) if err != nil { panic(err) } @@ -259,12 +260,12 @@ func TestPatch(t *testing.T) { t.Run("convert-types", func(t *testing.T) { a := &tmstruct{Foo: "a", Bar: 1} b := &customTypeStruct{Foo: "b", Bar: 2} - cl := Changelog{ - Change{Type: UPDATE, Path: []string{"foo"}, From: CustomStringType("a"), To: CustomStringType("b")}, - Change{Type: UPDATE, Path: []string{"bar"}, From: CustomIntType(1), To: CustomIntType(2)}, + cl := diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: CustomStringType("a"), To: CustomStringType("b")}, + diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: CustomIntType(1), To: CustomIntType(2)}, } - d, err := NewDiffer() + d, err := diff.NewDiffer() if err != nil { panic(err) } @@ -272,7 +273,7 @@ func TestPatch(t *testing.T) { assert.True(t, pl.HasErrors()) - d, err = NewDiffer(ConvertCompatibleTypes()) + d, err = diff.NewDiffer(diff.ConvertCompatibleTypes()) if err != nil { panic(err) } From ad087c2f2aa935ba7783657e1bdbaca739b8d66b Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Tue, 23 Nov 2021 14:46:02 +0000 Subject: [PATCH 41/56] safely interface reflect values (#70) --- diff_bool.go | 6 +++--- diff_float.go | 6 +++--- diff_int.go | 6 +++--- diff_slice.go | 12 ++++++------ diff_string.go | 6 +++--- diff_struct.go | 4 ++-- diff_time.go | 10 +++++----- diff_uint.go | 6 +++--- 8 files changed, 28 insertions(+), 28 deletions(-) diff --git a/diff_bool.go b/diff_bool.go index 2ab2a70..0e30503 100644 --- a/diff_bool.go +++ b/diff_bool.go @@ -8,12 +8,12 @@ import "reflect" func (d *Differ) diffBool(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { - d.cl.Add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, exportInterface(b)) return nil } if b.Kind() == reflect.Invalid { - d.cl.Add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, exportInterface(a), nil) return nil } @@ -22,7 +22,7 @@ func (d *Differ) diffBool(path []string, a, b reflect.Value, parent interface{}) } if a.Bool() != b.Bool() { - d.cl.Add(UPDATE, path, a.Interface(), b.Interface(), parent) + d.cl.Add(UPDATE, path, exportInterface(a), exportInterface(b), parent) } return nil diff --git a/diff_float.go b/diff_float.go index 5fcf2e0..9494365 100644 --- a/diff_float.go +++ b/diff_float.go @@ -10,12 +10,12 @@ import ( func (d *Differ) diffFloat(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { - d.cl.Add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, exportInterface(b)) return nil } if b.Kind() == reflect.Invalid { - d.cl.Add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, exportInterface(a), nil) return nil } @@ -25,7 +25,7 @@ func (d *Differ) diffFloat(path []string, a, b reflect.Value, parent interface{} if a.Float() != b.Float() { if a.CanInterface() { - d.cl.Add(UPDATE, path, a.Interface(), b.Interface(), parent) + d.cl.Add(UPDATE, path, exportInterface(a), exportInterface(b), parent) } else { d.cl.Add(UPDATE, path, a.Float(), b.Float(), parent) } diff --git a/diff_int.go b/diff_int.go index 77f7e64..3658bf7 100644 --- a/diff_int.go +++ b/diff_int.go @@ -10,12 +10,12 @@ import ( func (d *Differ) diffInt(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { - d.cl.Add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, exportInterface(b)) return nil } if b.Kind() == reflect.Invalid { - d.cl.Add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, exportInterface(a), nil) return nil } @@ -25,7 +25,7 @@ func (d *Differ) diffInt(path []string, a, b reflect.Value, parent interface{}) if a.Int() != b.Int() { if a.CanInterface() { - d.cl.Add(UPDATE, path, a.Interface(), b.Interface(), parent) + d.cl.Add(UPDATE, path, exportInterface(a), exportInterface(b), parent) } else { d.cl.Add(UPDATE, path, a.Int(), b.Int(), parent) } diff --git a/diff_slice.go b/diff_slice.go index dff1fae..5441bc7 100644 --- a/diff_slice.go +++ b/diff_slice.go @@ -10,12 +10,12 @@ import ( func (d *Differ) diffSlice(path []string, a, b reflect.Value) error { if a.Kind() == reflect.Invalid { - d.cl.Add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, exportInterface(b)) return nil } if b.Kind() == reflect.Invalid { - d.cl.Add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, exportInterface(a), nil) return nil } @@ -56,7 +56,7 @@ func (d *Differ) diffSliceGeneric(path []string, a, b reflect.Value) error { return nil } - return d.diffComparative(path, missing, a.Interface()) + return d.diffComparative(path, missing, exportInterface(a)) } func (d *Differ) diffSliceComparative(path []string, a, b reflect.Value) error { @@ -82,7 +82,7 @@ func (d *Differ) diffSliceComparative(path []string, a, b reflect.Value) error { } } - return d.diffComparative(path, c, a.Interface()) + return d.diffComparative(path, c, exportInterface(a)) } // keeps track of elements that have already been matched, to stop duplicate matches from occurring @@ -100,7 +100,7 @@ func (st *sliceTracker) has(s, v reflect.Value) bool { } x := s.Index(i) - if reflect.DeepEqual(x.Interface(), v.Interface()) { + if reflect.DeepEqual(exportInterface(x), exportInterface(v)) { (*st)[i] = true return true } @@ -124,7 +124,7 @@ func hasAtSameIndex(s, v reflect.Value, atIndex int) bool { // check the element in the slice at atIndex to see if it matches Value, if it is a valid index into the slice if atIndex < s.Len() { x := s.Index(atIndex) - return reflect.DeepEqual(x.Interface(), v.Interface()) + return reflect.DeepEqual(exportInterface(x), exportInterface(v)) } return false diff --git a/diff_string.go b/diff_string.go index abf28ce..74182e2 100644 --- a/diff_string.go +++ b/diff_string.go @@ -8,12 +8,12 @@ import "reflect" func (d *Differ) diffString(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { - d.cl.Add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, exportInterface(b)) return nil } if b.Kind() == reflect.Invalid { - d.cl.Add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, exportInterface(a), nil) return nil } @@ -24,7 +24,7 @@ func (d *Differ) diffString(path []string, a, b reflect.Value, parent interface{ if a.String() != b.String() { if a.CanInterface() { // If a and/or b is of a type that is an alias for String, store that type in changelog - d.cl.Add(UPDATE, path, a.Interface(), b.Interface(), parent) + d.cl.Add(UPDATE, path, exportInterface(a), exportInterface(b), parent) } else { d.cl.Add(UPDATE, path, a.String(), b.String(), parent) } diff --git a/diff_struct.go b/diff_struct.go index a16e34c..bc9a496 100644 --- a/diff_struct.go +++ b/diff_struct.go @@ -59,7 +59,7 @@ func (d *Differ) diffStruct(path []string, a, b reflect.Value) error { continue } - err := d.diff(fpath, af, bf, a.Interface()) + err := d.diff(fpath, af, bf, exportInterface(a)) if err != nil { return err } @@ -108,7 +108,7 @@ func (d *Differ) structValues(t string, path []string, a reflect.Value) error { continue } - err := nd.diff(fpath, xf, af, a.Interface()) + err := nd.diff(fpath, xf, af, exportInterface(a)) if err != nil { return err } diff --git a/diff_time.go b/diff_time.go index 4f583ff..4275e4a 100644 --- a/diff_time.go +++ b/diff_time.go @@ -11,12 +11,12 @@ import ( func (d *Differ) diffTime(path []string, a, b reflect.Value) error { if a.Kind() == reflect.Invalid { - d.cl.Add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, exportInterface(b)) return nil } if b.Kind() == reflect.Invalid { - d.cl.Add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, exportInterface(a), nil) return nil } @@ -25,11 +25,11 @@ func (d *Differ) diffTime(path []string, a, b reflect.Value) error { } // Marshal and unmarshal time type will lose accuracy. Using unix nano to compare time type. - au := a.Interface().(time.Time).UnixNano() - bu := b.Interface().(time.Time).UnixNano() + au := exportInterface(a).(time.Time).UnixNano() + bu := exportInterface(b).(time.Time).UnixNano() if au != bu { - d.cl.Add(UPDATE, path, a.Interface(), b.Interface()) + d.cl.Add(UPDATE, path, exportInterface(a), exportInterface(b)) } return nil diff --git a/diff_uint.go b/diff_uint.go index 5244b27..fbe133f 100644 --- a/diff_uint.go +++ b/diff_uint.go @@ -10,12 +10,12 @@ import ( func (d *Differ) diffUint(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { - d.cl.Add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, exportInterface(b)) return nil } if b.Kind() == reflect.Invalid { - d.cl.Add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, exportInterface(a), nil) return nil } @@ -25,7 +25,7 @@ func (d *Differ) diffUint(path []string, a, b reflect.Value, parent interface{}) if a.Uint() != b.Uint() { if a.CanInterface() { - d.cl.Add(UPDATE, path, a.Interface(), b.Interface(), parent) + d.cl.Add(UPDATE, path, exportInterface(a), exportInterface(b), parent) } else { d.cl.Add(UPDATE, path, a.Uint(), b.Uint(), parent) } From 314e50977c9e7ab4e02f0041c2a5c141efa2c76c Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Tue, 14 Dec 2021 23:37:00 +0000 Subject: [PATCH 42/56] fix patch assignment to pointer values --- change_value.go | 10 +++++++++- patch_test.go | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/change_value.go b/change_value.go index 2f8df91..156b6aa 100644 --- a/change_value.go +++ b/change_value.go @@ -108,8 +108,10 @@ func (c *ChangeValue) Set(value reflect.Value, convertCompatibleTypes bool) { if r := recover(); r != nil { c.AddError(NewError(r.(string))) c.SetFlag(FlagFailed) + } }() + if c.HasFlag(OptionImmutable) { c.SetFlag(FlagIgnored) return @@ -124,7 +126,13 @@ func (c *ChangeValue) Set(value reflect.Value, convertCompatibleTypes bool) { c.target.Set(value.Convert(c.target.Type())) } else { if value.IsValid() { - c.target.Set(value) + if c.target.Kind() == reflect.Ptr && value.Kind() != reflect.Ptr { + tv := reflect.New(value.Type()) + tv.Elem().Set(value) + c.target.Set(tv) + } else { + c.target.Set(value) + } } else if !c.target.IsZero() { t := c.target.Elem() t.Set(reflect.Zero(t.Type())) diff --git a/patch_test.go b/patch_test.go index 80ca1d1..63fd69a 100644 --- a/patch_test.go +++ b/patch_test.go @@ -285,3 +285,21 @@ func TestPatch(t *testing.T) { require.Equal(t, len(cl), len(pl)) }) } + +func TestPatchPointer(t *testing.T) { + type tps struct { + S *string + } + + str1 := "before" + str2 := "after" + + t1 := tps{S: &str1} + t2 := tps{S: &str2} + + changelog, err := diff.Diff(t1, t2) + assert.NoError(t, err) + + patchLog := diff.Patch(changelog, &t1) + assert.False(t, patchLog.HasErrors()) +} From be3229dfd12a79f742330d6a5d1e760fbf230807 Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Thu, 16 Dec 2021 12:09:57 +0000 Subject: [PATCH 43/56] fix pointers when convert types is enabled --- change_value.go | 25 ++++++++++++++++++++----- patch_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/change_value.go b/change_value.go index 156b6aa..529554b 100644 --- a/change_value.go +++ b/change_value.go @@ -118,12 +118,27 @@ func (c *ChangeValue) Set(value reflect.Value, convertCompatibleTypes bool) { } if convertCompatibleTypes { - if !value.Type().ConvertibleTo(c.target.Type()) { - c.AddError(fmt.Errorf("Value of type %s is not convertible to %s", value.Type().String(), c.target.Type().String())) - c.SetFlag(FlagFailed) - return + if c.target.Kind() == reflect.Ptr && value.Kind() != reflect.Ptr { + if !value.Type().ConvertibleTo(c.target.Elem().Type()) { + c.AddError(fmt.Errorf("Value of type %s is not convertible to %s", value.Type().String(), c.target.Type().String())) + c.SetFlag(FlagFailed) + return + } + + fmt.Println(c.target.Elem().Type()) + + tv := reflect.New(c.target.Elem().Type()) + tv.Elem().Set(value.Convert(c.target.Elem().Type())) + c.target.Set(tv) + } else { + if !value.Type().ConvertibleTo(c.target.Type()) { + c.AddError(fmt.Errorf("Value of type %s is not convertible to %s", value.Type().String(), c.target.Type().String())) + c.SetFlag(FlagFailed) + return + } + + c.target.Set(value.Convert(c.target.Type())) } - c.target.Set(value.Convert(c.target.Type())) } else { if value.IsValid() { if c.target.Kind() == reflect.Ptr && value.Kind() != reflect.Ptr { diff --git a/patch_test.go b/patch_test.go index 63fd69a..bb69e7a 100644 --- a/patch_test.go +++ b/patch_test.go @@ -1,6 +1,7 @@ package diff_test import ( + "encoding/json" "testing" "time" @@ -303,3 +304,32 @@ func TestPatchPointer(t *testing.T) { patchLog := diff.Patch(changelog, &t1) assert.False(t, patchLog.HasErrors()) } + +func TestPatchPointerConvertTypes(t *testing.T) { + type tps struct { + S *int + } + + val1 := 1 + val2 := 2 + + t1 := tps{S: &val1} + t2 := tps{S: &val2} + + changelog, err := diff.Diff(t1, t2) + assert.NoError(t, err) + + js, err := json.Marshal(changelog) + assert.NoError(t, err) + + assert.NoError(t, json.Unmarshal(js, &changelog)) + + d, err := diff.NewDiffer(diff.ConvertCompatibleTypes()) + assert.NoError(t, err) + + assert.Equal(t, 1, *t1.S) + + patchLog := d.Patch(changelog, &t1) + assert.False(t, patchLog.HasErrors()) + assert.Equal(t, 2, *t1.S) +} From b723d66c6638e5a1c85808132766124f2ccb4933 Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Mon, 20 Dec 2021 15:29:25 +0000 Subject: [PATCH 44/56] fix patching nil values --- change_value.go | 89 +++++++++++++++++++++++++++---------------------- patch_test.go | 76 +++++++++++++++++++++++------------------ 2 files changed, 92 insertions(+), 73 deletions(-) diff --git a/change_value.go b/change_value.go index 529554b..f115da9 100644 --- a/change_value.go +++ b/change_value.go @@ -103,58 +103,67 @@ func (c ChangeValue) Len() int { //Set echos reflect set func (c *ChangeValue) Set(value reflect.Value, convertCompatibleTypes bool) { - if c != nil { - defer func() { - if r := recover(); r != nil { - c.AddError(NewError(r.(string))) - c.SetFlag(FlagFailed) + if c == nil { + return + } + defer func() { + if r := recover(); r != nil { + switch e := r.(type) { + case string: + c.AddError(NewError(e)) + case *reflect.ValueError: + c.AddError(NewError(e.Error())) } - }() - if c.HasFlag(OptionImmutable) { - c.SetFlag(FlagIgnored) - return + c.SetFlag(FlagFailed) } + }() - if convertCompatibleTypes { - if c.target.Kind() == reflect.Ptr && value.Kind() != reflect.Ptr { - if !value.Type().ConvertibleTo(c.target.Elem().Type()) { - c.AddError(fmt.Errorf("Value of type %s is not convertible to %s", value.Type().String(), c.target.Type().String())) - c.SetFlag(FlagFailed) - return - } + if c.HasFlag(OptionImmutable) { + c.SetFlag(FlagIgnored) + return + } - fmt.Println(c.target.Elem().Type()) + if convertCompatibleTypes { + if c.target.Kind() == reflect.Ptr && value.Kind() != reflect.Ptr { + if !value.IsValid() { + c.target.Set(reflect.Zero(c.target.Type())) + c.SetFlag(FlagApplied) + return + } else if !value.Type().ConvertibleTo(c.target.Elem().Type()) { + c.AddError(fmt.Errorf("Value of type %s is not convertible to %s", value.Type().String(), c.target.Type().String())) + c.SetFlag(FlagFailed) + return + } + + tv := reflect.New(c.target.Elem().Type()) + tv.Elem().Set(value.Convert(c.target.Elem().Type())) + c.target.Set(tv) + } else { + if !value.Type().ConvertibleTo(c.target.Type()) { + c.AddError(fmt.Errorf("Value of type %s is not convertible to %s", value.Type().String(), c.target.Type().String())) + c.SetFlag(FlagFailed) + return + } - tv := reflect.New(c.target.Elem().Type()) - tv.Elem().Set(value.Convert(c.target.Elem().Type())) + c.target.Set(value.Convert(c.target.Type())) + } + } else { + if value.IsValid() { + if c.target.Kind() == reflect.Ptr && value.Kind() != reflect.Ptr { + tv := reflect.New(value.Type()) + tv.Elem().Set(value) c.target.Set(tv) } else { - if !value.Type().ConvertibleTo(c.target.Type()) { - c.AddError(fmt.Errorf("Value of type %s is not convertible to %s", value.Type().String(), c.target.Type().String())) - c.SetFlag(FlagFailed) - return - } - - c.target.Set(value.Convert(c.target.Type())) - } - } else { - if value.IsValid() { - if c.target.Kind() == reflect.Ptr && value.Kind() != reflect.Ptr { - tv := reflect.New(value.Type()) - tv.Elem().Set(value) - c.target.Set(tv) - } else { - c.target.Set(value) - } - } else if !c.target.IsZero() { - t := c.target.Elem() - t.Set(reflect.Zero(t.Type())) + c.target.Set(value) } + } else if !c.target.IsZero() { + t := c.target.Elem() + t.Set(reflect.Zero(t.Type())) } - c.SetFlag(FlagApplied) } + c.SetFlag(FlagApplied) } //Index echo for index diff --git a/patch_test.go b/patch_test.go index bb69e7a..4f6b116 100644 --- a/patch_test.go +++ b/patch_test.go @@ -285,51 +285,61 @@ func TestPatch(t *testing.T) { assert.Equal(t, int(b.Bar), a.Bar) require.Equal(t, len(cl), len(pl)) }) -} -func TestPatchPointer(t *testing.T) { - type tps struct { - S *string - } + t.Run("pointer", func(t *testing.T) { + type tps struct { + S *string + } - str1 := "before" - str2 := "after" + str1 := "before" + str2 := "after" - t1 := tps{S: &str1} - t2 := tps{S: &str2} + t1 := tps{S: &str1} + t2 := tps{S: &str2} - changelog, err := diff.Diff(t1, t2) - assert.NoError(t, err) + changelog, err := diff.Diff(t1, t2) + assert.NoError(t, err) - patchLog := diff.Patch(changelog, &t1) - assert.False(t, patchLog.HasErrors()) -} + patchLog := diff.Patch(changelog, &t1) + assert.False(t, patchLog.HasErrors()) + }) -func TestPatchPointerConvertTypes(t *testing.T) { - type tps struct { - S *int - } + t.Run("pointer-with-converted-type", func(t *testing.T) { + type tps struct { + S *int + } + + val1 := 1 + val2 := 2 + + t1 := tps{S: &val1} + t2 := tps{S: &val2} - val1 := 1 - val2 := 2 + changelog, err := diff.Diff(t1, t2) + assert.NoError(t, err) - t1 := tps{S: &val1} - t2 := tps{S: &val2} + js, err := json.Marshal(changelog) + assert.NoError(t, err) - changelog, err := diff.Diff(t1, t2) - assert.NoError(t, err) + assert.NoError(t, json.Unmarshal(js, &changelog)) - js, err := json.Marshal(changelog) - assert.NoError(t, err) + d, err := diff.NewDiffer(diff.ConvertCompatibleTypes()) + assert.NoError(t, err) - assert.NoError(t, json.Unmarshal(js, &changelog)) + assert.Equal(t, 1, *t1.S) - d, err := diff.NewDiffer(diff.ConvertCompatibleTypes()) - assert.NoError(t, err) + patchLog := d.Patch(changelog, &t1) + assert.False(t, patchLog.HasErrors()) + assert.Equal(t, 2, *t1.S) - assert.Equal(t, 1, *t1.S) + // test nil pointer + t1 = tps{S: &val1} + t2 = tps{S: nil} - patchLog := d.Patch(changelog, &t1) - assert.False(t, patchLog.HasErrors()) - assert.Equal(t, 2, *t1.S) + changelog, err = diff.Diff(t1, t2) + assert.NoError(t, err) + + patchLog = d.Patch(changelog, &t1) + assert.False(t, patchLog.HasErrors()) + }) } From b53a548a2c33484d301791cc33b78c5c4f67f2af Mon Sep 17 00:00:00 2001 From: Robert Kopaczewski Date: Mon, 31 Jan 2022 10:01:07 +0100 Subject: [PATCH 45/56] fix: add map with pointer value support (#76) --- diff_pointer.go | 6 ++++++ patch.go | 8 ++++++++ patch_test.go | 28 ++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/diff_pointer.go b/diff_pointer.go index 605abf8..7c9d875 100644 --- a/diff_pointer.go +++ b/diff_pointer.go @@ -17,12 +17,18 @@ func (d *Differ) diffPtr(path []string, a, b reflect.Value, parent interface{}) if !b.IsNil() { return d.diff(path, reflect.ValueOf(nil), reflect.Indirect(b), parent) } + + d.cl.Add(CREATE, path, nil, exportInterface(b), parent) + return nil } if b.Kind() == reflect.Invalid { if !a.IsNil() { return d.diff(path, reflect.Indirect(a), reflect.ValueOf(nil), parent) } + + d.cl.Add(DELETE, path, exportInterface(a), nil, parent) + return nil } return ErrTypeMismatch diff --git a/patch.go b/patch.go index 73d2a90..5814c53 100644 --- a/patch.go +++ b/patch.go @@ -188,6 +188,14 @@ func (d *Differ) renderChangeTarget(c *ChangeValue) { //walking a path means dealing with real elements case reflect.Interface, reflect.Ptr: + if c.target.IsNil() { + n := reflect.New(c.target.Type().Elem()) + c.target.Set(n) + c.target = &n + d.renderChangeTarget(c) + return + } + el := c.target.Elem() c.target = &el c.ClearFlag(FlagInvalidTarget) diff --git a/patch_test.go b/patch_test.go index 4f6b116..8703c17 100644 --- a/patch_test.go +++ b/patch_test.go @@ -304,6 +304,34 @@ func TestPatch(t *testing.T) { assert.False(t, patchLog.HasErrors()) }) + t.Run("map-to-pointer", func(t *testing.T) { + t1 := make(map[string]*tmstruct) + t2 := map[string]*tmstruct{"after": {Foo: "val"}} + + changelog, err := diff.Diff(t1, t2) + assert.NoError(t, err) + + patchLog := diff.Patch(changelog, &t1) + assert.False(t, patchLog.HasErrors()) + + assert.True(t, len(t2) == len(t1)) + assert.Equal(t, t1["after"], &tmstruct{Foo: "val"}) + }) + + t.Run("map-to-nil-pointer", func(t *testing.T) { + t1 := make(map[string]*tmstruct) + t2 := map[string]*tmstruct{"after": nil} + + changelog, err := diff.Diff(t1, t2) + assert.NoError(t, err) + + patchLog := diff.Patch(changelog, &t1) + assert.False(t, patchLog.HasErrors()) + + assert.Equal(t, len(t2), len(t1)) + assert.Nil(t, t1["after"]) + }) + t.Run("pointer-with-converted-type", func(t *testing.T) { type tps struct { S *int From aa40c07628afe86ee5ccbbfb55f35e5674edcafc Mon Sep 17 00:00:00 2001 From: Alfschmalf Date: Thu, 3 Feb 2022 19:06:26 +0100 Subject: [PATCH 46/56] Update diff_struct.go (#77) Enable customValueDiffers for structs in structs --- diff_struct.go | 1 + 1 file changed, 1 insertion(+) diff --git a/diff_struct.go b/diff_struct.go index bc9a496..b58f3e8 100644 --- a/diff_struct.go +++ b/diff_struct.go @@ -71,6 +71,7 @@ func (d *Differ) diffStruct(path []string, a, b reflect.Value) error { func (d *Differ) structValues(t string, path []string, a reflect.Value) error { var nd Differ nd.Filter = d.Filter + nd.customValueDiffers = d.customValueDiffers if t != CREATE && t != DELETE { return ErrInvalidChangeType From 2eb23ac5db4df0011bfd2c8cb3ff9c1c8e2ed38c Mon Sep 17 00:00:00 2001 From: Robert Kopaczewski Date: Thu, 3 Feb 2022 19:07:04 +0100 Subject: [PATCH 47/56] fix: update to nil pointer (#78) --- change_value.go | 2 ++ patch_test.go | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/change_value.go b/change_value.go index f115da9..b0f3a73 100644 --- a/change_value.go +++ b/change_value.go @@ -158,6 +158,8 @@ func (c *ChangeValue) Set(value reflect.Value, convertCompatibleTypes bool) { } else { c.target.Set(value) } + } else if c.target.Kind() == reflect.Ptr { + c.target.Set(reflect.Zero(c.target.Type())) } else if !c.target.IsZero() { t := c.target.Elem() t.Set(reflect.Zero(t.Type())) diff --git a/patch_test.go b/patch_test.go index 8703c17..f85a9d7 100644 --- a/patch_test.go +++ b/patch_test.go @@ -126,6 +126,12 @@ func TestPatch(t *testing.T) { nil, }, { + "struct-string-pointer-update-to-nil", &tstruct{Pointer: sptr("test")}, &tstruct{Pointer: nil}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"pointer"}, From: sptr("test"), To: nil}, + }, + nil, + }, { "struct-generic-slice-insert", &tstruct{Values: []string{"one"}}, &tstruct{Values: []string{"one", "two"}}, diff.Changelog{ diff.Change{Type: diff.CREATE, Path: []string{"values", "1"}, From: nil, To: "two"}, From e323b245f44b07ffff310bc9df6a7657270e6878 Mon Sep 17 00:00:00 2001 From: Robert Kopaczewski Date: Fri, 4 Feb 2022 19:40:30 +0100 Subject: [PATCH 48/56] fix: slice index patch (#79) --- patch_slice.go | 15 +++++++++++++-- patch_test.go | 34 +++------------------------------- 2 files changed, 16 insertions(+), 33 deletions(-) diff --git a/patch_slice.go b/patch_slice.go index 751f64c..9a703e5 100644 --- a/patch_slice.go +++ b/patch_slice.go @@ -17,12 +17,23 @@ func (d *Differ) renderSlice(c *ChangeValue) { //field better be an index of the slice if c.index, err = strconv.Atoi(field); err != nil { - c.AddError(NewErrorf("invalid index in path. %s is not a number", field). - WithCause(err)) + //if struct element is has identifier, use it instead + if identifier(d.TagName, reflect.Zero(c.target.Type().Elem())) != nil { + for c.index = 0; c.index < c.Len(); c.index++ { + if identifier(d.TagName, c.Index(c.index)) == field { + break + } + } + } else { + c.AddError(NewErrorf("invalid index in path. %s is not a number", field). + WithCause(err)) + } } var x reflect.Value if c.Len() > c.index { x = c.Index(c.index) + } else if c.change.Type == CREATE && !c.HasFlag(OptionNoCreate) { + x = c.NewArrayElement() } if !x.IsValid() { if !c.HasFlag(OptionOmitUnequal) { diff --git a/patch_test.go b/patch_test.go index f85a9d7..6dfd2b9 100644 --- a/patch_test.go +++ b/patch_test.go @@ -15,35 +15,30 @@ func TestPatch(t *testing.T) { Name string A, B interface{} Changelog diff.Changelog - Error error }{ { "uint-slice-insert", &[]uint{1, 2, 3}, &[]uint{1, 2, 3, 4}, diff.Changelog{ diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: uint(4)}, }, - nil, }, { "int-slice-insert", &[]int{1, 2, 3}, &[]int{1, 2, 3, 4}, diff.Changelog{ diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: 4}, }, - nil, }, { "uint-slice-delete", &[]uint{1, 2, 3}, &[]uint{1, 3}, diff.Changelog{ diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: uint(2)}, }, - nil, }, { "int-slice-delete", &[]int{1, 2, 3}, &[]int{1, 3}, diff.Changelog{ diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: 2}, }, - nil, }, { "uint-slice-insert-delete", &[]uint{1, 2, 3}, &[]uint{1, 3, 4}, @@ -51,7 +46,6 @@ func TestPatch(t *testing.T) { diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: uint(2)}, diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: uint(4)}, }, - nil, }, { "int-slice-insert-delete", &[]int{1, 2, 3}, &[]int{1, 3, 4}, @@ -59,21 +53,18 @@ func TestPatch(t *testing.T) { diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: 2}, diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: 4}, }, - nil, }, { "string-slice-insert", &[]string{"1", "2", "3"}, &[]string{"1", "2", "3", "4"}, diff.Changelog{ diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: "4"}, }, - nil, }, { "string-slice-delete", &[]string{"1", "2", "3"}, &[]string{"1", "3"}, diff.Changelog{ diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"}, }, - nil, }, { "string-slice-insert-delete", &[]string{"1", "2", "3"}, &[]string{"1", "3", "4"}, @@ -81,69 +72,59 @@ func TestPatch(t *testing.T) { diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"}, diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: "4"}, }, - nil, }, { - "comparable-slice-update", &[]tistruct{{"one", 1}}, &[]tistruct{{"one", 50}}, + "comparable-slice-update", &[]tistruct{{"one", 1}, {"two", 2}}, &[]tistruct{{"one", 1}, {"two", 50}}, diff.Changelog{ - diff.Change{Type: diff.UPDATE, Path: []string{"one", "value"}, From: 1, To: 50}, + diff.Change{Type: diff.UPDATE, Path: []string{"two", "value"}, From: 1, To: 50}, }, - nil, }, { "struct-string-update", &tstruct{Name: "one"}, &tstruct{Name: "two"}, diff.Changelog{ diff.Change{Type: diff.UPDATE, Path: []string{"name"}, From: "one", To: "two"}, }, - nil, }, { "struct-int-update", &tstruct{Value: 1}, &tstruct{Value: 50}, diff.Changelog{ diff.Change{Type: diff.UPDATE, Path: []string{"value"}, From: 1, To: 50}, }, - nil, }, { "struct-bool-update", &tstruct{Bool: true}, &tstruct{Bool: false}, diff.Changelog{ diff.Change{Type: diff.UPDATE, Path: []string{"bool"}, From: true, To: false}, }, - nil, }, { "struct-time-update", &tstruct{}, &tstruct{Time: currentTime}, diff.Changelog{ diff.Change{Type: diff.UPDATE, Path: []string{"time"}, From: time.Time{}, To: currentTime}, }, - nil, }, { "struct-nil-string-pointer-update", &tstruct{Pointer: nil}, &tstruct{Pointer: sptr("test")}, diff.Changelog{ diff.Change{Type: diff.UPDATE, Path: []string{"pointer"}, From: nil, To: sptr("test")}, }, - nil, }, { "struct-string-pointer-update-to-nil", &tstruct{Pointer: sptr("test")}, &tstruct{Pointer: nil}, diff.Changelog{ diff.Change{Type: diff.UPDATE, Path: []string{"pointer"}, From: sptr("test"), To: nil}, }, - nil, }, { "struct-generic-slice-insert", &tstruct{Values: []string{"one"}}, &tstruct{Values: []string{"one", "two"}}, diff.Changelog{ diff.Change{Type: diff.CREATE, Path: []string{"values", "1"}, From: nil, To: "two"}, }, - nil, }, { "struct-generic-slice-delete", &tstruct{Values: []string{"one", "two"}}, &tstruct{Values: []string{"one"}}, diff.Changelog{ diff.Change{Type: diff.DELETE, Path: []string{"values", "1"}, From: "two", To: nil}, }, - nil, }, { "struct-unidentifiable-slice-insert-delete", &tstruct{Unidentifiables: []tuistruct{{1}, {2}, {3}}}, &tstruct{Unidentifiables: []tuistruct{{5}, {2}, {3}, {4}}}, @@ -151,7 +132,6 @@ func TestPatch(t *testing.T) { diff.Change{Type: diff.UPDATE, Path: []string{"unidentifiables", "0", "value"}, From: 1, To: 5}, diff.Change{Type: diff.CREATE, Path: []string{"unidentifiables", "3", "value"}, From: nil, To: 4}, }, - nil, }, { "slice", &tstruct{}, &tstruct{Nested: tnstruct{Slice: []tmstruct{{"one", 1}, {"two", 2}}}}, @@ -161,14 +141,12 @@ func TestPatch(t *testing.T) { diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "1", "foo"}, From: nil, To: "two"}, diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "1", "bar"}, From: nil, To: 2}, }, - nil, }, { "slice-duplicate-items", &[]int{1}, &[]int{1, 1}, diff.Changelog{ diff.Change{Type: diff.CREATE, Path: []string{"1"}, From: nil, To: 1}, }, - nil, }, { "embedded-struct-field", @@ -179,7 +157,6 @@ func TestPatch(t *testing.T) { diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: 2, To: 3}, diff.Change{Type: diff.UPDATE, Path: []string{"baz"}, From: true, To: false}, }, - nil, }, { "custom-tags", @@ -189,7 +166,6 @@ func TestPatch(t *testing.T) { diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: "abc", To: "def"}, diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: 3, To: 4}, }, - nil, }, { "custom-types", @@ -199,7 +175,6 @@ func TestPatch(t *testing.T) { diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: CustomStringType("a"), To: CustomStringType("b")}, diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: CustomIntType(1), To: CustomIntType(2)}, }, - nil, }, { "map", @@ -210,7 +185,6 @@ func TestPatch(t *testing.T) { diff.Change{Type: diff.CREATE, Path: []string{"2"}, From: nil, To: "two"}, diff.Change{Type: diff.UPDATE, Path: []string{"3"}, From: "three", To: "tres"}, }, - nil, }, { "map-nested-create", @@ -219,7 +193,6 @@ func TestPatch(t *testing.T) { diff.Changelog{ diff.Change{Type: "create", Path: []string{"details", "secondary-attributes"}, From: nil, To: map[string]interface{}{"attrA": "A", "attrB": "B"}}, }, - nil, }, { "map-nested-update", @@ -228,7 +201,6 @@ func TestPatch(t *testing.T) { diff.Changelog{ diff.Change{Type: "update", Path: []string{"details", "attributes"}, From: map[string]interface{}{"attrA": "A", "attrB": "B"}, To: map[string]interface{}{"attrA": "C", "attrD": "X"}}, }, - nil, }, { "map-nested-delete", @@ -237,7 +209,6 @@ func TestPatch(t *testing.T) { diff.Changelog{ diff.Change{Type: "delete", Path: []string{"details", "attributes"}, From: map[string]interface{}{"attrA": "A", "attrB": "B"}, To: nil}, }, - nil, }, } @@ -261,6 +232,7 @@ func TestPatch(t *testing.T) { assert.Equal(t, tc.B, tc.A) require.Equal(t, len(tc.Changelog), len(pl)) + assert.False(t, pl.HasErrors()) }) } From c3c4714de392c384c4a9842720fa4e52ffca1c66 Mon Sep 17 00:00:00 2001 From: Wang Fenjin Date: Thu, 10 Feb 2022 19:35:49 +0800 Subject: [PATCH 49/56] Add FilterOut method (#80) Co-authored-by: Fenjin Wang --- diff.go | 13 +++++++++++++ diff_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/diff.go b/diff.go index e57abd3..f7e1b84 100644 --- a/diff.go +++ b/diff.go @@ -108,6 +108,19 @@ func StructValues(t string, path []string, s interface{}) (Changelog, error) { return d.cl, d.structValues(t, path, v) } +// FilterOut filter out the changes based on path. Paths may contain valid regexp to match items +func (cl *Changelog) FilterOut(path []string) Changelog { + var ncl Changelog + + for _, c := range *cl { + if !pathmatch(path, c.Path) { + ncl = append(ncl, c) + } + } + + return ncl +} + // Filter filter changes based on path. Paths may contain valid regexp to match items func (cl *Changelog) Filter(path []string) Changelog { var ncl Changelog diff --git a/diff_test.go b/diff_test.go index 938393f..23942df 100644 --- a/diff_test.go +++ b/diff_test.go @@ -758,6 +758,32 @@ func TestFilter(t *testing.T) { } } +func TestFilterOut(t *testing.T) { + cases := []struct { + Name string + Filter []string + Expected [][]string + }{ + {"simple", []string{"item-1", "subitem"}, [][]string{{"item-2", "subitem"}}}, + {"regex", []string{"item-*"}, [][]string{}}, + } + + cl := diff.Changelog{ + {Path: []string{"item-1", "subitem"}}, + {Path: []string{"item-2", "subitem"}}, + } + + for _, tc := range cases { + t.Run(tc.Name, func(t *testing.T) { + ncl := cl.FilterOut(tc.Filter) + assert.Len(t, ncl, len(tc.Expected)) + for i, e := range tc.Expected { + assert.Equal(t, e, ncl[i].Path) + } + }) + } +} + func TestStructValues(t *testing.T) { cases := []struct { Name string From db3f716104aef930d9a647fc9843d59143eacae2 Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Thu, 10 Feb 2022 12:43:30 +0000 Subject: [PATCH 50/56] fix example test and check if identifier call is for struct --- diff.go | 4 ++++ diff_examples_test.go | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/diff.go b/diff.go index f7e1b84..8ac8bd0 100644 --- a/diff.go +++ b/diff.go @@ -227,6 +227,10 @@ func tagName(tag string, f reflect.StructField) string { } func identifier(tag string, v reflect.Value) interface{} { + if v.Kind() != reflect.Struct { + return nil + } + for i := 0; i < v.NumField(); i++ { if hasTagOption(tag, v.Type().Field(i), "identifier") { return v.Field(i).Interface() diff --git a/diff_examples_test.go b/diff_examples_test.go index 2885eb7..e43c29d 100644 --- a/diff_examples_test.go +++ b/diff_examples_test.go @@ -312,7 +312,7 @@ func ExampleComplexSlicePatch() { fmt.Printf("Patched %d entries and encountered %d errors", len(patchLog), patchLog.ErrorCount()) - //Output: Patched 7 entries and encountered 4 errors + //Output: Patched 7 entries and encountered 3 errors } //ExampleComplexMapPatch demonstrates how to use the Patch function for complex slices. From 22500a7b86fb68088c88f8919a10edc482fa92ac Mon Sep 17 00:00:00 2001 From: XIE Long Date: Tue, 22 Mar 2022 00:05:36 +0800 Subject: [PATCH 51/56] fix if structure contains slice inside, but the items in the internal slice are not in the same order give wrong result (#82) --- diff_slice.go | 18 ++++++++++++++---- diff_test.go | 7 +++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/diff_slice.go b/diff_slice.go index 5441bc7..f6b5455 100644 --- a/diff_slice.go +++ b/diff_slice.go @@ -37,7 +37,7 @@ func (d *Differ) diffSliceGeneric(path []string, a, b reflect.Value) error { for i := 0; i < a.Len(); i++ { ae := a.Index(i) - if (d.SliceOrdering && !hasAtSameIndex(b, ae, i)) || (!d.SliceOrdering && !slice.has(b, ae)) { + if (d.SliceOrdering && !hasAtSameIndex(b, ae, i)) || (!d.SliceOrdering && !slice.has(b, ae, d)) { missing.addA(i, &ae) } } @@ -46,7 +46,7 @@ func (d *Differ) diffSliceGeneric(path []string, a, b reflect.Value) error { for i := 0; i < b.Len(); i++ { be := b.Index(i) - if (d.SliceOrdering && !hasAtSameIndex(a, be, i)) || (!d.SliceOrdering && !slice.has(a, be)) { + if (d.SliceOrdering && !hasAtSameIndex(a, be, i)) || (!d.SliceOrdering && !slice.has(a, be, d)) { missing.addB(i, &be) } } @@ -88,7 +88,7 @@ func (d *Differ) diffSliceComparative(path []string, a, b reflect.Value) error { // keeps track of elements that have already been matched, to stop duplicate matches from occurring type sliceTracker []bool -func (st *sliceTracker) has(s, v reflect.Value) bool { +func (st *sliceTracker) has(s, v reflect.Value, d *Differ) bool { if len(*st) != s.Len() { (*st) = make([]bool, s.Len()) } @@ -100,7 +100,17 @@ func (st *sliceTracker) has(s, v reflect.Value) bool { } x := s.Index(i) - if reflect.DeepEqual(exportInterface(x), exportInterface(v)) { + + var nd Differ + nd.Filter = d.Filter + nd.customValueDiffers = d.customValueDiffers + + err := nd.diff([]string{}, x, v, nil) + if err != nil { + continue + } + + if len(nd.cl) == 0 { (*st)[i] = true return true } diff --git a/diff_test.go b/diff_test.go index 23942df..a075fac 100644 --- a/diff_test.go +++ b/diff_test.go @@ -610,6 +610,13 @@ func TestDiff(t *testing.T) { }, nil, }, + { + "slice-of-struct-with-slice", + []tnstruct{{[]tmstruct{struct1, struct2}}, {[]tmstruct{struct2, struct2}}}, + []tnstruct{{[]tmstruct{struct2, struct2}}, {[]tmstruct{struct2, struct1}}}, + diff.Changelog{}, + nil, + }, } for _, tc := range cases { From 0f0841e52d4fe265c8464ba39736ca2a5c99b274 Mon Sep 17 00:00:00 2001 From: Gustavo Massaneiro Date: Thu, 7 Apr 2022 17:06:15 -0300 Subject: [PATCH 52/56] Added DiffType and DiffFunc to ValueDiffer interface (#83) Added the ability to intercept the data using custom differs --- diff.go | 113 +++++++++++++++++++++++++++++++++++++------------ diff_map.go | 2 +- diff_slice.go | 2 +- diff_struct.go | 2 +- diff_test.go | 53 ++++++++++++++++++++++- 5 files changed, 141 insertions(+), 31 deletions(-) diff --git a/diff.go b/diff.go index 8ac8bd0..64e3dcd 100644 --- a/diff.go +++ b/diff.go @@ -23,6 +23,56 @@ const ( DELETE = "delete" ) +// DiffType represents an enum with all the supported diff types +type DiffType uint8 + +const ( + UNSUPPORTED DiffType = iota + STRUCT + SLICE + ARRAY + STRING + BOOL + INT + UINT + FLOAT + MAP + PTR + INTERFACE +) + +func (t DiffType) String() string { + switch t { + case STRUCT: + return "STRUCT" + case SLICE: + return "SLICE" + case ARRAY: + return "ARRAY" + case STRING: + return "STRING" + case BOOL: + return "BOOL" + case INT: + return "INT" + case UINT: + return "UINT" + case FLOAT: + return "FLOAT" + case MAP: + return "MAP" + case PTR: + return "PTR" + case INTERFACE: + return "INTERFACE" + default: + return "UNSUPPORTED" + } +} + +// DiffFunc represents the built-in diff functions +type DiffFunc func([]string, reflect.Value, reflect.Value, interface{}) error + // Differ a configurable diff instance type Differ struct { TagName string @@ -53,7 +103,7 @@ type Change struct { // ValueDiffer is an interface for custom differs type ValueDiffer interface { Match(a, b reflect.Value) bool - Diff(cl *Changelog, path []string, a, b reflect.Value) error + Diff(dt DiffType, df DiffFunc, cl *Changelog, path []string, a, b reflect.Value, parent interface{}) error InsertParentDiffer(dfunc func(path []string, a, b reflect.Value, p interface{}) error) } @@ -134,6 +184,35 @@ func (cl *Changelog) Filter(path []string) Changelog { return ncl } +func (d *Differ) getDiffType(a, b reflect.Value) (DiffType, DiffFunc) { + switch { + case are(a, b, reflect.Struct, reflect.Invalid): + return STRUCT, d.diffStruct + case are(a, b, reflect.Slice, reflect.Invalid): + return SLICE, d.diffSlice + case are(a, b, reflect.Array, reflect.Invalid): + return ARRAY, d.diffSlice + case are(a, b, reflect.String, reflect.Invalid): + return STRING, d.diffString + case are(a, b, reflect.Bool, reflect.Invalid): + return BOOL, d.diffBool + case are(a, b, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Invalid): + return INT, d.diffInt + case are(a, b, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Invalid): + return UINT, d.diffUint + case are(a, b, reflect.Float32, reflect.Float64, reflect.Invalid): + return FLOAT, d.diffFloat + case are(a, b, reflect.Map, reflect.Invalid): + return MAP, d.diffMap + case are(a, b, reflect.Ptr, reflect.Invalid): + return PTR, d.diffPtr + case are(a, b, reflect.Interface, reflect.Invalid): + return INTERFACE, d.diffInterface + default: + return UNSUPPORTED, nil + } +} + // Diff returns a changelog of all mutated values from both func (d *Differ) Diff(a, b interface{}) (Changelog, error) { // reset the state of the diff @@ -160,11 +239,14 @@ func (d *Differ) diff(path []string, a, b reflect.Value, parent interface{}) err return ErrTypeMismatch } + // get the diff type and the corresponding built-int diff function to handle this type + diffType, diffFunc := d.getDiffType(a, b) + // first go through custom diff functions if len(d.customValueDiffers) > 0 { for _, vd := range d.customValueDiffers { if vd.Match(a, b) { - err := vd.Diff(&d.cl, path, a, b) + err := vd.Diff(diffType, diffFunc, &d.cl, path, a, b, parent) if err != nil { return err } @@ -174,32 +256,11 @@ func (d *Differ) diff(path []string, a, b reflect.Value, parent interface{}) err } // then built-in diff functions - switch { - case are(a, b, reflect.Struct, reflect.Invalid): - return d.diffStruct(path, a, b) - case are(a, b, reflect.Slice, reflect.Invalid): - return d.diffSlice(path, a, b) - case are(a, b, reflect.Array, reflect.Invalid): - return d.diffSlice(path, a, b) - case are(a, b, reflect.String, reflect.Invalid): - return d.diffString(path, a, b, parent) - case are(a, b, reflect.Bool, reflect.Invalid): - return d.diffBool(path, a, b, parent) - case are(a, b, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Invalid): - return d.diffInt(path, a, b, parent) - case are(a, b, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Invalid): - return d.diffUint(path, a, b, parent) - case are(a, b, reflect.Float32, reflect.Float64, reflect.Invalid): - return d.diffFloat(path, a, b, parent) - case are(a, b, reflect.Map, reflect.Invalid): - return d.diffMap(path, a, b) - case are(a, b, reflect.Ptr, reflect.Invalid): - return d.diffPtr(path, a, b, parent) - case are(a, b, reflect.Interface, reflect.Invalid): - return d.diffInterface(path, a, b, parent) - default: + if diffType == UNSUPPORTED { return errors.New("unsupported type: " + a.Kind().String()) } + + return diffFunc(path, a, b, parent) } func (cl *Changelog) Add(t string, path []string, ftco ...interface{}) { diff --git a/diff_map.go b/diff_map.go index dcb8efb..02b1c1b 100644 --- a/diff_map.go +++ b/diff_map.go @@ -11,7 +11,7 @@ import ( "github.com/vmihailenco/msgpack" ) -func (d *Differ) diffMap(path []string, a, b reflect.Value) error { +func (d *Differ) diffMap(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { return d.mapValues(CREATE, path, b) } diff --git a/diff_slice.go b/diff_slice.go index f6b5455..3fd281b 100644 --- a/diff_slice.go +++ b/diff_slice.go @@ -8,7 +8,7 @@ import ( "reflect" ) -func (d *Differ) diffSlice(path []string, a, b reflect.Value) error { +func (d *Differ) diffSlice(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { d.cl.Add(CREATE, path, nil, exportInterface(b)) return nil diff --git a/diff_struct.go b/diff_struct.go index b58f3e8..fb14c57 100644 --- a/diff_struct.go +++ b/diff_struct.go @@ -9,7 +9,7 @@ import ( "time" ) -func (d *Differ) diffStruct(path []string, a, b reflect.Value) error { +func (d *Differ) diffStruct(path []string, a, b reflect.Value, parent interface{}) error { if AreType(a, b, reflect.TypeOf(time.Time{})) { return d.diffTime(path, a, b) } diff --git a/diff_test.go b/diff_test.go index a075fac..22a6655 100644 --- a/diff_test.go +++ b/diff_test.go @@ -6,6 +6,7 @@ package diff_test import ( "reflect" + "strings" "sync" "testing" "time" @@ -907,7 +908,7 @@ func (o *testTypeDiffer) InsertParentDiffer(dfunc func(path []string, a, b refle func (o *testTypeDiffer) Match(a, b reflect.Value) bool { return diff.AreType(a, b, reflect.TypeOf(testType(""))) } -func (o *testTypeDiffer) Diff(cl *diff.Changelog, path []string, a, b reflect.Value) error { +func (o *testTypeDiffer) Diff(dt diff.DiffType, df diff.DiffFunc, cl *diff.Changelog, path []string, a, b reflect.Value, parent interface{}) error { if a.String() != "custom" && b.String() != "match" { cl.Add(diff.UPDATE, path, a.Interface(), b.Interface()) } @@ -944,6 +945,54 @@ func TestCustomDiffer(t *testing.T) { assert.Len(t, cl, 1) } +type testStringInterceptorDiffer struct { + DiffFunc (func(path []string, a, b reflect.Value, p interface{}) error) +} + +func (o *testStringInterceptorDiffer) InsertParentDiffer(dfunc func(path []string, a, b reflect.Value, p interface{}) error) { + o.DiffFunc = dfunc +} + +func (o *testStringInterceptorDiffer) Match(a, b reflect.Value) bool { + return diff.AreType(a, b, reflect.TypeOf(testType(""))) +} +func (o *testStringInterceptorDiffer) Diff(dt diff.DiffType, df diff.DiffFunc, cl *diff.Changelog, path []string, a, b reflect.Value, parent interface{}) error { + if dt.String() == "STRING" { + // intercept the data + aValue, aOk := a.Interface().(testType) + bValue, bOk := b.Interface().(testType) + + if aOk && bOk { + if aValue == "avalue" { + aValue = testType(strings.ToUpper(string(aValue))) + a = reflect.ValueOf(aValue) + } + + if bValue == "bvalue" { + bValue = testType(strings.ToUpper(string(aValue))) + b = reflect.ValueOf(bValue) + } + } + } + + // continue the diff logic passing the updated a/b values + return df(path, a, b, parent) +} + +func TestStringInterceptorDiffer(t *testing.T) { + d, err := diff.NewDiffer( + diff.CustomValueDiffers( + &testStringInterceptorDiffer{}, + ), + ) + require.Nil(t, err) + + cl, err := d.Diff(testType("avalue"), testType("bvalue")) + require.Nil(t, err) + + assert.Len(t, cl, 0) +} + type RecursiveTestStruct struct { Id int Children []RecursiveTestStruct @@ -961,7 +1010,7 @@ func (o *recursiveTestStructDiffer) Match(a, b reflect.Value) bool { return diff.AreType(a, b, reflect.TypeOf(RecursiveTestStruct{})) } -func (o *recursiveTestStructDiffer) Diff(cl *diff.Changelog, path []string, a, b reflect.Value) error { +func (o *recursiveTestStructDiffer) Diff(dt diff.DiffType, df diff.DiffFunc, cl *diff.Changelog, path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { cl.Add(diff.CREATE, path, nil, b.Interface()) return nil From 02e874c4da737b046500e271e2d0cb1c01875feb Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Thu, 7 Apr 2022 21:11:31 +0100 Subject: [PATCH 53/56] prepare for v3 --- README.md | 18 +++++++++--------- diff_examples_test.go | 2 +- diff_test.go | 2 +- go.mod | 2 +- patch_test.go | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index ef16bc5..97aee65 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ A library for diffing golang structures and values. Utilizing field tags and reflection, it is able to compare two structures of the same type and create a changelog of all modified values. The produced changelog can easily be serialized to json. -NOTE: All active development now takes place on the v2 branch. +NOTE: All active development now takes place on the v3 branch. ## Installation -For version 2: +For version 3: ``` -go get github.com/r3labs/diff/v2 +go get github.com/r3labs/diff/v3 ``` ## Changelog Format @@ -84,7 +84,7 @@ In order for struct fields to be compared, they must be tagged with a given name Diffing a basic set of values can be accomplished using the diff functions. Any items that specify a "diff" tag using a name will be compared. ```go -import "github.com/r3labs/diff/v2" +import "github.com/r3labs/diff/v3" type Order struct { ID string `diff:"id"` @@ -125,7 +125,7 @@ When marshalling the changelog to json, the output will look like: Options can be set on the differ at call time which effect how diff acts when building the change log. ```go -import "github.com/r3labs/diff/v2" +import "github.com/r3labs/diff/v3" type Order struct { ID string `diff:"id"` @@ -151,7 +151,7 @@ func main() { You can also create a new instance of a differ that allows options to be set. ```go -import "github.com/r3labs/diff/v2" +import "github.com/r3labs/diff/v3" type Order struct { ID string `diff:"id"` @@ -205,7 +205,7 @@ To accommodate this patch keeps track of each change log option it attempts to a happened for further scrutiny. ```go -import "github.com/r3labs/diff/v2" +import "github.com/r3labs/diff/v3" type Order struct { ID string `diff:"id"` @@ -239,7 +239,7 @@ Instances of differ with options set can also be used when patching. ```go package main -import "github.com/r3labs/diff/v2" +import "github.com/r3labs/diff/v3" type Order struct { ID string `json:"id"` @@ -271,7 +271,7 @@ As a convenience, there is a Merge function that allows one to take three interf time. ```go -import "github.com/r3labs/diff/v2" +import "github.com/r3labs/diff/v3" type Order struct { ID string `diff:"id"` diff --git a/diff_examples_test.go b/diff_examples_test.go index e43c29d..e9d6e8c 100644 --- a/diff_examples_test.go +++ b/diff_examples_test.go @@ -5,7 +5,7 @@ import ( "math/big" "reflect" - "github.com/r3labs/diff/v2" + "github.com/r3labs/diff/v3" ) //Try to do a bunch of stuff that will result in some or all failures diff --git a/diff_test.go b/diff_test.go index 22a6655..4008623 100644 --- a/diff_test.go +++ b/diff_test.go @@ -11,7 +11,7 @@ import ( "testing" "time" - "github.com/r3labs/diff/v2" + "github.com/r3labs/diff/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/go.mod b/go.mod index fee4ee0..216a3b5 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/r3labs/diff/v2 +module github.com/r3labs/diff/v3 go 1.13 diff --git a/patch_test.go b/patch_test.go index 6dfd2b9..fe7f8cd 100644 --- a/patch_test.go +++ b/patch_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/r3labs/diff/v2" + "github.com/r3labs/diff/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) From 4566b8fc342d858a34b9a7859b2197ef849a11ea Mon Sep 17 00:00:00 2001 From: Mark Newman Date: Tue, 10 May 2022 13:01:24 +0100 Subject: [PATCH 54/56] Migrate CI to GitHub Actions --- .github/workflows/ci.yml | 18 ++++++++++++++++++ .travis.yml | 8 -------- 2 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f9e3b60 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,18 @@ +name: CI + +on: [push, workflow_dispatch] + +jobs: + test: + runs-on: ubuntu-latest + + container: + image: golang + + steps: + - uses: actions/checkout@v2 + + - name: Test + run: | + make deps + go test ./... diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 17952b0..0000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: go - -go: - - "1.14" - - master - -before_script: - - make deps From bfef7169a4b92888a509eac1eceda3718907c70d Mon Sep 17 00:00:00 2001 From: Mark Newman Date: Tue, 10 May 2022 13:53:52 +0100 Subject: [PATCH 55/56] Migrate CI to GitHub Actions --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9e3b60..b9b9965 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: [push, workflow_dispatch] jobs: - test: + Test: runs-on: ubuntu-latest container: From e2707de1bcbfb3cbc32042ce63a44b573304fb2f Mon Sep 17 00:00:00 2001 From: Tom Bevan Date: Wed, 18 Jan 2023 12:36:21 +0000 Subject: [PATCH 56/56] update msgpack --- diff.go | 2 +- diff_map.go | 2 +- go.mod | 5 ++--- go.sum | 25 ++++++++----------------- patch_map.go | 9 +++++---- 5 files changed, 17 insertions(+), 26 deletions(-) diff --git a/diff.go b/diff.go index 64e3dcd..3c2ba17 100644 --- a/diff.go +++ b/diff.go @@ -11,7 +11,7 @@ import ( "strconv" "strings" - "github.com/vmihailenco/msgpack" + "github.com/vmihailenco/msgpack/v5" ) const ( diff --git a/diff_map.go b/diff_map.go index 02b1c1b..675ff93 100644 --- a/diff_map.go +++ b/diff_map.go @@ -8,7 +8,7 @@ import ( "fmt" "reflect" - "github.com/vmihailenco/msgpack" + "github.com/vmihailenco/msgpack/v5" ) func (d *Differ) diffMap(path []string, a, b reflect.Value, parent interface{}) error { diff --git a/go.mod b/go.mod index 216a3b5..ccf2fc9 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/r3labs/diff/v3 go 1.13 require ( - github.com/stretchr/testify v1.5.1 - github.com/vmihailenco/msgpack v4.0.4+incompatible - google.golang.org/appengine v1.6.6 // indirect + github.com/stretchr/testify v1.6.1 + github.com/vmihailenco/msgpack/v5 v5.3.5 ) diff --git a/go.sum b/go.sum index 9426b74..d48fa03 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,15 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= -github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/patch_map.go b/patch_map.go index 8a3906a..6c3f035 100644 --- a/patch_map.go +++ b/patch_map.go @@ -4,10 +4,10 @@ import ( "errors" "reflect" - "github.com/vmihailenco/msgpack" + "github.com/vmihailenco/msgpack/v5" ) -//renderMap - handle map rendering for patch +// renderMap - handle map rendering for patch func (d *Differ) renderMap(c *ChangeValue) (m, k, v *reflect.Value) { //we must tease out the type of the key, we use the msgpack from diff to recreate the key kt := c.target.Type().Key() @@ -68,8 +68,9 @@ func (d *Differ) renderMap(c *ChangeValue) (m, k, v *reflect.Value) { } // updateMapEntry - deletes are special, they are handled differently based on options -// container type etc. We have to have special handling for each -// type. Set values are more generic even if they must be instanced +// +// container type etc. We have to have special handling for each +// type. Set values are more generic even if they must be instanced func (d *Differ) updateMapEntry(c *ChangeValue, m, k, v *reflect.Value) { if k == nil || m == nil { return