package diff_test import ( "encoding/json" "testing" "time" "github.com/r3labs/diff/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestPatch(t *testing.T) { cases := []struct { Name string A, B interface{} Changelog diff.Changelog }{ { "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)}, }, }, { "int-slice-insert", &[]int{1, 2, 3}, &[]int{1, 2, 3, 4}, diff.Changelog{ diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: 4}, }, }, { "uint-slice-delete", &[]uint{1, 2, 3}, &[]uint{1, 3}, diff.Changelog{ diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: uint(2)}, }, }, { "int-slice-delete", &[]int{1, 2, 3}, &[]int{1, 3}, diff.Changelog{ diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: 2}, }, }, { "uint-slice-insert-delete", &[]uint{1, 2, 3}, &[]uint{1, 3, 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)}, }, }, { "int-slice-insert-delete", &[]int{1, 2, 3}, &[]int{1, 3, 4}, diff.Changelog{ diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: 2}, diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: 4}, }, }, { "string-slice-insert", &[]string{"1", "2", "3"}, &[]string{"1", "2", "3", "4"}, diff.Changelog{ diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: "4"}, }, }, { "string-slice-delete", &[]string{"1", "2", "3"}, &[]string{"1", "3"}, diff.Changelog{ diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"}, }, }, { "string-slice-insert-delete", &[]string{"1", "2", "3"}, &[]string{"1", "3", "4"}, diff.Changelog{ diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"}, diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: "4"}, }, }, { "comparable-slice-update", &[]tistruct{{"one", 1}, {"two", 2}}, &[]tistruct{{"one", 1}, {"two", 50}}, diff.Changelog{ diff.Change{Type: diff.UPDATE, Path: []string{"two", "value"}, From: 1, To: 50}, }, }, { "struct-string-update", &tstruct{Name: "one"}, &tstruct{Name: "two"}, diff.Changelog{ diff.Change{Type: diff.UPDATE, Path: []string{"name"}, From: "one", To: "two"}, }, }, { "struct-int-update", &tstruct{Value: 1}, &tstruct{Value: 50}, diff.Changelog{ diff.Change{Type: diff.UPDATE, Path: []string{"value"}, From: 1, To: 50}, }, }, { "struct-bool-update", &tstruct{Bool: true}, &tstruct{Bool: false}, diff.Changelog{ diff.Change{Type: diff.UPDATE, Path: []string{"bool"}, From: true, To: false}, }, }, { "struct-time-update", &tstruct{}, &tstruct{Time: currentTime}, diff.Changelog{ diff.Change{Type: diff.UPDATE, Path: []string{"time"}, From: time.Time{}, To: currentTime}, }, }, { "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")}, }, }, { "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}, }, }, { "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"}, }, }, { "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}, }, }, { "struct-unidentifiable-slice-insert-delete", &tstruct{Unidentifiables: []tuistruct{{1}, {2}, {3}}}, &tstruct{Unidentifiables: []tuistruct{{5}, {2}, {3}, {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}, }, }, { "slice", &tstruct{}, &tstruct{Nested: tnstruct{Slice: []tmstruct{{"one", 1}, {"two", 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}, }, }, { "slice-duplicate-items", &[]int{1}, &[]int{1, 1}, diff.Changelog{ diff.Change{Type: diff.CREATE, Path: []string{"1"}, From: nil, To: 1}, }, }, { "embedded-struct-field", &embedstruct{Embedded{Foo: "a", Bar: 2}, true}, &embedstruct{Embedded{Foo: "b", Bar: 3}, 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}, }, }, { "custom-tags", &customTagStruct{Foo: "abc", Bar: 3}, &customTagStruct{Foo: "def", Bar: 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}, }, }, { "custom-types", &customTypeStruct{Foo: "a", Bar: 1}, &customTypeStruct{Foo: "b", Bar: 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)}, }, }, { "map", map[string]interface{}{"1": "one", "3": "three"}, map[string]interface{}{"2": "two", "3": "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"}, }, }, { "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"}}}, diff.Changelog{ diff.Change{Type: "create", Path: []string{"details", "secondary-attributes"}, From: nil, To: map[string]interface{}{"attrA": "A", "attrB": "B"}}, }, }, { "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"}}}, 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"}}, }, }, { "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"}}, diff.Changelog{ diff.Change{Type: "delete", Path: []string{"details", "attributes"}, From: map[string]interface{}{"attrA": "A", "attrB": "B"}, To: nil}, }, }, } for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { var options []func(d *diff.Differ) error switch tc.Name { case "mixed-slice-map", "nil-map", "map-nil": options = append(options, diff.StructMapKeySupport()) case "embedded-struct-field": options = append(options, diff.FlattenEmbeddedStructs()) case "custom-tags": options = append(options, diff.TagName("json")) } d, err := diff.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)) assert.False(t, pl.HasErrors()) }) } t.Run("convert-types", func(t *testing.T) { a := &tmstruct{Foo: "a", Bar: 1} b := &customTypeStruct{Foo: "b", Bar: 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 := diff.NewDiffer() if err != nil { panic(err) } pl := d.Patch(cl, a) assert.True(t, pl.HasErrors()) d, err = diff.NewDiffer(diff.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)) }) t.Run("pointer", func(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()) }) 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 } 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) // test nil pointer t1 = tps{S: &val1} t2 = tps{S: nil} changelog, err = diff.Diff(t1, t2) assert.NoError(t, err) patchLog = d.Patch(changelog, &t1) assert.False(t, patchLog.HasErrors()) }) }