├── Makefile ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── go.mod ├── filter.go ├── diff_bool.go ├── diff_int.go ├── diff_uint.go ├── diff_float.go ├── diff_time.go ├── diff_interface.go ├── diff_string.go ├── comparative.go ├── go.sum ├── diff_comparative.go ├── diff_pointer.go ├── patch_struct.go ├── error.go ├── diff_map.go ├── patch_slice.go ├── diff_struct.go ├── patch_map.go ├── options.go ├── CONTRIBUTING.md ├── diff_slice.go ├── change_value.go ├── patch.go ├── diff.go ├── README.md ├── patch_test.go ├── diff_examples_test.go ├── LICENSE └── diff_test.go /Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | go install -v ${LDFLAGS} 3 | 4 | deps: 5 | go get github.com/stretchr/testify 6 | 7 | test: 8 | @go test -v -cover ./... 9 | 10 | cover: 11 | @go test -coverprofile cover.out 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, workflow_dispatch] 4 | 5 | jobs: 6 | Test: 7 | runs-on: ubuntu-latest 8 | 9 | container: 10 | image: golang 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Test 16 | run: | 17 | make deps 18 | go test ./... 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | .idea/ 16 | 17 | patchflags_string.go 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/r3labs/diff/v3 2 | 3 | go 1.23.5 4 | 5 | require ( 6 | github.com/stretchr/testify v1.10.0 7 | github.com/vmihailenco/msgpack/v5 v5.4.1 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 14 | gopkg.in/yaml.v3 v3.0.1 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /filter.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | package diff 6 | 7 | import "regexp" 8 | 9 | func pathmatch(filter, path []string) bool { 10 | for i, f := range filter { 11 | if len(path) < i+1 { 12 | return false 13 | } 14 | 15 | matched, _ := regexp.MatchString(f, path[i]) 16 | if !matched { 17 | return false 18 | } 19 | } 20 | 21 | return true 22 | } 23 | -------------------------------------------------------------------------------- /diff_bool.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | package diff 6 | 7 | import "reflect" 8 | 9 | func (d *Differ) diffBool(path []string, a, b reflect.Value, parent interface{}) error { 10 | if a.Kind() == reflect.Invalid { 11 | d.cl.Add(CREATE, path, nil, exportInterface(b)) 12 | return nil 13 | } 14 | 15 | if b.Kind() == reflect.Invalid { 16 | d.cl.Add(DELETE, path, exportInterface(a), nil) 17 | return nil 18 | } 19 | 20 | if a.Kind() != b.Kind() { 21 | return ErrTypeMismatch 22 | } 23 | 24 | if a.Bool() != b.Bool() { 25 | d.cl.Add(UPDATE, path, exportInterface(a), exportInterface(b), parent) 26 | } 27 | 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /diff_int.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | package diff 6 | 7 | import ( 8 | "reflect" 9 | ) 10 | 11 | func (d *Differ) diffInt(path []string, a, b reflect.Value, parent interface{}) error { 12 | if a.Kind() == reflect.Invalid { 13 | d.cl.Add(CREATE, path, nil, exportInterface(b)) 14 | return nil 15 | } 16 | 17 | if b.Kind() == reflect.Invalid { 18 | d.cl.Add(DELETE, path, exportInterface(a), nil) 19 | return nil 20 | } 21 | 22 | if a.Kind() != b.Kind() { 23 | return ErrTypeMismatch 24 | } 25 | 26 | if a.Int() != b.Int() { 27 | if a.CanInterface() { 28 | d.cl.Add(UPDATE, path, exportInterface(a), exportInterface(b), parent) 29 | } else { 30 | d.cl.Add(UPDATE, path, a.Int(), b.Int(), parent) 31 | } 32 | } 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /diff_uint.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | package diff 6 | 7 | import ( 8 | "reflect" 9 | ) 10 | 11 | func (d *Differ) diffUint(path []string, a, b reflect.Value, parent interface{}) error { 12 | if a.Kind() == reflect.Invalid { 13 | d.cl.Add(CREATE, path, nil, exportInterface(b)) 14 | return nil 15 | } 16 | 17 | if b.Kind() == reflect.Invalid { 18 | d.cl.Add(DELETE, path, exportInterface(a), nil) 19 | return nil 20 | } 21 | 22 | if a.Kind() != b.Kind() { 23 | return ErrTypeMismatch 24 | } 25 | 26 | if a.Uint() != b.Uint() { 27 | if a.CanInterface() { 28 | d.cl.Add(UPDATE, path, exportInterface(a), exportInterface(b), parent) 29 | } else { 30 | d.cl.Add(UPDATE, path, a.Uint(), b.Uint(), parent) 31 | } 32 | } 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /diff_float.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | package diff 6 | 7 | import ( 8 | "reflect" 9 | ) 10 | 11 | func (d *Differ) diffFloat(path []string, a, b reflect.Value, parent interface{}) error { 12 | if a.Kind() == reflect.Invalid { 13 | d.cl.Add(CREATE, path, nil, exportInterface(b)) 14 | return nil 15 | } 16 | 17 | if b.Kind() == reflect.Invalid { 18 | d.cl.Add(DELETE, path, exportInterface(a), nil) 19 | return nil 20 | } 21 | 22 | if a.Kind() != b.Kind() { 23 | return ErrTypeMismatch 24 | } 25 | 26 | if a.Float() != b.Float() { 27 | if a.CanInterface() { 28 | d.cl.Add(UPDATE, path, exportInterface(a), exportInterface(b), parent) 29 | } else { 30 | d.cl.Add(UPDATE, path, a.Float(), b.Float(), parent) 31 | } 32 | } 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /diff_time.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | package diff 6 | 7 | import ( 8 | "reflect" 9 | "time" 10 | ) 11 | 12 | func (d *Differ) diffTime(path []string, a, b reflect.Value) error { 13 | if a.Kind() == reflect.Invalid { 14 | d.cl.Add(CREATE, path, nil, exportInterface(b)) 15 | return nil 16 | } 17 | 18 | if b.Kind() == reflect.Invalid { 19 | d.cl.Add(DELETE, path, exportInterface(a), nil) 20 | return nil 21 | } 22 | 23 | if a.Kind() != b.Kind() { 24 | return ErrTypeMismatch 25 | } 26 | 27 | // Marshal and unmarshal time type will lose accuracy. Using unix nano to compare time type. 28 | au := exportInterface(a).(time.Time).UnixNano() 29 | bu := exportInterface(b).(time.Time).UnixNano() 30 | 31 | if au != bu { 32 | d.cl.Add(UPDATE, path, exportInterface(a), exportInterface(b)) 33 | } 34 | 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /diff_interface.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | package diff 6 | 7 | import "reflect" 8 | 9 | func (d *Differ) diffInterface(path []string, a, b reflect.Value, parent interface{}) error { 10 | if a.Kind() == reflect.Invalid { 11 | d.cl.Add(CREATE, path, nil, exportInterface(b)) 12 | return nil 13 | } 14 | 15 | if b.Kind() == reflect.Invalid { 16 | d.cl.Add(DELETE, path, exportInterface(a), nil) 17 | return nil 18 | } 19 | 20 | if a.Kind() != b.Kind() { 21 | return ErrTypeMismatch 22 | } 23 | 24 | if a.IsNil() && b.IsNil() { 25 | return nil 26 | } 27 | 28 | if a.IsNil() { 29 | d.cl.Add(UPDATE, path, nil, exportInterface(b), parent) 30 | return nil 31 | } 32 | 33 | if b.IsNil() { 34 | d.cl.Add(UPDATE, path, exportInterface(a), nil, parent) 35 | return nil 36 | } 37 | 38 | return d.diff(path, a.Elem(), b.Elem(), parent) 39 | } 40 | -------------------------------------------------------------------------------- /diff_string.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | package diff 6 | 7 | import "reflect" 8 | 9 | func (d *Differ) diffString(path []string, a, b reflect.Value, parent interface{}) error { 10 | if a.Kind() == reflect.Invalid { 11 | d.cl.Add(CREATE, path, nil, exportInterface(b)) 12 | return nil 13 | } 14 | 15 | if b.Kind() == reflect.Invalid { 16 | d.cl.Add(DELETE, path, exportInterface(a), nil) 17 | return nil 18 | } 19 | 20 | if a.Kind() != b.Kind() { 21 | return ErrTypeMismatch 22 | } 23 | 24 | if a.String() != b.String() { 25 | if a.CanInterface() { 26 | // If a and/or b is of a type that is an alias for String, store that type in changelog 27 | d.cl.Add(UPDATE, path, exportInterface(a), exportInterface(b), parent) 28 | } else { 29 | d.cl.Add(UPDATE, path, a.String(), b.String(), parent) 30 | } 31 | } 32 | 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /comparative.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | package diff 6 | 7 | import ( 8 | "reflect" 9 | ) 10 | 11 | // Comparative ... 12 | type Comparative struct { 13 | A, B *reflect.Value 14 | } 15 | 16 | // ComparativeList : stores indexed comparative 17 | type ComparativeList struct { 18 | m map[interface{}]*Comparative 19 | keys []interface{} 20 | } 21 | 22 | // NewComparativeList : returns a new comparative list 23 | func NewComparativeList() *ComparativeList { 24 | return &ComparativeList{ 25 | m: make(map[interface{}]*Comparative), 26 | keys: make([]interface{}, 0), 27 | } 28 | } 29 | 30 | func (cl *ComparativeList) addA(k interface{}, v *reflect.Value) { 31 | if (*cl).m[k] == nil { 32 | (*cl).m[k] = &Comparative{} 33 | (*cl).keys = append((*cl).keys, k) 34 | } 35 | (*cl).m[k].A = v 36 | } 37 | 38 | func (cl *ComparativeList) addB(k interface{}, v *reflect.Value) { 39 | if (*cl).m[k] == nil { 40 | (*cl).m[k] = &Comparative{} 41 | (*cl).keys = append((*cl).keys, k) 42 | } 43 | (*cl).m[k].B = v 44 | } 45 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 6 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 7 | github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= 8 | github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= 9 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 10 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 12 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 13 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 14 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 15 | -------------------------------------------------------------------------------- /diff_comparative.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | package diff 6 | 7 | import ( 8 | "reflect" 9 | ) 10 | 11 | func (d *Differ) diffComparative(path []string, c *ComparativeList, parent interface{}) error { 12 | for _, k := range c.keys { 13 | id := idstring(k) 14 | if d.StructMapKeys { 15 | id = idComplex(k) 16 | } 17 | 18 | fpath := copyAppend(path, id) 19 | nv := reflect.ValueOf(nil) 20 | 21 | if c.m[k].A == nil { 22 | c.m[k].A = &nv 23 | } 24 | 25 | if c.m[k].B == nil { 26 | c.m[k].B = &nv 27 | } 28 | 29 | err := d.diff(fpath, *c.m[k].A, *c.m[k].B, parent) 30 | if err != nil { 31 | return err 32 | } 33 | } 34 | 35 | return nil 36 | } 37 | 38 | func (d *Differ) comparative(a, b reflect.Value) bool { 39 | if a.Len() > 0 { 40 | ae := a.Index(0) 41 | ak := getFinalValue(ae) 42 | 43 | if ak.Kind() == reflect.Struct { 44 | if identifier(d.TagName, ak) != nil { 45 | return true 46 | } 47 | } 48 | } 49 | 50 | if b.Len() > 0 { 51 | be := b.Index(0) 52 | bk := getFinalValue(be) 53 | 54 | if bk.Kind() == reflect.Struct { 55 | if identifier(d.TagName, bk) != nil { 56 | return true 57 | } 58 | } 59 | } 60 | 61 | return false 62 | } 63 | -------------------------------------------------------------------------------- /diff_pointer.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | package diff 6 | 7 | import ( 8 | "reflect" 9 | "unsafe" 10 | ) 11 | 12 | var isExportFlag uintptr = (1 << 5) | (1 << 6) 13 | 14 | func (d *Differ) diffPtr(path []string, a, b reflect.Value, parent interface{}) error { 15 | if a.Kind() != b.Kind() { 16 | if a.Kind() == reflect.Invalid { 17 | if !b.IsNil() { 18 | return d.diff(path, reflect.ValueOf(nil), reflect.Indirect(b), parent) 19 | } 20 | 21 | d.cl.Add(CREATE, path, nil, exportInterface(b), parent) 22 | return nil 23 | } 24 | 25 | if b.Kind() == reflect.Invalid { 26 | if !a.IsNil() { 27 | return d.diff(path, reflect.Indirect(a), reflect.ValueOf(nil), parent) 28 | } 29 | 30 | d.cl.Add(DELETE, path, exportInterface(a), nil, parent) 31 | return nil 32 | } 33 | 34 | return ErrTypeMismatch 35 | } 36 | 37 | if a.IsNil() && b.IsNil() { 38 | return nil 39 | } 40 | 41 | if a.IsNil() { 42 | d.cl.Add(UPDATE, path, nil, exportInterface(b), parent) 43 | return nil 44 | } 45 | 46 | if b.IsNil() { 47 | d.cl.Add(UPDATE, path, exportInterface(a), nil, parent) 48 | return nil 49 | } 50 | 51 | return d.diff(path, reflect.Indirect(a), reflect.Indirect(b), parent) 52 | } 53 | 54 | func exportInterface(v reflect.Value) interface{} { 55 | if !v.CanInterface() { 56 | flagTmp := (*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + 2*unsafe.Sizeof(uintptr(0)))) 57 | *flagTmp = (*flagTmp) & (^isExportFlag) 58 | } 59 | return v.Interface() 60 | } 61 | -------------------------------------------------------------------------------- /patch_struct.go: -------------------------------------------------------------------------------- 1 | package diff 2 | 3 | import "reflect" 4 | 5 | /** 6 | Types are being split out to more closely follow the library structure already 7 | in place. Keeps the file simpler as well. 8 | */ 9 | 10 | type structField struct { 11 | f reflect.StructField 12 | v reflect.Value 13 | } 14 | 15 | func getNestedFields(v reflect.Value, flattenEmbedded bool) []structField { 16 | fields := make([]structField, 0) 17 | 18 | for i := 0; i < v.NumField(); i++ { 19 | f := v.Type().Field(i) 20 | fv := v.Field(i) 21 | 22 | if fv.Kind() == reflect.Struct && f.Anonymous && flattenEmbedded { 23 | fields = append(fields, getNestedFields(fv, flattenEmbedded)...) 24 | } else { 25 | fields = append(fields, structField{f, fv}) 26 | } 27 | } 28 | 29 | return fields 30 | } 31 | 32 | //patchStruct - handles the rendering of a struct field 33 | func (d *Differ) patchStruct(c *ChangeValue) { 34 | 35 | field := c.change.Path[c.pos] 36 | 37 | structFields := getNestedFields(*c.target, d.FlattenEmbeddedStructs) 38 | for _, structField := range structFields { 39 | f := structField.f 40 | tname := tagName(d.TagName, f) 41 | if tname == "-" { 42 | continue 43 | } 44 | if tname == field || f.Name == field { 45 | x := structField.v 46 | if hasTagOption(d.TagName, f, "nocreate") { 47 | c.SetFlag(OptionNoCreate) 48 | } 49 | if hasTagOption(d.TagName, f, "omitunequal") { 50 | c.SetFlag(OptionOmitUnequal) 51 | } 52 | if hasTagOption(d.TagName, f, "immutable") { 53 | c.SetFlag(OptionImmutable) 54 | } 55 | c.swap(&x) 56 | break 57 | } 58 | } 59 | } 60 | 61 | //track and zero out struct members 62 | func (d *Differ) deleteStructEntry(c *ChangeValue) { 63 | 64 | //deleting a struct value set's it to the 'basic' type 65 | c.Set(reflect.Zero(c.target.Type()), d.ConvertCompatibleTypes) 66 | } 67 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package diff 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | var ( 8 | // ErrTypeMismatch Compared types do not match 9 | ErrTypeMismatch = NewError("types do not match") 10 | // ErrInvalidChangeType The specified change values are not unsupported 11 | ErrInvalidChangeType = NewError("change type must be one of 'create' or 'delete'") 12 | ) 13 | 14 | //our own version of an error, which can wrap others 15 | type DiffError struct { 16 | count int 17 | message string 18 | next error 19 | } 20 | 21 | //Unwrap implement 1.13 unwrap feature for compatibility 22 | func (s *DiffError) Unwrap() error { 23 | return s.next 24 | } 25 | 26 | //Error implements the error interface 27 | func (s DiffError) Error() string { 28 | cause := "" 29 | if s.next != nil { 30 | cause = s.next.Error() 31 | } 32 | return fmt.Sprintf(" %s (cause count %d)\n%s", s.message, s.count, cause) 33 | } 34 | 35 | //AppendCause appends a new cause error to the chain 36 | func (s *DiffError) WithCause(err error) *DiffError { 37 | if s != nil && err != nil { 38 | s.count++ 39 | if s.next != nil { 40 | if v, ok := err.(DiffError); ok { 41 | s.next = v.WithCause(s.next) 42 | } else if v, ok := err.(*DiffError); ok { 43 | s.next = v.WithCause(s.next) 44 | } else { 45 | v = &DiffError{ 46 | message: "auto wrapped error", 47 | next: err, 48 | } 49 | s.next = v.WithCause(s.next) 50 | } 51 | } else { 52 | s.next = err 53 | } 54 | } 55 | return s 56 | } 57 | 58 | //NewErrorf just give me a plain error with formatting 59 | func NewErrorf(format string, messages ...interface{}) *DiffError { 60 | return &DiffError{ 61 | message: fmt.Sprintf(format, messages...), 62 | } 63 | } 64 | 65 | //NewError just give me a plain error 66 | func NewError(message string, causes ...error) *DiffError { 67 | s := &DiffError{ 68 | message: message, 69 | } 70 | for _, cause := range causes { 71 | s.WithCause(cause) // nolint: errcheck 72 | } 73 | return s 74 | } 75 | -------------------------------------------------------------------------------- /diff_map.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | package diff 6 | 7 | import ( 8 | "fmt" 9 | "reflect" 10 | 11 | "github.com/vmihailenco/msgpack/v5" 12 | ) 13 | 14 | func (d *Differ) diffMap(path []string, a, b reflect.Value, parent interface{}) error { 15 | if a.Kind() == reflect.Invalid { 16 | return d.mapValues(CREATE, path, b) 17 | } 18 | 19 | if b.Kind() == reflect.Invalid { 20 | return d.mapValues(DELETE, path, a) 21 | } 22 | 23 | c := NewComparativeList() 24 | 25 | for _, k := range a.MapKeys() { 26 | ae := a.MapIndex(k) 27 | c.addA(exportInterface(k), &ae) 28 | } 29 | 30 | for _, k := range b.MapKeys() { 31 | be := b.MapIndex(k) 32 | c.addB(exportInterface(k), &be) 33 | } 34 | 35 | return d.diffComparative(path, c, exportInterface(a)) 36 | } 37 | 38 | func (d *Differ) mapValues(t string, path []string, a reflect.Value) error { 39 | if t != CREATE && t != DELETE { 40 | return ErrInvalidChangeType 41 | } 42 | 43 | if a.Kind() == reflect.Ptr { 44 | a = reflect.Indirect(a) 45 | } 46 | 47 | if a.Kind() != reflect.Map { 48 | return ErrTypeMismatch 49 | } 50 | 51 | x := reflect.New(a.Type()).Elem() 52 | 53 | for _, k := range a.MapKeys() { 54 | ae := a.MapIndex(k) 55 | xe := x.MapIndex(k) 56 | 57 | var err error 58 | if d.StructMapKeys { 59 | //it's not enough to turn k to a string, we need to able to marshal a type when 60 | //we apply it in patch so... we'll marshal it to JSON 61 | var b []byte 62 | if b, err = msgpack.Marshal(k.Interface()); err == nil { 63 | err = d.diff(append(path, string(b)), xe, ae, a.Interface()) 64 | } 65 | } else { 66 | err = d.diff(append(path, fmt.Sprint(k.Interface())), xe, ae, a.Interface()) 67 | } 68 | if err != nil { 69 | return err 70 | } 71 | } 72 | 73 | for i := 0; i < len(d.cl); i++ { 74 | // only swap changes on the relevant map 75 | if pathmatch(path, d.cl[i].Path) { 76 | d.cl[i] = swapChange(t, d.cl[i]) 77 | } 78 | } 79 | 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /patch_slice.go: -------------------------------------------------------------------------------- 1 | package diff 2 | 3 | /** 4 | Types are being split out to more closely follow the library structure already 5 | in place. Keeps the file simpler as well. 6 | */ 7 | import ( 8 | "reflect" 9 | "strconv" 10 | ) 11 | 12 | //renderSlice - handle slice rendering for patch 13 | func (d *Differ) renderSlice(c *ChangeValue) { 14 | 15 | var err error 16 | field := c.change.Path[c.pos] 17 | 18 | //field better be an index of the slice 19 | if c.index, err = strconv.Atoi(field); err != nil { 20 | //if struct element is has identifier, use it instead 21 | if identifier(d.TagName, reflect.Zero(c.target.Type().Elem())) != nil { 22 | for c.index = 0; c.index < c.Len(); c.index++ { 23 | if identifier(d.TagName, c.Index(c.index)) == field { 24 | break 25 | } 26 | } 27 | } else { 28 | c.AddError(NewErrorf("invalid index in path. %s is not a number", field). 29 | WithCause(err)) 30 | } 31 | } 32 | var x reflect.Value 33 | if c.Len() > c.index { 34 | x = c.Index(c.index) 35 | } else if c.change.Type == CREATE && !c.HasFlag(OptionNoCreate) { 36 | x = c.NewArrayElement() 37 | } 38 | if !x.IsValid() { 39 | if !c.HasFlag(OptionOmitUnequal) { 40 | c.AddError(NewErrorf("Value index %d is invalid", c.index). 41 | WithCause(NewError("scanning for Value index"))) 42 | for c.index = 0; c.index < c.Len(); c.index++ { 43 | y := c.Index(c.index) 44 | if reflect.DeepEqual(y, c.change.From) { 45 | c.AddError(NewErrorf("Value changed index to %d", c.index)) 46 | x = y 47 | break 48 | } 49 | } 50 | } 51 | } 52 | if !x.IsValid() && c.change.Type != DELETE && !c.HasFlag(OptionNoCreate) { 53 | x = c.NewArrayElement() 54 | } 55 | if !x.IsValid() && c.change.Type == DELETE { 56 | c.index = -1 //no existing element to delete so don't bother 57 | } 58 | c.swap(&x) //containers must swap out the parent Value 59 | } 60 | 61 | //deleteSliceEntry - deletes are special, they are handled differently based on options 62 | // container type etc. We have to have special handling for each 63 | // type. Set values are more generic even if they must be instanced 64 | func (d *Differ) deleteSliceEntry(c *ChangeValue) { 65 | //for a slice with only one element 66 | if c.ParentLen() == 1 && c.index != -1 { 67 | c.ParentSet(reflect.MakeSlice(c.parent.Type(), 0, 0), d.ConvertCompatibleTypes) 68 | c.SetFlag(FlagDeleted) 69 | //for a slice with multiple elements 70 | } else if c.index != -1 { //this is an array delete the element from the parent 71 | c.ParentIndex(c.index).Set(c.ParentIndex(c.ParentLen() - 1)) 72 | c.ParentSet(c.parent.Slice(0, c.ParentLen()-1), d.ConvertCompatibleTypes) 73 | c.SetFlag(FlagDeleted) 74 | //for other slice elements, we ignore 75 | } else { 76 | c.SetFlag(FlagIgnored) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /diff_struct.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | package diff 6 | 7 | import ( 8 | "reflect" 9 | "time" 10 | ) 11 | 12 | func (d *Differ) diffStruct(path []string, a, b reflect.Value, parent interface{}) error { 13 | if AreType(a, b, reflect.TypeOf(time.Time{})) { 14 | return d.diffTime(path, a, b) 15 | } 16 | 17 | if a.Kind() == reflect.Invalid { 18 | if d.DisableStructValues { 19 | d.cl.Add(CREATE, path, nil, exportInterface(b)) 20 | return nil 21 | } 22 | return d.structValues(CREATE, path, b) 23 | } 24 | 25 | if b.Kind() == reflect.Invalid { 26 | if d.DisableStructValues { 27 | d.cl.Add(DELETE, path, exportInterface(a), nil) 28 | return nil 29 | } 30 | return d.structValues(DELETE, path, a) 31 | } 32 | 33 | for i := 0; i < a.NumField(); i++ { 34 | field := a.Type().Field(i) 35 | tname := tagName(d.TagName, field) 36 | 37 | if tname == "-" || hasTagOption(d.TagName, field, "immutable") { 38 | continue 39 | } 40 | 41 | if tname == "" { 42 | tname = field.Name 43 | } 44 | 45 | af := a.Field(i) 46 | bf := b.FieldByName(field.Name) 47 | 48 | fpath := path 49 | if !(d.FlattenEmbeddedStructs && field.Anonymous) { 50 | fpath = copyAppend(fpath, tname) 51 | } 52 | 53 | if d.Filter != nil && !d.Filter(fpath, a.Type(), field) { 54 | continue 55 | } 56 | 57 | // skip private fields 58 | if !a.CanInterface() { 59 | continue 60 | } 61 | 62 | err := d.diff(fpath, af, bf, exportInterface(a)) 63 | if err != nil { 64 | return err 65 | } 66 | } 67 | 68 | return nil 69 | } 70 | 71 | func (d *Differ) structValues(t string, path []string, a reflect.Value) error { 72 | var nd Differ 73 | nd.Filter = d.Filter 74 | nd.customValueDiffers = d.customValueDiffers 75 | 76 | if t != CREATE && t != DELETE { 77 | return ErrInvalidChangeType 78 | } 79 | 80 | if a.Kind() == reflect.Ptr { 81 | a = reflect.Indirect(a) 82 | } 83 | 84 | if a.Kind() != reflect.Struct { 85 | return ErrTypeMismatch 86 | } 87 | 88 | x := reflect.New(a.Type()).Elem() 89 | 90 | for i := 0; i < a.NumField(); i++ { 91 | 92 | field := a.Type().Field(i) 93 | tname := tagName(d.TagName, field) 94 | 95 | if tname == "-" { 96 | continue 97 | } 98 | 99 | if tname == "" { 100 | tname = field.Name 101 | } 102 | 103 | af := a.Field(i) 104 | xf := x.FieldByName(field.Name) 105 | 106 | fpath := copyAppend(path, tname) 107 | 108 | if nd.Filter != nil && !nd.Filter(fpath, a.Type(), field) { 109 | continue 110 | } 111 | 112 | err := nd.diff(fpath, xf, af, exportInterface(a)) 113 | if err != nil { 114 | return err 115 | } 116 | } 117 | 118 | for i := 0; i < len(nd.cl); i++ { 119 | (d.cl) = append(d.cl, swapChange(t, nd.cl[i])) 120 | } 121 | 122 | return nil 123 | } 124 | -------------------------------------------------------------------------------- /patch_map.go: -------------------------------------------------------------------------------- 1 | package diff 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | 7 | "github.com/vmihailenco/msgpack/v5" 8 | ) 9 | 10 | // renderMap - handle map rendering for patch 11 | func (d *Differ) renderMap(c *ChangeValue) (m, k, v *reflect.Value) { 12 | //we must tease out the type of the key, we use the msgpack from diff to recreate the key 13 | kt := c.target.Type().Key() 14 | field := reflect.New(kt) 15 | 16 | if d.StructMapKeys { 17 | if err := msgpack.Unmarshal([]byte(c.change.Path[c.pos]), field.Interface()); err != nil { 18 | c.SetFlag(FlagIgnored) 19 | c.AddError(NewError("Unable to unmarshal path element to target type for key in map", err)) 20 | return 21 | } 22 | c.key = field.Elem() 23 | } else { 24 | c.key = reflect.ValueOf(c.change.Path[c.pos]) 25 | } 26 | 27 | if c.target.IsNil() && c.target.IsValid() { 28 | c.target.Set(reflect.MakeMap(c.target.Type())) 29 | } 30 | 31 | // we need to check that MapIndex does not panic here 32 | // when the key type is not a string 33 | defer func() { 34 | if err := recover(); err != nil { 35 | switch x := err.(type) { 36 | case error: 37 | c.AddError(NewError("Unable to unmarshal path element to target type for key in map", x)) 38 | case string: 39 | c.AddError(NewError("Unable to unmarshal path element to target type for key in map", errors.New(x))) 40 | } 41 | c.SetFlag(FlagIgnored) 42 | } 43 | }() 44 | 45 | x := c.target.MapIndex(c.key) 46 | 47 | if !x.IsValid() && c.change.Type != DELETE && !c.HasFlag(OptionNoCreate) { 48 | x = c.NewElement() 49 | } 50 | if x.IsValid() { //Map elements come out as read only so we must convert 51 | nv := reflect.New(x.Type()).Elem() 52 | nv.Set(x) 53 | x = nv 54 | } 55 | 56 | if x.IsValid() && !reflect.DeepEqual(c.change.From, x.Interface()) && 57 | c.HasFlag(OptionOmitUnequal) { 58 | c.SetFlag(FlagIgnored) 59 | c.AddError(NewError("target change doesn't match original")) 60 | return 61 | } 62 | mp := *c.target //these may change out from underneath us as we recurse 63 | key := c.key //so we make copies and pass back pointers to them 64 | c.swap(&x) 65 | 66 | return &mp, &key, &x 67 | 68 | } 69 | 70 | // updateMapEntry - deletes are special, they are handled differently based on options 71 | // 72 | // container type etc. We have to have special handling for each 73 | // type. Set values are more generic even if they must be instanced 74 | func (d *Differ) updateMapEntry(c *ChangeValue, m, k, v *reflect.Value) { 75 | if k == nil || m == nil { 76 | return 77 | } 78 | 79 | switch c.change.Type { 80 | case DELETE: 81 | if c.HasFlag(FlagDeleted) { 82 | return 83 | } 84 | 85 | if !m.CanSet() && v.IsValid() && v.Kind() == reflect.Struct { 86 | for x := 0; x < v.NumField(); x++ { 87 | if !v.Field(x).IsZero() { 88 | m.SetMapIndex(*k, *v) 89 | return 90 | } 91 | } //if all the fields are zero, remove from map 92 | } 93 | 94 | m.SetMapIndex(*k, reflect.Value{}) 95 | c.SetFlag(FlagDeleted) 96 | 97 | case CREATE: 98 | m.SetMapIndex(*k, *v) 99 | c.SetFlag(FlagCreated) 100 | 101 | case UPDATE: 102 | m.SetMapIndex(*k, *v) 103 | c.SetFlag(FlagUpdated) 104 | 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package diff 2 | 3 | // ConvertTypes enables values that are convertible to the target type to be converted when patching 4 | func ConvertCompatibleTypes() func(d *Differ) error { 5 | return func(d *Differ) error { 6 | d.ConvertCompatibleTypes = true 7 | return nil 8 | } 9 | } 10 | 11 | // FlattenEmbeddedStructs determines whether fields of embedded structs should behave as if they are directly under the parent 12 | func FlattenEmbeddedStructs() func(d *Differ) error { 13 | return func(d *Differ) error { 14 | d.FlattenEmbeddedStructs = true 15 | return nil 16 | } 17 | } 18 | 19 | // SliceOrdering determines whether the ordering of items in a slice results in a change 20 | func SliceOrdering(enabled bool) func(d *Differ) error { 21 | return func(d *Differ) error { 22 | d.SliceOrdering = enabled 23 | return nil 24 | } 25 | } 26 | 27 | // TagName sets the tag name to use when getting field names and options 28 | func TagName(tag string) func(d *Differ) error { 29 | return func(d *Differ) error { 30 | d.TagName = tag 31 | return nil 32 | } 33 | } 34 | 35 | // DisableStructValues disables populating a separate change for each item in a struct, 36 | // where the struct is being compared to a nil value 37 | func DisableStructValues() func(d *Differ) error { 38 | return func(d *Differ) error { 39 | d.DisableStructValues = true 40 | return nil 41 | } 42 | } 43 | 44 | // CustomValueDiffers allows you to register custom differs for specific types 45 | func CustomValueDiffers(vd ...ValueDiffer) func(d *Differ) error { 46 | return func(d *Differ) error { 47 | d.customValueDiffers = append(d.customValueDiffers, vd...) 48 | for k := range d.customValueDiffers { 49 | d.customValueDiffers[k].InsertParentDiffer(d.diff) 50 | } 51 | return nil 52 | } 53 | } 54 | 55 | // AllowTypeMismatch changed behaviour to report value as "updated" when its type has changed instead of error 56 | func AllowTypeMismatch(enabled bool) func(d *Differ) error { 57 | return func(d *Differ) error { 58 | d.AllowTypeMismatch = enabled 59 | return nil 60 | } 61 | } 62 | 63 | //StructMapKeySupport - Changelog paths do not provided structured object values for maps that contain complex 64 | //keys (such as other structs). You must enable this support via an option and it then uses msgpack to encode 65 | //path elements that are structs. If you don't have this on, and try to patch, your apply will fail for that 66 | //element. 67 | func StructMapKeySupport() func(d *Differ) error { 68 | return func(d *Differ) error { 69 | d.StructMapKeys = true 70 | return nil 71 | } 72 | } 73 | 74 | //DiscardComplexOrigin - by default, we are now keeping the complex struct associated with a create entry. 75 | //This allows us to fix the merge to new object issue of not having enough change log details when allocating 76 | //new objects. This however is a trade off of memory size and complexity vs correctness which is often only 77 | //necessary when embedding structs in slices and arrays. It memory constrained environments, it may be desirable 78 | //to turn this feature off however from a computational perspective, keeping the complex origin is actually quite 79 | //cheap so, make sure you're extremely clear on the pitfalls of turning this off prior to doing so. 80 | func DiscardComplexOrigin() func(d *Differ) error { 81 | return func(d *Differ) error { 82 | d.DiscardParent = true 83 | return nil 84 | } 85 | } 86 | 87 | // Filter allows you to determine which fields the differ descends into 88 | func Filter(f FilterFunc) func(d *Differ) error { 89 | return func(d *Differ) error { 90 | d.Filter = f 91 | return nil 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guidelines 2 | 3 | Looking to contribute something to this project? Here's how you can help: 4 | 5 | Please take a moment to review this document in order to make the contribution process easy and effective for everyone involved. 6 | 7 | Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue or assessing patches and features. 8 | 9 | We also have a [code of conduct](https://ernest.io/conduct). 10 | 11 | ## Using the issue tracker 12 | 13 | The issue tracker is the preferred channel for [bug reports](#bug-reports), [features requests](#feature-requests) and [submitting pull requests](#pull-requests), but please respect the following restrictions: 14 | 15 | * Please **do not** use the issue tracker for personal support requests. 16 | 17 | * Please **do not** derail issues. Keep the discussion on topic and 18 | respect the opinions of others. 19 | 20 | 21 | ## Bug reports 22 | 23 | A bug is a _demonstrable problem_ that is caused by the code in the repository. 24 | Good bug reports are extremely helpful - thank you! 25 | 26 | Guidelines for bug reports: 27 | 28 | 1. **Use the GitHub issue search** — check if the issue has already been 29 | reported. 30 | 31 | 2. **Check if the issue has been fixed** — try to reproduce it using the 32 | latest `master` or `develop` branch in the repository. 33 | 34 | 3. **Isolate the problem** — create a reduced test case and a live example. 35 | 36 | A good bug report shouldn't leave others needing to chase you up for more 37 | information. Please try to be as detailed as possible in your report. What is 38 | your environment? What steps will reproduce the issue? Which environment experience the problem? What would you expect to be the outcome? All these 39 | details will help people to fix any potential bugs. 40 | 41 | Example: 42 | 43 | > Short and descriptive example bug report title 44 | > 45 | > A summary of the issue and the environment in which it occurs. If 46 | > suitable, include the steps required to reproduce the bug. 47 | > 48 | > 1. This is the first step 49 | > 2. This is the second step 50 | > 3. Further steps, etc. 51 | > 52 | > `` - a link to the reduced test case 53 | > 54 | > Any other information you want to share that is relevant to the issue being 55 | > reported. This might include the lines of code that you have identified as 56 | > causing the bug, and potential solutions (and your opinions on their 57 | > merits). 58 | 59 | 60 | ## Feature requests 61 | 62 | Feature requests are welcome. But take a moment to find out whether your idea 63 | fits with the scope and aims of the project. It's up to *you* to make a strong 64 | case to convince the project's developers of the merits of this feature. Please 65 | provide as much detail and context as possible. 66 | 67 | 68 | ## Pull requests 69 | 70 | Good pull requests - patches, improvements, new features - are a fantastic 71 | help. They should remain focused in scope and avoid containing unrelated 72 | commits. 73 | 74 | [**Please ask first**](https://ernest.io/community) before embarking on any significant pull request (e.g. 75 | implementing features, refactoring code, porting to a different language), 76 | otherwise you risk spending a lot of time working on something that the 77 | project's developers might not want to merge into the project. 78 | 79 | Please adhere to the coding conventions used throughout a project (indentation, 80 | accurate comments, etc.) and any other requirements (such as test coverage). 81 | -------------------------------------------------------------------------------- /diff_slice.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | package diff 6 | 7 | import ( 8 | "reflect" 9 | ) 10 | 11 | func (d *Differ) diffSlice(path []string, a, b reflect.Value, parent interface{}) error { 12 | if a.Kind() == reflect.Invalid { 13 | d.cl.Add(CREATE, path, nil, exportInterface(b)) 14 | return nil 15 | } 16 | 17 | if b.Kind() == reflect.Invalid { 18 | d.cl.Add(DELETE, path, exportInterface(a), nil) 19 | return nil 20 | } 21 | 22 | if a.Kind() != b.Kind() { 23 | return ErrTypeMismatch 24 | } 25 | 26 | if d.comparative(a, b) { 27 | return d.diffSliceComparative(path, a, b) 28 | } 29 | 30 | return d.diffSliceGeneric(path, a, b) 31 | } 32 | 33 | func (d *Differ) diffSliceGeneric(path []string, a, b reflect.Value) error { 34 | missing := NewComparativeList() 35 | 36 | slice := sliceTracker{} 37 | for i := 0; i < a.Len(); i++ { 38 | ae := a.Index(i) 39 | 40 | if (d.SliceOrdering && !hasAtSameIndex(b, ae, i)) || (!d.SliceOrdering && !slice.has(b, ae, d)) { 41 | missing.addA(i, &ae) 42 | } 43 | } 44 | 45 | slice = sliceTracker{} 46 | for i := 0; i < b.Len(); i++ { 47 | be := b.Index(i) 48 | 49 | if (d.SliceOrdering && !hasAtSameIndex(a, be, i)) || (!d.SliceOrdering && !slice.has(a, be, d)) { 50 | missing.addB(i, &be) 51 | } 52 | } 53 | 54 | // fallback to comparing based on order in slice if item is missing 55 | if len(missing.keys) == 0 { 56 | return nil 57 | } 58 | 59 | return d.diffComparative(path, missing, exportInterface(a)) 60 | } 61 | 62 | func (d *Differ) diffSliceComparative(path []string, a, b reflect.Value) error { 63 | c := NewComparativeList() 64 | 65 | for i := 0; i < a.Len(); i++ { 66 | ae := a.Index(i) 67 | ak := getFinalValue(ae) 68 | 69 | id := identifier(d.TagName, ak) 70 | if id != nil { 71 | c.addA(id, &ae) 72 | } 73 | } 74 | 75 | for i := 0; i < b.Len(); i++ { 76 | be := b.Index(i) 77 | bk := getFinalValue(be) 78 | 79 | id := identifier(d.TagName, bk) 80 | if id != nil { 81 | c.addB(id, &be) 82 | } 83 | } 84 | 85 | return d.diffComparative(path, c, exportInterface(a)) 86 | } 87 | 88 | // keeps track of elements that have already been matched, to stop duplicate matches from occurring 89 | type sliceTracker []bool 90 | 91 | func (st *sliceTracker) has(s, v reflect.Value, d *Differ) bool { 92 | if len(*st) != s.Len() { 93 | (*st) = make([]bool, s.Len()) 94 | } 95 | 96 | for i := 0; i < s.Len(); i++ { 97 | // skip already matched elements 98 | if (*st)[i] { 99 | continue 100 | } 101 | 102 | x := s.Index(i) 103 | 104 | var nd Differ 105 | nd.Filter = d.Filter 106 | nd.customValueDiffers = d.customValueDiffers 107 | 108 | err := nd.diff([]string{}, x, v, nil) 109 | if err != nil { 110 | continue 111 | } 112 | 113 | if len(nd.cl) == 0 { 114 | (*st)[i] = true 115 | return true 116 | } 117 | } 118 | 119 | return false 120 | } 121 | 122 | func getFinalValue(t reflect.Value) reflect.Value { 123 | switch t.Kind() { 124 | case reflect.Interface: 125 | return getFinalValue(t.Elem()) 126 | case reflect.Ptr: 127 | return getFinalValue(reflect.Indirect(t)) 128 | default: 129 | return t 130 | } 131 | } 132 | 133 | func hasAtSameIndex(s, v reflect.Value, atIndex int) bool { 134 | // check the element in the slice at atIndex to see if it matches Value, if it is a valid index into the slice 135 | if atIndex < s.Len() { 136 | x := s.Index(atIndex) 137 | return reflect.DeepEqual(exportInterface(x), exportInterface(v)) 138 | } 139 | 140 | return false 141 | } 142 | -------------------------------------------------------------------------------- /change_value.go: -------------------------------------------------------------------------------- 1 | package diff 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | //ChangeValue is a specialized struct for monitoring patching 9 | type ChangeValue struct { 10 | parent *reflect.Value 11 | target *reflect.Value 12 | flags PatchFlags 13 | change *Change 14 | err error 15 | pos int 16 | index int 17 | key reflect.Value 18 | } 19 | 20 | //swap swaps out the target as we move down the path. Note that a nil 21 | // check is foregone here due to the fact we control usage. 22 | func (c *ChangeValue) swap(newTarget *reflect.Value) { 23 | if newTarget.IsValid() { 24 | c.ClearFlag(FlagInvalidTarget) 25 | c.parent = c.target 26 | c.target = newTarget 27 | c.pos++ 28 | } 29 | } 30 | 31 | // Sets a flag on the node and saves the change 32 | func (c *ChangeValue) SetFlag(flag PatchFlags) { 33 | if c != nil { 34 | c.flags = c.flags | flag 35 | } 36 | } 37 | 38 | //ClearFlag removes just a single flag 39 | func (c *ChangeValue) ClearFlag(flag PatchFlags) { 40 | if c != nil { 41 | c.flags = c.flags &^ flag 42 | } 43 | } 44 | 45 | //HasFlag indicates if a flag is set on the node. returns false if node is bad 46 | func (c *ChangeValue) HasFlag(flag PatchFlags) bool { 47 | return (c.flags & flag) != 0 48 | } 49 | 50 | //IsValid echo for is valid 51 | func (c *ChangeValue) IsValid() bool { 52 | if c != nil { 53 | return c.target.IsValid() || !c.HasFlag(FlagInvalidTarget) 54 | } 55 | return false 56 | } 57 | 58 | //ParentKind - helps keep us nil safe 59 | func (c ChangeValue) ParentKind() reflect.Kind { 60 | if c.parent != nil { 61 | return c.parent.Kind() 62 | } 63 | return reflect.Invalid 64 | } 65 | 66 | //ParentLen is a nil safe parent length check 67 | func (c ChangeValue) ParentLen() (ret int) { 68 | if c.parent != nil && 69 | (c.parent.Kind() == reflect.Slice || 70 | c.parent.Kind() == reflect.Map) { 71 | ret = c.parent.Len() 72 | } 73 | return 74 | } 75 | 76 | //ParentSet - nil safe parent set 77 | func (c *ChangeValue) ParentSet(value reflect.Value, convertCompatibleTypes bool) { 78 | if c != nil && c.parent != nil { 79 | defer func() { 80 | if r := recover(); r != nil { 81 | c.SetFlag(FlagParentSetFailed) 82 | } 83 | }() 84 | 85 | if convertCompatibleTypes { 86 | if !value.Type().ConvertibleTo(c.parent.Type()) { 87 | c.AddError(fmt.Errorf("Value of type %s is not convertible to %s", value.Type().String(), c.parent.Type().String())) 88 | c.SetFlag(FlagParentSetFailed) 89 | return 90 | } 91 | c.parent.Set(value.Convert(c.parent.Type())) 92 | } else { 93 | c.parent.Set(value) 94 | } 95 | c.SetFlag(FlagParentSetApplied) 96 | } 97 | } 98 | 99 | //Len echo for len 100 | func (c ChangeValue) Len() int { 101 | return c.target.Len() 102 | } 103 | 104 | //Set echos reflect set 105 | func (c *ChangeValue) Set(value reflect.Value, convertCompatibleTypes bool) { 106 | if c == nil { 107 | return 108 | } 109 | 110 | defer func() { 111 | if r := recover(); r != nil { 112 | switch e := r.(type) { 113 | case string: 114 | c.AddError(NewError(e)) 115 | case *reflect.ValueError: 116 | c.AddError(NewError(e.Error())) 117 | } 118 | 119 | c.SetFlag(FlagFailed) 120 | } 121 | }() 122 | 123 | if c.HasFlag(OptionImmutable) { 124 | c.SetFlag(FlagIgnored) 125 | return 126 | } 127 | 128 | if convertCompatibleTypes { 129 | if c.target.Kind() == reflect.Ptr && value.Kind() != reflect.Ptr { 130 | if !value.IsValid() { 131 | c.target.Set(reflect.Zero(c.target.Type())) 132 | c.SetFlag(FlagApplied) 133 | return 134 | } else if !value.Type().ConvertibleTo(c.target.Elem().Type()) { 135 | c.AddError(fmt.Errorf("Value of type %s is not convertible to %s", value.Type().String(), c.target.Type().String())) 136 | c.SetFlag(FlagFailed) 137 | return 138 | } 139 | 140 | tv := reflect.New(c.target.Elem().Type()) 141 | tv.Elem().Set(value.Convert(c.target.Elem().Type())) 142 | c.target.Set(tv) 143 | } else { 144 | if !value.Type().ConvertibleTo(c.target.Type()) { 145 | c.AddError(fmt.Errorf("Value of type %s is not convertible to %s", value.Type().String(), c.target.Type().String())) 146 | c.SetFlag(FlagFailed) 147 | return 148 | } 149 | 150 | c.target.Set(value.Convert(c.target.Type())) 151 | } 152 | } else { 153 | if value.IsValid() { 154 | if c.target.Kind() == reflect.Ptr && value.Kind() != reflect.Ptr { 155 | tv := reflect.New(value.Type()) 156 | tv.Elem().Set(value) 157 | c.target.Set(tv) 158 | } else { 159 | c.target.Set(value) 160 | } 161 | } else if c.target.Kind() == reflect.Ptr { 162 | c.target.Set(reflect.Zero(c.target.Type())) 163 | } else if !c.target.IsZero() { 164 | t := c.target.Elem() 165 | t.Set(reflect.Zero(t.Type())) 166 | } 167 | } 168 | c.SetFlag(FlagApplied) 169 | } 170 | 171 | //Index echo for index 172 | func (c ChangeValue) Index(i int) reflect.Value { 173 | return c.target.Index(i) 174 | } 175 | 176 | //ParentIndex - get us the parent version, nil safe 177 | func (c ChangeValue) ParentIndex(i int) (ret reflect.Value) { 178 | if c.parent != nil { 179 | ret = c.parent.Index(i) 180 | } 181 | return 182 | } 183 | 184 | //Instance a new element of type for target. Taking the 185 | //copy of the complex origin avoids the 'lack of data' issue 186 | //present when allocating complex structs with slices and 187 | //arrays 188 | func (c ChangeValue) NewElement() reflect.Value { 189 | ret := c.change.parent 190 | if ret != nil { 191 | return reflect.ValueOf(ret) 192 | } 193 | return reflect.New(c.target.Type().Elem()).Elem() 194 | } 195 | 196 | //NewArrayElement gives us a dynamically typed new element 197 | func (c ChangeValue) NewArrayElement() reflect.Value { 198 | c.target.Set(reflect.Append(*c.target, c.NewElement())) 199 | c.SetFlag(FlagCreated) 200 | return c.Index(c.Len() - 1) 201 | } 202 | 203 | //AddError appends errors to this change value 204 | func (c *ChangeValue) AddError(err error) *ChangeValue { 205 | if c != nil { 206 | c.err = err 207 | } 208 | return c 209 | } 210 | -------------------------------------------------------------------------------- /patch.go: -------------------------------------------------------------------------------- 1 | package diff 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | /** 8 | This is a method of applying a changelog to a value or struct. change logs 9 | should be generated with Diff and never manually created. This DOES NOT 10 | apply fuzzy logic as would be in the case of a text patch. It does however 11 | have a few additional features added to our struct tags. 12 | 13 | 1) create. This tag on a struct field indicates that the patch should 14 | create the value if it's not there. I.e. if it's nil. This works for 15 | pointers, maps and slices. 16 | 17 | 2) omitunequal. Generally, you don't want to do this, the expectation is 18 | that if an item isn't there, you want to add it. For example, if your 19 | diff shows an array element at index 6 is a string 'hello' but your target 20 | only has 3 elements, none of them matching... you want to add 'hello' 21 | regardless of the index. (think in a distributed context, another process 22 | may have deleted more than one entry and 'hello' may no longer be in that 23 | indexed spot. 24 | 25 | So given this scenario, the default behavior is to scan for the previous 26 | value and replace it anyway, or simply append the new value. For maps the 27 | default behavior is to simply add the key if it doesn't match. 28 | 29 | However, if you don't like the default behavior, and add the omitunequal 30 | tag to your struct, patch will *NOT* update an array or map with the key 31 | or array value unless they key or index contains a 'match' to the 32 | previous value. In which case it will skip over that change. 33 | 34 | Patch is implemented as a best effort algorithm. That means you can receive 35 | multiple nested errors and still successfully have a modified target. This 36 | may even be acceptable depending on your use case. So keep in mind, just 37 | because err != nil *DOESN'T* mean that the patch didn't accomplish your goal 38 | in setting those changes that are actually available. For example, you may 39 | diff two structs of the same type, then attempt to apply to an entirely 40 | different struct that is similar in constitution (think interface here) and 41 | you may in fact get all of the values populated you wished to anyway. 42 | */ 43 | 44 | //Not strictly necessary but might be nice in some cases 45 | //go:generate stringer -type=PatchFlags 46 | type PatchFlags uint32 47 | 48 | const ( 49 | OptionCreate PatchFlags = 1 << iota 50 | OptionNoCreate 51 | OptionOmitUnequal 52 | OptionImmutable 53 | FlagInvalidTarget 54 | FlagApplied 55 | FlagFailed 56 | FlagCreated 57 | FlagIgnored 58 | FlagDeleted 59 | FlagUpdated 60 | FlagParentSetApplied 61 | FlagParentSetFailed 62 | ) 63 | 64 | //PatchLogEntry defines how a DiffLog entry was applied 65 | type PatchLogEntry struct { 66 | Path []string `json:"path"` 67 | From interface{} `json:"from"` 68 | To interface{} `json:"to"` 69 | Flags PatchFlags `json:"flags"` 70 | Errors error `json:"errors"` 71 | } 72 | type PatchLog []PatchLogEntry 73 | 74 | //HasFlag - convenience function for users 75 | func (p PatchLogEntry) HasFlag(flag PatchFlags) bool { 76 | return (p.Flags & flag) != 0 77 | } 78 | 79 | //Applied - returns true if all change log entries were actually 80 | // applied, regardless of if any errors were encountered 81 | func (p PatchLog) Applied() bool { 82 | if p.HasErrors() { 83 | for _, ple := range p { 84 | if !ple.HasFlag(FlagApplied) { 85 | return false 86 | } 87 | } 88 | } 89 | return true 90 | } 91 | 92 | //HasErrors - indicates if a patch log contains any errors 93 | func (p PatchLog) HasErrors() (ret bool) { 94 | for _, ple := range p { 95 | if ple.Errors != nil { 96 | ret = true 97 | } 98 | } 99 | return 100 | } 101 | 102 | //ErrorCount -- counts the number of errors encountered while patching 103 | func (p PatchLog) ErrorCount() (ret uint) { 104 | for _, ple := range p { 105 | if ple.Errors != nil { 106 | ret++ 107 | } 108 | } 109 | return 110 | } 111 | 112 | func Merge(original interface{}, changed interface{}, target interface{}) (PatchLog, error) { 113 | d, _ := NewDiffer() 114 | return d.Merge(original, changed, target) 115 | } 116 | 117 | // Merge is a convenience function that diffs, the original and changed items 118 | // and merges said changes with target all in one call. 119 | func (d *Differ) Merge(original interface{}, changed interface{}, target interface{}) (PatchLog, error) { 120 | StructMapKeySupport()(d) // nolint: errcheck 121 | if cl, err := d.Diff(original, changed); err == nil { 122 | return Patch(cl, target), nil 123 | } else { 124 | return nil, err 125 | } 126 | } 127 | 128 | func Patch(cl Changelog, target interface{}) (ret PatchLog) { 129 | d, _ := NewDiffer() 130 | return d.Patch(cl, target) 131 | } 132 | 133 | //Patch... the missing feature. 134 | func (d *Differ) Patch(cl Changelog, target interface{}) (ret PatchLog) { 135 | for _, c := range cl { 136 | ret = append(ret, NewPatchLogEntry(NewChangeValue(d, c, target))) 137 | } 138 | return ret 139 | } 140 | 141 | //NewPatchLogEntry converts our complicated reflection based struct to 142 | //a simpler format for the consumer 143 | func NewPatchLogEntry(cv *ChangeValue) PatchLogEntry { 144 | return PatchLogEntry{ 145 | Path: cv.change.Path, 146 | From: cv.change.From, 147 | To: cv.change.To, 148 | Flags: cv.flags, 149 | Errors: cv.err, 150 | } 151 | } 152 | 153 | //NewChangeValue idiomatic constructor (also invokes render) 154 | func NewChangeValue(d *Differ, c Change, target interface{}) (ret *ChangeValue) { 155 | val := reflect.ValueOf(target) 156 | ret = &ChangeValue{ 157 | target: &val, 158 | change: &c, 159 | } 160 | d.renderChangeTarget(ret) 161 | return 162 | } 163 | 164 | //renderChangeValue applies 'path' in change to target. nil check is foregone 165 | // here as we control usage 166 | func (d *Differ) renderChangeTarget(c *ChangeValue) { 167 | //This particular change element may potentially have the immutable flag 168 | if c.HasFlag(OptionImmutable) { 169 | c.AddError(NewError("Option immutable set, cannot apply change")) 170 | return 171 | } //the we always set a failure, and only unset if we successfully render the element 172 | c.SetFlag(FlagInvalidTarget) 173 | 174 | //substitute and solve for t (path) 175 | switch c.target.Kind() { 176 | 177 | //path element that is a map 178 | case reflect.Map: 179 | //map elements are 'copies' and immutable so if we set the new value to the 180 | //map prior to editing the value, it will fail to stick. To fix this, we 181 | //defer the safe until the stack unwinds 182 | m, k, v := d.renderMap(c) 183 | defer d.updateMapEntry(c, m, k, v) 184 | 185 | //path element that is a slice 186 | case reflect.Slice: 187 | d.renderSlice(c) 188 | 189 | //walking a path means dealing with real elements 190 | case reflect.Interface, reflect.Ptr: 191 | if c.target.IsNil() { 192 | n := reflect.New(c.target.Type().Elem()) 193 | c.target.Set(n) 194 | c.target = &n 195 | d.renderChangeTarget(c) 196 | return 197 | } 198 | 199 | el := c.target.Elem() 200 | c.target = &el 201 | c.ClearFlag(FlagInvalidTarget) 202 | 203 | //path element that is a struct 204 | case reflect.Struct: 205 | d.patchStruct(c) 206 | } 207 | 208 | //if for some reason, rendering this element fails, c will no longer be valid 209 | //we are best effort though, so we keep on trucking 210 | if !c.IsValid() { 211 | c.AddError(NewErrorf("Unable to access path position %d. Target field is invalid", c.pos)) 212 | } 213 | 214 | //we've taken care of this path element, are there any more? if so, process 215 | //else, let's take some action 216 | if c.pos < len(c.change.Path) && !c.HasFlag(FlagInvalidTarget) { 217 | d.renderChangeTarget(c) 218 | 219 | } else { //we're at the end of the line... set the Value 220 | switch c.change.Type { 221 | case DELETE: 222 | switch c.ParentKind() { 223 | case reflect.Slice: 224 | d.deleteSliceEntry(c) 225 | case reflect.Struct: 226 | d.deleteStructEntry(c) 227 | default: 228 | c.SetFlag(FlagIgnored) 229 | } 230 | case UPDATE, CREATE: 231 | // this is generic because... we only deal in primitives here. AND 232 | // the diff format To field already contains the correct type. 233 | c.Set(reflect.ValueOf(c.change.To), d.ConvertCompatibleTypes) 234 | c.SetFlag(FlagUpdated) 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /diff.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | package diff 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "reflect" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/vmihailenco/msgpack/v5" 15 | ) 16 | 17 | const ( 18 | // CREATE represents when an element has been added 19 | CREATE = "create" 20 | // UPDATE represents when an element has been updated 21 | UPDATE = "update" 22 | // DELETE represents when an element has been removed 23 | DELETE = "delete" 24 | ) 25 | 26 | // DiffType represents an enum with all the supported diff types 27 | type DiffType uint8 28 | 29 | const ( 30 | UNSUPPORTED DiffType = iota 31 | STRUCT 32 | SLICE 33 | ARRAY 34 | STRING 35 | BOOL 36 | INT 37 | UINT 38 | FLOAT 39 | MAP 40 | PTR 41 | INTERFACE 42 | ) 43 | 44 | func (t DiffType) String() string { 45 | switch t { 46 | case STRUCT: 47 | return "STRUCT" 48 | case SLICE: 49 | return "SLICE" 50 | case ARRAY: 51 | return "ARRAY" 52 | case STRING: 53 | return "STRING" 54 | case BOOL: 55 | return "BOOL" 56 | case INT: 57 | return "INT" 58 | case UINT: 59 | return "UINT" 60 | case FLOAT: 61 | return "FLOAT" 62 | case MAP: 63 | return "MAP" 64 | case PTR: 65 | return "PTR" 66 | case INTERFACE: 67 | return "INTERFACE" 68 | default: 69 | return "UNSUPPORTED" 70 | } 71 | } 72 | 73 | // DiffFunc represents the built-in diff functions 74 | type DiffFunc func([]string, reflect.Value, reflect.Value, interface{}) error 75 | 76 | // Differ a configurable diff instance 77 | type Differ struct { 78 | TagName string 79 | SliceOrdering bool 80 | DisableStructValues bool 81 | customValueDiffers []ValueDiffer 82 | cl Changelog 83 | AllowTypeMismatch bool 84 | DiscardParent bool 85 | StructMapKeys bool 86 | FlattenEmbeddedStructs bool 87 | ConvertCompatibleTypes bool 88 | Filter FilterFunc 89 | } 90 | 91 | // Changelog stores a list of changed items 92 | type Changelog []Change 93 | 94 | // Change stores information about a changed item 95 | type Change struct { 96 | Type string `json:"type"` 97 | Path []string `json:"path"` 98 | From interface{} `json:"from"` 99 | To interface{} `json:"to"` 100 | parent interface{} `json:"parent"` 101 | } 102 | 103 | // ValueDiffer is an interface for custom differs 104 | type ValueDiffer interface { 105 | Match(a, b reflect.Value) bool 106 | Diff(dt DiffType, df DiffFunc, cl *Changelog, path []string, a, b reflect.Value, parent interface{}) error 107 | InsertParentDiffer(dfunc func(path []string, a, b reflect.Value, p interface{}) error) 108 | } 109 | 110 | // Changed returns true if both values differ 111 | func Changed(a, b interface{}) bool { 112 | cl, _ := Diff(a, b) 113 | return len(cl) > 0 114 | } 115 | 116 | // Diff returns a changelog of all mutated values from both 117 | func Diff(a, b interface{}, opts ...func(d *Differ) error) (Changelog, error) { 118 | d, err := NewDiffer(opts...) 119 | if err != nil { 120 | return nil, err 121 | } 122 | return d.Diff(a, b) 123 | } 124 | 125 | // NewDiffer creates a new configurable diffing object 126 | func NewDiffer(opts ...func(d *Differ) error) (*Differ, error) { 127 | d := Differ{ 128 | TagName: "diff", 129 | DiscardParent: false, 130 | } 131 | 132 | for _, opt := range opts { 133 | err := opt(&d) 134 | if err != nil { 135 | return nil, err 136 | } 137 | } 138 | 139 | return &d, nil 140 | } 141 | 142 | // FilterFunc is a function that determines whether to descend into a struct field. 143 | // parent is the struct being examined and field is a field on that struct. path 144 | // is the path to the field from the root of the diff. 145 | type FilterFunc func(path []string, parent reflect.Type, field reflect.StructField) bool 146 | 147 | // StructValues gets all values from a struct 148 | // values are stored as "created" or "deleted" entries in the changelog, 149 | // depending on the change type specified 150 | func StructValues(t string, path []string, s interface{}) (Changelog, error) { 151 | d := Differ{ 152 | TagName: "diff", 153 | DiscardParent: false, 154 | } 155 | 156 | v := reflect.ValueOf(s) 157 | 158 | return d.cl, d.structValues(t, path, v) 159 | } 160 | 161 | // FilterOut filter out the changes based on path. Paths may contain valid regexp to match items 162 | func (cl *Changelog) FilterOut(path []string) Changelog { 163 | var ncl Changelog 164 | 165 | for _, c := range *cl { 166 | if !pathmatch(path, c.Path) { 167 | ncl = append(ncl, c) 168 | } 169 | } 170 | 171 | return ncl 172 | } 173 | 174 | // Filter filter changes based on path. Paths may contain valid regexp to match items 175 | func (cl *Changelog) Filter(path []string) Changelog { 176 | var ncl Changelog 177 | 178 | for _, c := range *cl { 179 | if pathmatch(path, c.Path) { 180 | ncl = append(ncl, c) 181 | } 182 | } 183 | 184 | return ncl 185 | } 186 | 187 | func (d *Differ) getDiffType(a, b reflect.Value) (DiffType, DiffFunc) { 188 | switch { 189 | case are(a, b, reflect.Struct, reflect.Invalid): 190 | return STRUCT, d.diffStruct 191 | case are(a, b, reflect.Slice, reflect.Invalid): 192 | return SLICE, d.diffSlice 193 | case are(a, b, reflect.Array, reflect.Invalid): 194 | return ARRAY, d.diffSlice 195 | case are(a, b, reflect.String, reflect.Invalid): 196 | return STRING, d.diffString 197 | case are(a, b, reflect.Bool, reflect.Invalid): 198 | return BOOL, d.diffBool 199 | case are(a, b, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Invalid): 200 | return INT, d.diffInt 201 | case are(a, b, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Invalid): 202 | return UINT, d.diffUint 203 | case are(a, b, reflect.Float32, reflect.Float64, reflect.Invalid): 204 | return FLOAT, d.diffFloat 205 | case are(a, b, reflect.Map, reflect.Invalid): 206 | return MAP, d.diffMap 207 | case are(a, b, reflect.Ptr, reflect.Invalid): 208 | return PTR, d.diffPtr 209 | case are(a, b, reflect.Interface, reflect.Invalid): 210 | return INTERFACE, d.diffInterface 211 | default: 212 | return UNSUPPORTED, nil 213 | } 214 | } 215 | 216 | // Diff returns a changelog of all mutated values from both 217 | func (d *Differ) Diff(a, b interface{}) (Changelog, error) { 218 | // reset the state of the diff 219 | d.cl = Changelog{} 220 | 221 | return d.cl, d.diff([]string{}, reflect.ValueOf(a), reflect.ValueOf(b), nil) 222 | } 223 | 224 | func (d *Differ) diff(path []string, a, b reflect.Value, parent interface{}) error { 225 | 226 | //look and see if we need to discard the parent 227 | if parent != nil { 228 | if d.DiscardParent || reflect.TypeOf(parent).Kind() != reflect.Struct { 229 | parent = nil 230 | } 231 | } 232 | 233 | // check if types match or are 234 | if invalid(a, b) { 235 | if d.AllowTypeMismatch { 236 | d.cl.Add(UPDATE, path, a.Interface(), b.Interface()) 237 | return nil 238 | } 239 | return ErrTypeMismatch 240 | } 241 | 242 | // get the diff type and the corresponding built-int diff function to handle this type 243 | diffType, diffFunc := d.getDiffType(a, b) 244 | 245 | // first go through custom diff functions 246 | if len(d.customValueDiffers) > 0 { 247 | for _, vd := range d.customValueDiffers { 248 | if vd.Match(a, b) { 249 | err := vd.Diff(diffType, diffFunc, &d.cl, path, a, b, parent) 250 | if err != nil { 251 | return err 252 | } 253 | return nil 254 | } 255 | } 256 | } 257 | 258 | // then built-in diff functions 259 | if diffType == UNSUPPORTED { 260 | return errors.New("unsupported type: " + a.Kind().String()) 261 | } 262 | 263 | return diffFunc(path, a, b, parent) 264 | } 265 | 266 | func (cl *Changelog) Add(t string, path []string, ftco ...interface{}) { 267 | change := Change{ 268 | Type: t, 269 | Path: path, 270 | From: ftco[0], 271 | To: ftco[1], 272 | } 273 | if len(ftco) > 2 { 274 | change.parent = ftco[2] 275 | } 276 | (*cl) = append((*cl), change) 277 | } 278 | 279 | func tagName(tag string, f reflect.StructField) string { 280 | t := f.Tag.Get(tag) 281 | 282 | parts := strings.Split(t, ",") 283 | if len(parts) < 1 { 284 | return "-" 285 | } 286 | 287 | return parts[0] 288 | } 289 | 290 | func identifier(tag string, v reflect.Value) interface{} { 291 | if v.Kind() != reflect.Struct { 292 | return nil 293 | } 294 | 295 | for i := 0; i < v.NumField(); i++ { 296 | if hasTagOption(tag, v.Type().Field(i), "identifier") { 297 | return v.Field(i).Interface() 298 | } 299 | } 300 | 301 | return nil 302 | } 303 | 304 | func hasTagOption(tag string, f reflect.StructField, opt string) bool { 305 | parts := strings.Split(f.Tag.Get(tag), ",") 306 | if len(parts) < 2 { 307 | return false 308 | } 309 | 310 | for _, option := range parts[1:] { 311 | if option == opt { 312 | return true 313 | } 314 | } 315 | 316 | return false 317 | } 318 | 319 | func swapChange(t string, c Change) Change { 320 | nc := Change{ 321 | Type: t, 322 | Path: c.Path, 323 | } 324 | 325 | switch t { 326 | case CREATE: 327 | nc.To = c.To 328 | case DELETE: 329 | nc.From = c.To 330 | } 331 | 332 | return nc 333 | } 334 | 335 | func idComplex(v interface{}) string { 336 | switch v := v.(type) { 337 | case string: 338 | return v 339 | case int: 340 | return strconv.Itoa(v) 341 | default: 342 | b, err := msgpack.Marshal(v) 343 | if err != nil { 344 | panic(err) 345 | } 346 | return string(b) 347 | } 348 | 349 | } 350 | func idstring(v interface{}) string { 351 | switch v := v.(type) { 352 | case string: 353 | return v 354 | case int: 355 | return strconv.Itoa(v) 356 | default: 357 | return fmt.Sprint(v) 358 | } 359 | } 360 | 361 | func invalid(a, b reflect.Value) bool { 362 | if a.Kind() == b.Kind() { 363 | return false 364 | } 365 | 366 | if a.Kind() == reflect.Invalid { 367 | return false 368 | } 369 | if b.Kind() == reflect.Invalid { 370 | return false 371 | } 372 | 373 | return true 374 | } 375 | 376 | func are(a, b reflect.Value, kinds ...reflect.Kind) bool { 377 | var amatch, bmatch bool 378 | 379 | for _, k := range kinds { 380 | if a.Kind() == k { 381 | amatch = true 382 | } 383 | if b.Kind() == k { 384 | bmatch = true 385 | } 386 | } 387 | 388 | return amatch && bmatch 389 | } 390 | 391 | func AreType(a, b reflect.Value, types ...reflect.Type) bool { 392 | var amatch, bmatch bool 393 | 394 | for _, t := range types { 395 | if a.Kind() != reflect.Invalid { 396 | if a.Type() == t { 397 | amatch = true 398 | } 399 | } 400 | if b.Kind() != reflect.Invalid { 401 | if b.Type() == t { 402 | bmatch = true 403 | } 404 | } 405 | } 406 | 407 | return amatch && bmatch 408 | } 409 | 410 | func copyAppend(src []string, elems ...string) []string { 411 | dst := make([]string, len(src)+len(elems)) 412 | copy(dst, src) 413 | for i := len(src); i < len(src)+len(elems); i++ { 414 | dst[i] = elems[i-len(src)] 415 | } 416 | return dst 417 | } 418 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 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) 2 | 3 | A library for diffing golang structures and values. 4 | 5 | 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. 6 | 7 | NOTE: All active development now takes place on the v3 branch. 8 | 9 | ## Installation 10 | 11 | For version 3: 12 | ``` 13 | go get github.com/r3labs/diff/v3 14 | ``` 15 | 16 | ## Changelog Format 17 | 18 | When diffing two structures using `Diff`, a changelog will be produced. Any detected changes will populate the changelog array with a Change type: 19 | 20 | ```go 21 | type Change struct { 22 | Type string // The type of change detected; can be one of create, update or delete 23 | Path []string // The path of the detected change; will contain any field name or array index that was part of the traversal 24 | From interface{} // The original value that was present in the "from" structure 25 | To interface{} // The new value that was detected as a change in the "to" structure 26 | } 27 | ``` 28 | 29 | Given the example below, we are diffing two slices where the third element has been removed: 30 | 31 | ```go 32 | from := []int{1, 2, 3, 4} 33 | to := []int{1, 2, 4} 34 | 35 | changelog, _ := diff.Diff(from, to) 36 | ``` 37 | 38 | The resultant changelog should contain one change: 39 | 40 | ```go 41 | Change{ 42 | Type: "delete", 43 | Path: ["2"], 44 | From: 3, 45 | To: nil, 46 | } 47 | ``` 48 | 49 | ## Supported Types 50 | 51 | A diffable value can be/contain any of the following types: 52 | 53 | 54 | | Type | Supported | 55 | | ------------ | --------- | 56 | | struct | ✔ | 57 | | slice | ✔ | 58 | | string | ✔ | 59 | | int | ✔ | 60 | | bool | ✔ | 61 | | map | ✔ | 62 | | pointer | ✔ | 63 | | custom types | ✔ | 64 | 65 | 66 | Please see the docs for more supported types, options and features. 67 | 68 | ### Tags 69 | 70 | 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"`. 71 | 72 | | Tag | Usage | 73 | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 74 | | `-` | Excludes a value from being diffed | 75 | | `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"` | 76 | | `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. | 77 | | `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. | 78 | | `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. | 79 | 80 | ## Usage 81 | 82 | ### Basic Example 83 | 84 | 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. 85 | 86 | ```go 87 | import "github.com/r3labs/diff/v3" 88 | 89 | type Order struct { 90 | ID string `diff:"id"` 91 | Items []int `diff:"items"` 92 | } 93 | 94 | func main() { 95 | a := Order{ 96 | ID: "1234", 97 | Items: []int{1, 2, 3, 4}, 98 | } 99 | 100 | b := Order{ 101 | ID: "1234", 102 | Items: []int{1, 2, 4}, 103 | } 104 | 105 | changelog, err := diff.Diff(a, b) 106 | ... 107 | } 108 | ``` 109 | 110 | In this example, the output generated in the changelog will indicate that the third element with a value of '3' was removed from items. 111 | When marshalling the changelog to json, the output will look like: 112 | 113 | ```json 114 | [ 115 | { 116 | "type": "delete", 117 | "path": ["items", "2"], 118 | "from": 3, 119 | "to": null 120 | } 121 | ] 122 | ``` 123 | 124 | ### Options and Configuration 125 | 126 | Options can be set on the differ at call time which effect how diff acts when building the change log. 127 | ```go 128 | import "github.com/r3labs/diff/v3" 129 | 130 | type Order struct { 131 | ID string `diff:"id"` 132 | Items []int `diff:"items"` 133 | } 134 | 135 | func main() { 136 | a := Order{ 137 | ID: "1234", 138 | Items: []int{1, 2, 3, 4}, 139 | } 140 | 141 | b := Order{ 142 | ID: "1234", 143 | Items: []int{1, 2, 4}, 144 | } 145 | 146 | changelog, err := diff.Diff(a, b, diff.DisableStructValues(), diff.AllowTypeMismatch(true)) 147 | ... 148 | } 149 | ``` 150 | 151 | You can also create a new instance of a differ that allows options to be set. 152 | 153 | ```go 154 | import "github.com/r3labs/diff/v3" 155 | 156 | type Order struct { 157 | ID string `diff:"id"` 158 | Items []int `diff:"items"` 159 | } 160 | 161 | func main() { 162 | a := Order{ 163 | ID: "1234", 164 | Items: []int{1, 2, 3, 4}, 165 | } 166 | 167 | b := Order{ 168 | ID: "1234", 169 | Items: []int{1, 2, 4}, 170 | } 171 | 172 | d, err := diff.NewDiffer(diff.SliceOrdering(true)) 173 | if err != nil { 174 | panic(err) 175 | } 176 | 177 | changelog, err := d.Diff(a, b) 178 | ... 179 | } 180 | ``` 181 | 182 | Supported options are: 183 | 184 | `SliceOrdering` ensures that the ordering of items in a slice is taken into account 185 | 186 | `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. 187 | 188 | `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. 189 | 190 | `Filter` provides a callback that allows you to determine which fields the differ descends into 191 | 192 | `DisableStructValues` disables populating a separate change for each item in a struct, where the struct is being compared to a nil Value. 193 | 194 | `TagName` sets the tag name to use when getting field names and options. 195 | 196 | ### Patch and merge support 197 | Diff additionally supports merge and patch. Similar in concept to text patching / merging the Patch function, given 198 | a change log and a target instance will make a _best effort_ to apply the changes in the change log to the variable 199 | pointed to. The intention is that the target pointer is of the same type however, that doesn't necessarily have to be 200 | true. For example, two slices of differing structs may be similar enough to apply changes to in a polymorphic way, and 201 | patch will certainly try. 202 | 203 | The patch function doesn't actually fail, and even if there are errors, it may succeed sufficiently for the task at hand. 204 | To accommodate this patch keeps track of each change log option it attempts to apply and reports the details of what 205 | happened for further scrutiny. 206 | 207 | ```go 208 | import "github.com/r3labs/diff/v3" 209 | 210 | type Order struct { 211 | ID string `diff:"id"` 212 | Items []int `diff:"items"` 213 | } 214 | 215 | func main() { 216 | a := Order{ 217 | ID: "1234", 218 | Items: []int{1, 2, 3, 4}, 219 | } 220 | 221 | b := Order{ 222 | ID: "1234", 223 | Items: []int{1, 2, 4}, 224 | } 225 | 226 | c := Order{} 227 | changelog, err := diff.Diff(a, b) 228 | 229 | patchlog := diff.Patch(changelog, &c) 230 | //Note the lack of an error. Patch is best effort and uses flags to indicate actions taken 231 | //and keeps any errors encountered along the way for review 232 | fmt.Printf("Encountered %d errors while patching", patchlog.ErrorCount()) 233 | ... 234 | } 235 | ``` 236 | 237 | Instances of differ with options set can also be used when patching. 238 | 239 | ```go 240 | package main 241 | 242 | import "github.com/r3labs/diff/v3" 243 | 244 | type Order struct { 245 | ID string `json:"id"` 246 | Items []int `json:"items"` 247 | } 248 | 249 | func main() { 250 | a := Order{ 251 | ID: "1234", 252 | Items: []int{1, 2, 3, 4}, 253 | } 254 | 255 | b := Order{ 256 | ID: "1234", 257 | Items: []int{1, 2, 4}, 258 | } 259 | 260 | d, _ := diff.NewDiffer(diff.TagName("json")) 261 | 262 | changelog, _ := d.Diff(a, b) 263 | 264 | d.Patch(changelog, &a) 265 | // reflect.DeepEqual(a, b) == true 266 | } 267 | 268 | ``` 269 | 270 | As a convenience, there is a Merge function that allows one to take three interfaces and perform all the tasks at the same 271 | time. 272 | 273 | ```go 274 | import "github.com/r3labs/diff/v3" 275 | 276 | type Order struct { 277 | ID string `diff:"id"` 278 | Items []int `diff:"items"` 279 | } 280 | 281 | func main() { 282 | a := Order{ 283 | ID: "1234", 284 | Items: []int{1, 2, 3, 4}, 285 | } 286 | 287 | b := Order{ 288 | ID: "1234", 289 | Items: []int{1, 2, 4}, 290 | } 291 | 292 | c := Order{} 293 | patchlog, err := diff.Merge(a, b, &c) 294 | if err != nil { 295 | fmt.Printf("Error encountered while diffing a & b") 296 | } 297 | fmt.Printf("Encountered %d errors while patching", patchlog.ErrorCount()) 298 | ... 299 | } 300 | ``` 301 | ## Running Tests 302 | 303 | ``` 304 | make test 305 | ``` 306 | 307 | ## Contributing 308 | 309 | Please read through our 310 | [contributing guidelines](CONTRIBUTING.md). 311 | Included are directions for opening issues, coding standards, and notes on 312 | development. 313 | 314 | Moreover, if your pull request contains patches or features, you must include 315 | relevant unit tests. 316 | 317 | ## Versioning 318 | 319 | For transparency into our release cycle and in striving to maintain backward 320 | compatibility, this project is maintained under [the Semantic Versioning guidelines](http://semver.org/). 321 | 322 | ## Copyright and License 323 | 324 | Code and documentation copyright since 2015 r3labs.io authors. 325 | 326 | Code released under 327 | [the Mozilla Public License Version 2.0](LICENSE). 328 | -------------------------------------------------------------------------------- /patch_test.go: -------------------------------------------------------------------------------- 1 | package diff_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | 8 | "github.com/r3labs/diff/v3" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestPatch(t *testing.T) { 14 | cases := []struct { 15 | Name string 16 | A, B interface{} 17 | Changelog diff.Changelog 18 | }{ 19 | { 20 | "uint-slice-insert", &[]uint{1, 2, 3}, &[]uint{1, 2, 3, 4}, 21 | diff.Changelog{ 22 | diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: uint(4)}, 23 | }, 24 | }, 25 | { 26 | "int-slice-insert", &[]int{1, 2, 3}, &[]int{1, 2, 3, 4}, 27 | diff.Changelog{ 28 | diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: 4}, 29 | }, 30 | }, 31 | { 32 | "uint-slice-delete", &[]uint{1, 2, 3}, &[]uint{1, 3}, 33 | diff.Changelog{ 34 | diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: uint(2)}, 35 | }, 36 | }, 37 | { 38 | "int-slice-delete", &[]int{1, 2, 3}, &[]int{1, 3}, 39 | diff.Changelog{ 40 | diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: 2}, 41 | }, 42 | }, 43 | { 44 | "uint-slice-insert-delete", &[]uint{1, 2, 3}, &[]uint{1, 3, 4}, 45 | diff.Changelog{ 46 | diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: uint(2)}, 47 | diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: uint(4)}, 48 | }, 49 | }, 50 | { 51 | "int-slice-insert-delete", &[]int{1, 2, 3}, &[]int{1, 3, 4}, 52 | diff.Changelog{ 53 | diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: 2}, 54 | diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: 4}, 55 | }, 56 | }, 57 | { 58 | "string-slice-insert", &[]string{"1", "2", "3"}, &[]string{"1", "2", "3", "4"}, 59 | diff.Changelog{ 60 | diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: "4"}, 61 | }, 62 | }, 63 | { 64 | "string-slice-delete", &[]string{"1", "2", "3"}, &[]string{"1", "3"}, 65 | diff.Changelog{ 66 | diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"}, 67 | }, 68 | }, 69 | { 70 | "string-slice-insert-delete", &[]string{"1", "2", "3"}, &[]string{"1", "3", "4"}, 71 | diff.Changelog{ 72 | diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"}, 73 | diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: "4"}, 74 | }, 75 | }, 76 | { 77 | "comparable-slice-update", &[]tistruct{{"one", 1}, {"two", 2}}, &[]tistruct{{"one", 1}, {"two", 50}}, 78 | diff.Changelog{ 79 | diff.Change{Type: diff.UPDATE, Path: []string{"two", "value"}, From: 1, To: 50}, 80 | }, 81 | }, 82 | { 83 | "struct-string-update", &tstruct{Name: "one"}, &tstruct{Name: "two"}, 84 | diff.Changelog{ 85 | diff.Change{Type: diff.UPDATE, Path: []string{"name"}, From: "one", To: "two"}, 86 | }, 87 | }, 88 | { 89 | "struct-int-update", &tstruct{Value: 1}, &tstruct{Value: 50}, 90 | diff.Changelog{ 91 | diff.Change{Type: diff.UPDATE, Path: []string{"value"}, From: 1, To: 50}, 92 | }, 93 | }, 94 | { 95 | "struct-bool-update", &tstruct{Bool: true}, &tstruct{Bool: false}, 96 | diff.Changelog{ 97 | diff.Change{Type: diff.UPDATE, Path: []string{"bool"}, From: true, To: false}, 98 | }, 99 | }, 100 | { 101 | "struct-time-update", &tstruct{}, &tstruct{Time: currentTime}, 102 | diff.Changelog{ 103 | diff.Change{Type: diff.UPDATE, Path: []string{"time"}, From: time.Time{}, To: currentTime}, 104 | }, 105 | }, 106 | { 107 | "struct-nil-string-pointer-update", &tstruct{Pointer: nil}, &tstruct{Pointer: sptr("test")}, 108 | diff.Changelog{ 109 | diff.Change{Type: diff.UPDATE, Path: []string{"pointer"}, From: nil, To: sptr("test")}, 110 | }, 111 | }, 112 | { 113 | "struct-string-pointer-update-to-nil", &tstruct{Pointer: sptr("test")}, &tstruct{Pointer: nil}, 114 | diff.Changelog{ 115 | diff.Change{Type: diff.UPDATE, Path: []string{"pointer"}, From: sptr("test"), To: nil}, 116 | }, 117 | }, { 118 | "struct-generic-slice-insert", &tstruct{Values: []string{"one"}}, &tstruct{Values: []string{"one", "two"}}, 119 | diff.Changelog{ 120 | diff.Change{Type: diff.CREATE, Path: []string{"values", "1"}, From: nil, To: "two"}, 121 | }, 122 | }, 123 | { 124 | "struct-generic-slice-delete", &tstruct{Values: []string{"one", "two"}}, &tstruct{Values: []string{"one"}}, 125 | diff.Changelog{ 126 | diff.Change{Type: diff.DELETE, Path: []string{"values", "1"}, From: "two", To: nil}, 127 | }, 128 | }, 129 | { 130 | "struct-unidentifiable-slice-insert-delete", &tstruct{Unidentifiables: []tuistruct{{1}, {2}, {3}}}, &tstruct{Unidentifiables: []tuistruct{{5}, {2}, {3}, {4}}}, 131 | diff.Changelog{ 132 | diff.Change{Type: diff.UPDATE, Path: []string{"unidentifiables", "0", "value"}, From: 1, To: 5}, 133 | diff.Change{Type: diff.CREATE, Path: []string{"unidentifiables", "3", "value"}, From: nil, To: 4}, 134 | }, 135 | }, 136 | { 137 | "slice", &tstruct{}, &tstruct{Nested: tnstruct{Slice: []tmstruct{{"one", 1}, {"two", 2}}}}, 138 | diff.Changelog{ 139 | diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "0", "foo"}, From: nil, To: "one"}, 140 | diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "0", "bar"}, From: nil, To: 1}, 141 | diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "1", "foo"}, From: nil, To: "two"}, 142 | diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "1", "bar"}, From: nil, To: 2}, 143 | }, 144 | }, 145 | { 146 | "slice-duplicate-items", &[]int{1}, &[]int{1, 1}, 147 | diff.Changelog{ 148 | diff.Change{Type: diff.CREATE, Path: []string{"1"}, From: nil, To: 1}, 149 | }, 150 | }, 151 | { 152 | "embedded-struct-field", 153 | &embedstruct{Embedded{Foo: "a", Bar: 2}, true}, 154 | &embedstruct{Embedded{Foo: "b", Bar: 3}, false}, 155 | diff.Changelog{ 156 | diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: "a", To: "b"}, 157 | diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: 2, To: 3}, 158 | diff.Change{Type: diff.UPDATE, Path: []string{"baz"}, From: true, To: false}, 159 | }, 160 | }, 161 | { 162 | "custom-tags", 163 | &customTagStruct{Foo: "abc", Bar: 3}, 164 | &customTagStruct{Foo: "def", Bar: 4}, 165 | diff.Changelog{ 166 | diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: "abc", To: "def"}, 167 | diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: 3, To: 4}, 168 | }, 169 | }, 170 | { 171 | "custom-types", 172 | &customTypeStruct{Foo: "a", Bar: 1}, 173 | &customTypeStruct{Foo: "b", Bar: 2}, 174 | diff.Changelog{ 175 | diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: CustomStringType("a"), To: CustomStringType("b")}, 176 | diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: CustomIntType(1), To: CustomIntType(2)}, 177 | }, 178 | }, 179 | { 180 | "map", 181 | map[string]interface{}{"1": "one", "3": "three"}, 182 | map[string]interface{}{"2": "two", "3": "tres"}, 183 | diff.Changelog{ 184 | diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "one", To: nil}, 185 | diff.Change{Type: diff.CREATE, Path: []string{"2"}, From: nil, To: "two"}, 186 | diff.Change{Type: diff.UPDATE, Path: []string{"3"}, From: "three", To: "tres"}, 187 | }, 188 | }, 189 | { 190 | "map-nested-create", 191 | map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active", "attributes": map[string]interface{}{"attrA": "A", "attrB": "B"}}}, 192 | 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"}}}, 193 | diff.Changelog{ 194 | diff.Change{Type: "create", Path: []string{"details", "secondary-attributes"}, From: nil, To: map[string]interface{}{"attrA": "A", "attrB": "B"}}, 195 | }, 196 | }, 197 | { 198 | "map-nested-update", 199 | map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active", "attributes": map[string]interface{}{"attrA": "A", "attrB": "B"}}}, 200 | map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active", "attributes": map[string]interface{}{"attrA": "C", "attrD": "X"}}}, 201 | diff.Changelog{ 202 | diff.Change{Type: "update", Path: []string{"details", "attributes"}, From: map[string]interface{}{"attrA": "A", "attrB": "B"}, To: map[string]interface{}{"attrA": "C", "attrD": "X"}}, 203 | }, 204 | }, 205 | { 206 | "map-nested-delete", 207 | map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active", "attributes": map[string]interface{}{"attrA": "A", "attrB": "B"}}}, 208 | map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active"}}, 209 | diff.Changelog{ 210 | diff.Change{Type: "delete", Path: []string{"details", "attributes"}, From: map[string]interface{}{"attrA": "A", "attrB": "B"}, To: nil}, 211 | }, 212 | }, 213 | } 214 | 215 | for _, tc := range cases { 216 | t.Run(tc.Name, func(t *testing.T) { 217 | 218 | var options []func(d *diff.Differ) error 219 | switch tc.Name { 220 | case "mixed-slice-map", "nil-map", "map-nil": 221 | options = append(options, diff.StructMapKeySupport()) 222 | case "embedded-struct-field": 223 | options = append(options, diff.FlattenEmbeddedStructs()) 224 | case "custom-tags": 225 | options = append(options, diff.TagName("json")) 226 | } 227 | d, err := diff.NewDiffer(options...) 228 | if err != nil { 229 | panic(err) 230 | } 231 | pl := d.Patch(tc.Changelog, tc.A) 232 | 233 | assert.Equal(t, tc.B, tc.A) 234 | require.Equal(t, len(tc.Changelog), len(pl)) 235 | assert.False(t, pl.HasErrors()) 236 | }) 237 | } 238 | 239 | t.Run("convert-types", func(t *testing.T) { 240 | a := &tmstruct{Foo: "a", Bar: 1} 241 | b := &customTypeStruct{Foo: "b", Bar: 2} 242 | cl := diff.Changelog{ 243 | diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: CustomStringType("a"), To: CustomStringType("b")}, 244 | diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: CustomIntType(1), To: CustomIntType(2)}, 245 | } 246 | 247 | d, err := diff.NewDiffer() 248 | if err != nil { 249 | panic(err) 250 | } 251 | pl := d.Patch(cl, a) 252 | 253 | assert.True(t, pl.HasErrors()) 254 | 255 | d, err = diff.NewDiffer(diff.ConvertCompatibleTypes()) 256 | if err != nil { 257 | panic(err) 258 | } 259 | pl = d.Patch(cl, a) 260 | 261 | assert.False(t, pl.HasErrors()) 262 | assert.Equal(t, string(b.Foo), a.Foo) 263 | assert.Equal(t, int(b.Bar), a.Bar) 264 | require.Equal(t, len(cl), len(pl)) 265 | }) 266 | 267 | t.Run("pointer", func(t *testing.T) { 268 | type tps struct { 269 | S *string 270 | } 271 | 272 | str1 := "before" 273 | str2 := "after" 274 | 275 | t1 := tps{S: &str1} 276 | t2 := tps{S: &str2} 277 | 278 | changelog, err := diff.Diff(t1, t2) 279 | assert.NoError(t, err) 280 | 281 | patchLog := diff.Patch(changelog, &t1) 282 | assert.False(t, patchLog.HasErrors()) 283 | }) 284 | 285 | t.Run("map-to-pointer", func(t *testing.T) { 286 | t1 := make(map[string]*tmstruct) 287 | t2 := map[string]*tmstruct{"after": {Foo: "val"}} 288 | 289 | changelog, err := diff.Diff(t1, t2) 290 | assert.NoError(t, err) 291 | 292 | patchLog := diff.Patch(changelog, &t1) 293 | assert.False(t, patchLog.HasErrors()) 294 | 295 | assert.True(t, len(t2) == len(t1)) 296 | assert.Equal(t, t1["after"], &tmstruct{Foo: "val"}) 297 | }) 298 | 299 | t.Run("map-to-nil-pointer", func(t *testing.T) { 300 | t1 := make(map[string]*tmstruct) 301 | t2 := map[string]*tmstruct{"after": nil} 302 | 303 | changelog, err := diff.Diff(t1, t2) 304 | assert.NoError(t, err) 305 | 306 | patchLog := diff.Patch(changelog, &t1) 307 | assert.False(t, patchLog.HasErrors()) 308 | 309 | assert.Equal(t, len(t2), len(t1)) 310 | assert.Nil(t, t1["after"]) 311 | }) 312 | 313 | t.Run("pointer-with-converted-type", func(t *testing.T) { 314 | type tps struct { 315 | S *int 316 | } 317 | 318 | val1 := 1 319 | val2 := 2 320 | 321 | t1 := tps{S: &val1} 322 | t2 := tps{S: &val2} 323 | 324 | changelog, err := diff.Diff(t1, t2) 325 | assert.NoError(t, err) 326 | 327 | js, err := json.Marshal(changelog) 328 | assert.NoError(t, err) 329 | 330 | assert.NoError(t, json.Unmarshal(js, &changelog)) 331 | 332 | d, err := diff.NewDiffer(diff.ConvertCompatibleTypes()) 333 | assert.NoError(t, err) 334 | 335 | assert.Equal(t, 1, *t1.S) 336 | 337 | patchLog := d.Patch(changelog, &t1) 338 | assert.False(t, patchLog.HasErrors()) 339 | assert.Equal(t, 2, *t1.S) 340 | 341 | // test nil pointer 342 | t1 = tps{S: &val1} 343 | t2 = tps{S: nil} 344 | 345 | changelog, err = diff.Diff(t1, t2) 346 | assert.NoError(t, err) 347 | 348 | patchLog = d.Patch(changelog, &t1) 349 | assert.False(t, patchLog.HasErrors()) 350 | }) 351 | } 352 | -------------------------------------------------------------------------------- /diff_examples_test.go: -------------------------------------------------------------------------------- 1 | package diff_test 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | "reflect" 7 | 8 | "github.com/r3labs/diff/v3" 9 | ) 10 | 11 | //Try to do a bunch of stuff that will result in some or all failures 12 | //when trying to apply either a valid or invalid changelog 13 | func ExamplePatchWithErrors() { 14 | 15 | type Fruit struct { 16 | ID int `diff:"ID" json:"Identifier"` 17 | Name string `diff:"name"` 18 | Healthy bool `diff:"-"` 19 | Nutrients []string `diff:"nutrients"` 20 | Labels map[string]int `diff:"labs"` 21 | } 22 | 23 | type Bat struct { 24 | ID string `diff:"ID"` 25 | Name string `diff:"-"` 26 | } 27 | 28 | a := Fruit{ 29 | ID: 1, 30 | Name: "Green Apple", 31 | Healthy: true, 32 | Nutrients: []string{ 33 | "vitamin a", 34 | "vitamin b", 35 | "vitamin c", 36 | "vitamin d", 37 | }, 38 | Labels: make(map[string]int), 39 | } 40 | a.Labels["likes"] = 10 41 | a.Labels["colors"] = 2 42 | 43 | b := Fruit{ 44 | ID: 2, 45 | Name: "Red Apple", 46 | Healthy: true, 47 | Nutrients: []string{ 48 | "vitamin c", 49 | "vitamin d", 50 | "vitamin e", 51 | }, 52 | Labels: make(map[string]int), 53 | } 54 | b.Labels["forests"] = 1223 55 | b.Labels["colors"] = 1222 56 | 57 | c := Fruit{ 58 | Labels: make(map[string]int), 59 | Nutrients: []string{ 60 | "vitamin c", 61 | "vitamin d", 62 | "vitamin a", 63 | }, 64 | } 65 | c.Labels["likes"] = 21 66 | c.Labels["colors"] = 42 67 | 68 | d := Bat{ 69 | ID: "first", 70 | Name: "second", 71 | } 72 | 73 | changelog, err := diff.Diff(a, b) 74 | if err != nil { 75 | panic(err) 76 | } 77 | 78 | //This fails in total because c is not assignable (passed by Value) 79 | patchLog := diff.Patch(changelog, c) 80 | 81 | //this also demonstrated the nested errors with 'next' 82 | 83 | errors := patchLog[0].Errors.(*diff.DiffError) 84 | 85 | //we can also continue to nest errors if we like 86 | message := errors.WithCause(diff.NewError("This is a custom message")). 87 | WithCause(fmt.Errorf("this is an error from somewhere else but still compatible")). 88 | Error() 89 | 90 | //invoke a few failures, i.e. bad changelog 91 | changelog[2].Path[1] = "bad index" 92 | changelog[3].Path[0] = "bad struct field" 93 | 94 | patchLog = diff.Patch(changelog, &c) 95 | 96 | patchLog, _ = diff.Merge(a, nil, &c) 97 | 98 | patchLog, _ = diff.Merge(a, d, &c) 99 | 100 | //try patching a string 101 | patchLog = diff.Patch(changelog, message) 102 | 103 | //test an invalid change Value 104 | var bad *diff.ChangeValue 105 | if bad.IsValid() { 106 | fmt.Print("this should never happen") 107 | } 108 | 109 | //Output: 110 | } 111 | 112 | //ExampleMerge demonstrates how to use the Merge function 113 | func ExampleMerge() { 114 | type Fruit struct { 115 | ID int `diff:"ID" json:"Identifier"` 116 | Name string `diff:"name"` 117 | Healthy bool `diff:"healthy"` 118 | Nutrients []string `diff:"nutrients,create,omitunequal"` 119 | Labels map[string]int `diff:"labs,create"` 120 | } 121 | 122 | a := Fruit{ 123 | ID: 1, 124 | Name: "Green Apple", 125 | Healthy: true, 126 | Nutrients: []string{ 127 | "vitamin a", 128 | "vitamin b", 129 | "vitamin c", 130 | "vitamin d", 131 | }, 132 | Labels: make(map[string]int), 133 | } 134 | a.Labels["likes"] = 10 135 | a.Labels["colors"] = 2 136 | 137 | b := Fruit{ 138 | ID: 2, 139 | Name: "Red Apple", 140 | Healthy: true, 141 | Nutrients: []string{ 142 | "vitamin c", 143 | "vitamin d", 144 | "vitamin e", 145 | }, 146 | Labels: make(map[string]int), 147 | } 148 | b.Labels["forests"] = 1223 149 | b.Labels["colors"] = 1222 150 | 151 | c := Fruit{ 152 | Labels: make(map[string]int), 153 | Nutrients: []string{ 154 | "vitamin a", 155 | "vitamin c", 156 | "vitamin d", 157 | }, 158 | } 159 | c.Labels["likes"] = 21 160 | c.Labels["colors"] = 42 161 | 162 | //the only error that can happen here comes from the diff step 163 | patchLog, _ := diff.Merge(a, b, &c) 164 | 165 | //Note that unlike our patch version we've not included 'create' in the 166 | //tag for nutrients. This will omit "vitamin e" from ending up in c 167 | fmt.Printf("%#v", len(patchLog)) 168 | 169 | //Output: 8 170 | } 171 | 172 | //ExamplePrimitiveSlice demonstrates working with arrays and primitive values 173 | func ExamplePrimitiveSlice() { 174 | sla := []string{ 175 | "this", 176 | "is", 177 | "a", 178 | "simple", 179 | } 180 | 181 | slb := []string{ 182 | "slice", 183 | "That", 184 | "can", 185 | "be", 186 | "diff'ed", 187 | } 188 | 189 | slc := []string{ 190 | "ok", 191 | } 192 | 193 | patch, err := diff.Diff(sla, slb, diff.StructMapKeySupport()) 194 | if err != nil { 195 | fmt.Print("failed to diff sla and slb") 196 | } 197 | cl := diff.Patch(patch, &slc) 198 | 199 | //now the other way, round 200 | sla = []string{ 201 | "slice", 202 | "That", 203 | "can", 204 | "be", 205 | "diff'ed", 206 | } 207 | slb = []string{ 208 | "this", 209 | "is", 210 | "a", 211 | "simple", 212 | } 213 | 214 | patch, err = diff.Diff(sla, slb) 215 | if err != nil { 216 | fmt.Print("failed to diff sla and slb") 217 | } 218 | cl = diff.Patch(patch, &slc) 219 | 220 | //and finally a clean view 221 | sla = []string{ 222 | "slice", 223 | "That", 224 | "can", 225 | "be", 226 | "diff'ed", 227 | } 228 | slb = []string{} 229 | 230 | patch, err = diff.Diff(sla, slb) 231 | if err != nil { 232 | fmt.Print("failed to diff sla and slb") 233 | } 234 | cl = diff.Patch(patch, &slc) 235 | 236 | fmt.Printf("%d changes made to string array; %v", len(cl), slc) 237 | 238 | //Output: 5 changes made to string array; [simple a] 239 | } 240 | 241 | //ExampleComplexMapPatch demonstrates how to use the Patch function for complex slices 242 | //NOTE: There is a potential pitfall here, take a close look at b[2]. If patching the 243 | // original, the operation will work intuitively however, in a merge situation we 244 | // may not get everything we expect because it's a true diff between a and b and 245 | // the diff log will not contain enough information to fully recreate b from an 246 | // empty slice. This is exemplified in that the test "colors" is dropped in element 247 | // 3 of c. Change "colors" to "color" and see what happens. Keep in mind this only 248 | // happens when we need to allocate a new complex element. In normal operations we 249 | // fix for this by keeping a copy of said element in the diff log (as parent) and 250 | // allocate such an element as a whole copy prior to applying any updates? 251 | // 252 | // The new default is to carry this information forward, we invoke this pitfall 253 | // by creating such a situation and explicitly telling diff to discard the parent 254 | // In memory constrained environments if the developer is careful, they can use 255 | // the discard feature but unless you REALLY understand what's happening here, use 256 | // the default. 257 | func ExampleComplexSlicePatch() { 258 | 259 | type Content struct { 260 | Text string `diff:",create"` 261 | Number int `diff:",create"` 262 | } 263 | type Attributes struct { 264 | Labels []Content `diff:",create"` 265 | } 266 | 267 | a := Attributes{ 268 | Labels: []Content{ 269 | { 270 | Text: "likes", 271 | Number: 10, 272 | }, 273 | { 274 | Text: "forests", 275 | Number: 10, 276 | }, 277 | { 278 | Text: "colors", 279 | Number: 2, 280 | }, 281 | }, 282 | } 283 | 284 | b := Attributes{ 285 | Labels: []Content{ 286 | { 287 | Text: "forests", 288 | Number: 14, 289 | }, 290 | { 291 | Text: "location", 292 | Number: 0x32, 293 | }, 294 | { 295 | Text: "colors", 296 | Number: 1222, 297 | }, 298 | { 299 | Text: "trees", 300 | Number: 34, 301 | }, 302 | }, 303 | } 304 | c := Attributes{} 305 | 306 | changelog, err := diff.Diff(a, b, diff.DiscardComplexOrigin(), diff.StructMapKeySupport()) 307 | if err != nil { 308 | panic(err) 309 | } 310 | 311 | patchLog := diff.Patch(changelog, &c) 312 | 313 | fmt.Printf("Patched %d entries and encountered %d errors", len(patchLog), patchLog.ErrorCount()) 314 | 315 | //Output: Patched 7 entries and encountered 3 errors 316 | } 317 | 318 | //ExampleComplexMapPatch demonstrates how to use the Patch function for complex slices. 319 | func ExampleComplexMapPatch() { 320 | 321 | type Key struct { 322 | Value string 323 | weight int 324 | } 325 | type Content struct { 326 | Text string 327 | Number float64 328 | WholeNumber int 329 | } 330 | type Attributes struct { 331 | Labels map[Key]Content 332 | } 333 | 334 | a := Attributes{ 335 | Labels: make(map[Key]Content), 336 | } 337 | a.Labels[Key{Value: "likes"}] = Content{ 338 | WholeNumber: 10, 339 | Number: 23.4, 340 | } 341 | 342 | a.Labels[Key{Value: "colors"}] = Content{ 343 | WholeNumber: 2, 344 | } 345 | 346 | b := Attributes{ 347 | Labels: make(map[Key]Content), 348 | } 349 | b.Labels[Key{Value: "forests"}] = Content{ 350 | Text: "Sherwood", 351 | } 352 | b.Labels[Key{Value: "colors"}] = Content{ 353 | Number: 1222, 354 | } 355 | b.Labels[Key{Value: "latitude"}] = Content{ 356 | Number: 38.978797, 357 | } 358 | b.Labels[Key{Value: "longitude"}] = Content{ 359 | Number: -76.490986, 360 | } 361 | 362 | //c := Attributes{} 363 | c := Attributes{ 364 | Labels: make(map[Key]Content), 365 | } 366 | c.Labels[Key{Value: "likes"}] = Content{ 367 | WholeNumber: 210, 368 | Number: 23.4453, 369 | } 370 | 371 | changelog, err := diff.Diff(a, b) 372 | if err != nil { 373 | panic(err) 374 | } 375 | 376 | patchLog := diff.Patch(changelog, &c) 377 | 378 | fmt.Printf("%#v", len(patchLog)) 379 | 380 | //Output: 7 381 | } 382 | 383 | //ExamplePatch demonstrates how to use the Patch function 384 | func ExamplePatch() { 385 | 386 | type Key struct { 387 | value string 388 | weight int 389 | } 390 | type Cycle struct { 391 | Name string `diff:"name,create"` 392 | Count int `diff:"count,create"` 393 | } 394 | type Fruit struct { 395 | ID int `diff:"ID" json:"Identifier"` 396 | Name string `diff:"name"` 397 | Healthy bool `diff:"healthy"` 398 | Nutrients []string `diff:"nutrients,create,omitunequal"` 399 | Labels map[Key]Cycle `diff:"labs,create"` 400 | Cycles []Cycle `diff:"cycles,immutable"` 401 | Weights []int 402 | } 403 | 404 | a := Fruit{ 405 | ID: 1, 406 | Name: "Green Apple", 407 | Healthy: true, 408 | Nutrients: []string{ 409 | "vitamin a", 410 | "vitamin b", 411 | "vitamin c", 412 | "vitamin d", 413 | }, 414 | Labels: make(map[Key]Cycle), 415 | } 416 | a.Labels[Key{value: "likes"}] = Cycle{ 417 | Count: 10, 418 | } 419 | a.Labels[Key{value: "colors"}] = Cycle{ 420 | Count: 2, 421 | } 422 | 423 | b := Fruit{ 424 | ID: 2, 425 | Name: "Red Apple", 426 | Healthy: true, 427 | Nutrients: []string{ 428 | "vitamin c", 429 | "vitamin d", 430 | "vitamin e", 431 | }, 432 | Labels: make(map[Key]Cycle), 433 | Weights: []int{ 434 | 1, 435 | 2, 436 | 3, 437 | 4, 438 | }, 439 | } 440 | b.Labels[Key{value: "forests"}] = Cycle{ 441 | Count: 1223, 442 | } 443 | b.Labels[Key{value: "colors"}] = Cycle{ 444 | Count: 1222, 445 | } 446 | 447 | c := Fruit{ 448 | //Labels: make(map[string]int), 449 | Nutrients: []string{ 450 | "vitamin a", 451 | "vitamin c", 452 | "vitamin d", 453 | }, 454 | } 455 | //c.Labels["likes"] = 21 456 | 457 | d := a 458 | d.Cycles = []Cycle{ 459 | Cycle{ 460 | Name: "First", 461 | Count: 45, 462 | }, 463 | Cycle{ 464 | Name: "Third", 465 | Count: 4, 466 | }, 467 | } 468 | d.Nutrients = append(d.Nutrients, "minerals") 469 | 470 | changelog, err := diff.Diff(a, b) 471 | if err != nil { 472 | panic(err) 473 | } 474 | 475 | patchLog := diff.Patch(changelog, &c) 476 | 477 | changelog, _ = diff.Diff(a, d) 478 | patchLog = diff.Patch(changelog, &c) 479 | 480 | fmt.Printf("%#v", len(patchLog)) 481 | 482 | //Output: 1 483 | } 484 | 485 | func ExampleDiff() { 486 | type Tag struct { 487 | Name string `diff:"name,identifier"` 488 | Value string `diff:"value"` 489 | } 490 | 491 | type Fruit struct { 492 | ID int `diff:"id"` 493 | Name string `diff:"name"` 494 | Healthy bool `diff:"healthy"` 495 | Nutrients []string `diff:"nutrients"` 496 | Tags []Tag `diff:"tags"` 497 | } 498 | 499 | a := Fruit{ 500 | ID: 1, 501 | Name: "Green Apple", 502 | Healthy: true, 503 | Nutrients: []string{ 504 | "vitamin c", 505 | "vitamin d", 506 | }, 507 | Tags: []Tag{ 508 | { 509 | Name: "kind", 510 | Value: "fruit", 511 | }, 512 | }, 513 | } 514 | 515 | b := Fruit{ 516 | ID: 2, 517 | Name: "Red Apple", 518 | Healthy: true, 519 | Nutrients: []string{ 520 | "vitamin c", 521 | "vitamin d", 522 | "vitamin e", 523 | }, 524 | Tags: []Tag{ 525 | { 526 | Name: "popularity", 527 | Value: "high", 528 | }, 529 | { 530 | Name: "kind", 531 | Value: "fruit", 532 | }, 533 | }, 534 | } 535 | 536 | changelog, err := diff.Diff(a, b) 537 | if err != nil { 538 | panic(err) 539 | } 540 | 541 | fmt.Printf("%#v", changelog) 542 | // 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"}}} 543 | } 544 | 545 | func ExampleFilter() { 546 | type Tag struct { 547 | Name string `diff:"name,identifier"` 548 | Value string `diff:"value"` 549 | } 550 | 551 | type Fruit struct { 552 | ID int `diff:"id"` 553 | Name string `diff:"name"` 554 | Healthy bool `diff:"healthy"` 555 | Nutrients []string `diff:"nutrients"` 556 | Tags []Tag `diff:"tags"` 557 | } 558 | 559 | a := Fruit{ 560 | ID: 1, 561 | Name: "Green Apple", 562 | Healthy: true, 563 | Nutrients: []string{ 564 | "vitamin c", 565 | "vitamin d", 566 | }, 567 | } 568 | 569 | b := Fruit{ 570 | ID: 2, 571 | Name: "Red Apple", 572 | Healthy: true, 573 | Nutrients: []string{ 574 | "vitamin c", 575 | "vitamin d", 576 | "vitamin e", 577 | }, 578 | } 579 | 580 | d, err := diff.NewDiffer(diff.Filter(func(path []string, parent reflect.Type, field reflect.StructField) bool { 581 | return field.Name != "Name" 582 | })) 583 | if err != nil { 584 | panic(err) 585 | } 586 | 587 | changelog, err := d.Diff(a, b) 588 | if err != nil { 589 | panic(err) 590 | } 591 | 592 | fmt.Printf("%#v", changelog) 593 | // 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)}} 594 | } 595 | 596 | func ExamplePrivatePtr() { 597 | type number struct { 598 | value *big.Int 599 | exp int32 600 | } 601 | a := number{} 602 | b := number{value: big.NewInt(111)} 603 | 604 | changelog, err := diff.Diff(a, b) 605 | if err != nil { 606 | panic(err) 607 | } 608 | 609 | fmt.Printf("%#v", changelog) 610 | // 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}}} 611 | } 612 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /diff_test.go: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | package diff_test 6 | 7 | import ( 8 | "reflect" 9 | "strings" 10 | "sync" 11 | "testing" 12 | "time" 13 | 14 | "github.com/r3labs/diff/v3" 15 | "github.com/stretchr/testify/assert" 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | var currentTime = time.Now() 20 | 21 | var struct1 = tmstruct{Bar: 1, Foo: "one"} 22 | var struct2 = tmstruct{Bar: 2, Foo: "two"} 23 | 24 | type tistruct struct { 25 | Name string `diff:"name,identifier"` 26 | Value int `diff:"value"` 27 | } 28 | 29 | type tuistruct struct { 30 | Value int `diff:"value"` 31 | } 32 | 33 | type tnstruct struct { 34 | Slice []tmstruct `diff:"slice"` 35 | } 36 | 37 | type tmstruct struct { 38 | Foo string `diff:"foo"` 39 | Bar int `diff:"bar"` 40 | } 41 | 42 | type Embedded struct { 43 | Foo string `diff:"foo"` 44 | Bar int `diff:"bar"` 45 | } 46 | 47 | type embedstruct struct { 48 | Embedded 49 | Baz bool `diff:"baz"` 50 | } 51 | 52 | type customTagStruct struct { 53 | Foo string `json:"foo"` 54 | Bar int `json:"bar"` 55 | } 56 | 57 | type privateValueStruct struct { 58 | Public string 59 | Private *sync.RWMutex 60 | } 61 | 62 | type privateMapStruct struct { 63 | set map[string]interface{} 64 | } 65 | 66 | type CustomStringType string 67 | type CustomIntType int 68 | type customTypeStruct struct { 69 | Foo CustomStringType `diff:"foo"` 70 | Bar CustomIntType `diff:"bar"` 71 | } 72 | 73 | type tstruct struct { 74 | ID string `diff:"id,immutable"` 75 | Name string `diff:"name"` 76 | Value int `diff:"value"` 77 | Bool bool `diff:"bool"` 78 | Values []string `diff:"values"` 79 | Map map[string]string `diff:"map"` 80 | Time time.Time `diff:"time"` 81 | Pointer *string `diff:"pointer"` 82 | Ignored bool `diff:"-"` 83 | Identifiables []tistruct `diff:"identifiables"` 84 | Unidentifiables []tuistruct `diff:"unidentifiables"` 85 | Nested tnstruct `diff:"nested"` 86 | private int `diff:"private"` 87 | } 88 | 89 | func sptr(s string) *string { 90 | return &s 91 | } 92 | 93 | func TestDiff(t *testing.T) { 94 | cases := []struct { 95 | Name string 96 | A, B interface{} 97 | Changelog diff.Changelog 98 | Error error 99 | }{ 100 | { 101 | "uint-slice-insert", []uint{1, 2, 3}, []uint{1, 2, 3, 4}, 102 | diff.Changelog{ 103 | diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: uint(4)}, 104 | }, 105 | nil, 106 | }, 107 | { 108 | "uint-array-insert", [3]uint{1, 2, 3}, [4]uint{1, 2, 3, 4}, 109 | diff.Changelog{ 110 | diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: uint(4)}, 111 | }, 112 | nil, 113 | }, 114 | { 115 | "int-slice-insert", []int{1, 2, 3}, []int{1, 2, 3, 4}, 116 | diff.Changelog{ 117 | diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: 4}, 118 | }, 119 | nil, 120 | }, 121 | { 122 | "int-array-insert", [3]int{1, 2, 3}, [4]int{1, 2, 3, 4}, 123 | diff.Changelog{ 124 | diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: 4}, 125 | }, 126 | nil, 127 | }, 128 | { 129 | "uint-slice-delete", []uint{1, 2, 3}, []uint{1, 3}, 130 | diff.Changelog{ 131 | diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: uint(2)}, 132 | }, 133 | nil, 134 | }, 135 | { 136 | "uint-array-delete", [3]uint{1, 2, 3}, [2]uint{1, 3}, 137 | diff.Changelog{ 138 | diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: uint(2)}, 139 | }, 140 | nil, 141 | }, 142 | { 143 | "int-slice-delete", []int{1, 2, 3}, []int{1, 3}, 144 | diff.Changelog{ 145 | diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: 2}, 146 | }, 147 | nil, 148 | }, 149 | { 150 | "uint-slice-insert-delete", []uint{1, 2, 3}, []uint{1, 3, 4}, 151 | diff.Changelog{ 152 | diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: uint(2)}, 153 | diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: uint(4)}, 154 | }, 155 | nil, 156 | }, 157 | { 158 | "uint-slice-array-delete", [3]uint{1, 2, 3}, [3]uint{1, 3, 4}, 159 | diff.Changelog{ 160 | diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: uint(2)}, 161 | diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: uint(4)}, 162 | }, 163 | nil, 164 | }, 165 | { 166 | "int-slice-insert-delete", []int{1, 2, 3}, []int{1, 3, 4}, 167 | diff.Changelog{ 168 | diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: 2}, 169 | diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: 4}, 170 | }, 171 | nil, 172 | }, 173 | { 174 | "string-slice-insert", []string{"1", "2", "3"}, []string{"1", "2", "3", "4"}, 175 | diff.Changelog{ 176 | diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: "4"}, 177 | }, 178 | nil, 179 | }, 180 | { 181 | "string-array-insert", [3]string{"1", "2", "3"}, [4]string{"1", "2", "3", "4"}, 182 | diff.Changelog{ 183 | diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: "4"}, 184 | }, 185 | nil, 186 | }, 187 | { 188 | "string-slice-delete", []string{"1", "2", "3"}, []string{"1", "3"}, 189 | diff.Changelog{ 190 | diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"}, 191 | }, 192 | nil, 193 | }, 194 | { 195 | "string-slice-delete", [3]string{"1", "2", "3"}, [2]string{"1", "3"}, 196 | diff.Changelog{ 197 | diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"}, 198 | }, 199 | nil, 200 | }, 201 | { 202 | "string-slice-insert-delete", []string{"1", "2", "3"}, []string{"1", "3", "4"}, 203 | diff.Changelog{ 204 | diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"}, 205 | diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: "4"}, 206 | }, 207 | nil, 208 | }, 209 | { 210 | "string-array-insert-delete", [3]string{"1", "2", "3"}, [3]string{"1", "3", "4"}, 211 | diff.Changelog{ 212 | diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"}, 213 | diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: "4"}, 214 | }, 215 | nil, 216 | }, 217 | { 218 | "comparable-slice-insert", []tistruct{{"one", 1}}, []tistruct{{"one", 1}, {"two", 2}}, 219 | diff.Changelog{ 220 | diff.Change{Type: diff.CREATE, Path: []string{"two", "name"}, To: "two"}, 221 | diff.Change{Type: diff.CREATE, Path: []string{"two", "value"}, To: 2}, 222 | }, 223 | nil, 224 | }, 225 | { 226 | "comparable-array-insert", [1]tistruct{{"one", 1}}, [2]tistruct{{"one", 1}, {"two", 2}}, 227 | diff.Changelog{ 228 | diff.Change{Type: diff.CREATE, Path: []string{"two", "name"}, To: "two"}, 229 | diff.Change{Type: diff.CREATE, Path: []string{"two", "value"}, To: 2}, 230 | }, 231 | nil, 232 | }, 233 | { 234 | "comparable-slice-delete", []tistruct{{"one", 1}, {"two", 2}}, []tistruct{{"one", 1}}, 235 | diff.Changelog{ 236 | diff.Change{Type: diff.DELETE, Path: []string{"two", "name"}, From: "two"}, 237 | diff.Change{Type: diff.DELETE, Path: []string{"two", "value"}, From: 2}, 238 | }, 239 | nil, 240 | }, 241 | { 242 | "comparable-array-delete", [2]tistruct{{"one", 1}, {"two", 2}}, [1]tistruct{{"one", 1}}, 243 | diff.Changelog{ 244 | diff.Change{Type: diff.DELETE, Path: []string{"two", "name"}, From: "two"}, 245 | diff.Change{Type: diff.DELETE, Path: []string{"two", "value"}, From: 2}, 246 | }, 247 | nil, 248 | }, 249 | { 250 | "comparable-slice-update", []tistruct{{"one", 1}}, []tistruct{{"one", 50}}, 251 | diff.Changelog{ 252 | diff.Change{Type: diff.UPDATE, Path: []string{"one", "value"}, From: 1, To: 50}, 253 | }, 254 | nil, 255 | }, 256 | { 257 | "comparable-array-update", [1]tistruct{{"one", 1}}, [1]tistruct{{"one", 50}}, 258 | diff.Changelog{ 259 | diff.Change{Type: diff.UPDATE, Path: []string{"one", "value"}, From: 1, To: 50}, 260 | }, 261 | nil, 262 | }, 263 | { 264 | "map-slice-insert", []map[string]string{{"test": "123"}}, []map[string]string{{"test": "123", "tset": "456"}}, 265 | diff.Changelog{ 266 | diff.Change{Type: diff.CREATE, Path: []string{"0", "tset"}, To: "456"}, 267 | }, 268 | nil, 269 | }, 270 | { 271 | "map-array-insert", [1]map[string]string{{"test": "123"}}, [1]map[string]string{{"test": "123", "tset": "456"}}, 272 | diff.Changelog{ 273 | diff.Change{Type: diff.CREATE, Path: []string{"0", "tset"}, To: "456"}, 274 | }, 275 | nil, 276 | }, 277 | { 278 | "map-slice-update", []map[string]string{{"test": "123"}}, []map[string]string{{"test": "456"}}, 279 | diff.Changelog{ 280 | diff.Change{Type: diff.UPDATE, Path: []string{"0", "test"}, From: "123", To: "456"}, 281 | }, 282 | nil, 283 | }, 284 | { 285 | "map-array-update", [1]map[string]string{{"test": "123"}}, [1]map[string]string{{"test": "456"}}, 286 | diff.Changelog{ 287 | diff.Change{Type: diff.UPDATE, Path: []string{"0", "test"}, From: "123", To: "456"}, 288 | }, 289 | nil, 290 | }, 291 | { 292 | "map-slice-delete", []map[string]string{{"test": "123", "tset": "456"}}, []map[string]string{{"test": "123"}}, 293 | diff.Changelog{ 294 | diff.Change{Type: diff.DELETE, Path: []string{"0", "tset"}, From: "456"}, 295 | }, 296 | nil, 297 | }, 298 | { 299 | "map-array-delete", [1]map[string]string{{"test": "123", "tset": "456"}}, [1]map[string]string{{"test": "123"}}, 300 | diff.Changelog{ 301 | diff.Change{Type: diff.DELETE, Path: []string{"0", "tset"}, From: "456"}, 302 | }, 303 | nil, 304 | }, 305 | { 306 | "map-interface-slice-update", []map[string]interface{}{{"test": nil}}, []map[string]interface{}{{"test": "456"}}, 307 | diff.Changelog{ 308 | diff.Change{Type: diff.UPDATE, Path: []string{"0", "test"}, From: nil, To: "456"}, 309 | }, 310 | nil, 311 | }, 312 | { 313 | "map-interface-array-update", [1]map[string]interface{}{{"test": nil}}, [1]map[string]interface{}{{"test": "456"}}, 314 | diff.Changelog{ 315 | diff.Change{Type: diff.UPDATE, Path: []string{"0", "test"}, From: nil, To: "456"}, 316 | }, 317 | nil, 318 | }, 319 | { 320 | "map-nil", map[string]string{"one": "test"}, nil, 321 | diff.Changelog{ 322 | diff.Change{Type: diff.DELETE, Path: []string{"\xa3one"}, From: "test", To: nil}, 323 | }, 324 | nil, 325 | }, 326 | { 327 | "nil-map", nil, map[string]string{"one": "test"}, 328 | diff.Changelog{ 329 | diff.Change{Type: diff.CREATE, Path: []string{"\xa3one"}, From: nil, To: "test"}, 330 | }, 331 | nil, 332 | }, 333 | { 334 | "nested-map-insert", map[string]map[string]string{"a": {"test": "123"}}, map[string]map[string]string{"a": {"test": "123", "tset": "456"}}, 335 | diff.Changelog{ 336 | diff.Change{Type: diff.CREATE, Path: []string{"a", "tset"}, To: "456"}, 337 | }, 338 | nil, 339 | }, 340 | { 341 | "nested-map-interface-insert", map[string]map[string]interface{}{"a": {"test": "123"}}, map[string]map[string]interface{}{"a": {"test": "123", "tset": "456"}}, 342 | diff.Changelog{ 343 | diff.Change{Type: diff.CREATE, Path: []string{"a", "tset"}, To: "456"}, 344 | }, 345 | nil, 346 | }, 347 | { 348 | "nested-map-update", map[string]map[string]string{"a": {"test": "123"}}, map[string]map[string]string{"a": {"test": "456"}}, 349 | diff.Changelog{ 350 | diff.Change{Type: diff.UPDATE, Path: []string{"a", "test"}, From: "123", To: "456"}, 351 | }, 352 | nil, 353 | }, 354 | { 355 | "nested-map-delete", map[string]map[string]string{"a": {"test": "123"}}, map[string]map[string]string{"a": {}}, 356 | diff.Changelog{ 357 | diff.Change{Type: diff.DELETE, Path: []string{"a", "test"}, From: "123", To: nil}, 358 | }, 359 | nil, 360 | }, 361 | { 362 | "nested-slice-insert", map[string][]int{"a": {1, 2, 3}}, map[string][]int{"a": {1, 2, 3, 4}}, 363 | diff.Changelog{ 364 | diff.Change{Type: diff.CREATE, Path: []string{"a", "3"}, To: 4}, 365 | }, 366 | nil, 367 | }, 368 | { 369 | "nested-array-insert", map[string][3]int{"a": {1, 2, 3}}, map[string][4]int{"a": {1, 2, 3, 4}}, 370 | diff.Changelog{ 371 | diff.Change{Type: diff.CREATE, Path: []string{"a", "3"}, To: 4}, 372 | }, 373 | nil, 374 | }, 375 | { 376 | "nested-slice-update", map[string][]int{"a": {1, 2, 3}}, map[string][]int{"a": {1, 4, 3}}, 377 | diff.Changelog{ 378 | diff.Change{Type: diff.UPDATE, Path: []string{"a", "1"}, From: 2, To: 4}, 379 | }, 380 | nil, 381 | }, 382 | { 383 | "nested-array-update", map[string][3]int{"a": {1, 2, 3}}, map[string][3]int{"a": {1, 4, 3}}, 384 | diff.Changelog{ 385 | diff.Change{Type: diff.UPDATE, Path: []string{"a", "1"}, From: 2, To: 4}, 386 | }, 387 | nil, 388 | }, 389 | { 390 | "nested-slice-delete", map[string][]int{"a": {1, 2, 3}}, map[string][]int{"a": {1, 3}}, 391 | diff.Changelog{ 392 | diff.Change{Type: diff.DELETE, Path: []string{"a", "1"}, From: 2, To: nil}, 393 | }, 394 | nil, 395 | }, 396 | { 397 | "nested-array-delete", map[string][3]int{"a": {1, 2, 3}}, map[string][2]int{"a": {1, 3}}, 398 | diff.Changelog{ 399 | diff.Change{Type: diff.DELETE, Path: []string{"a", "1"}, From: 2, To: nil}, 400 | }, 401 | nil, 402 | }, 403 | { 404 | "struct-string-update", tstruct{Name: "one"}, tstruct{Name: "two"}, 405 | diff.Changelog{ 406 | diff.Change{Type: diff.UPDATE, Path: []string{"name"}, From: "one", To: "two"}, 407 | }, 408 | nil, 409 | }, 410 | { 411 | "struct-int-update", tstruct{Value: 1}, tstruct{Value: 50}, 412 | diff.Changelog{ 413 | diff.Change{Type: diff.UPDATE, Path: []string{"value"}, From: 1, To: 50}, 414 | }, 415 | nil, 416 | }, 417 | { 418 | "struct-bool-update", tstruct{Bool: true}, tstruct{Bool: false}, 419 | diff.Changelog{ 420 | diff.Change{Type: diff.UPDATE, Path: []string{"bool"}, From: true, To: false}, 421 | }, 422 | nil, 423 | }, 424 | { 425 | "struct-time-update", tstruct{}, tstruct{Time: currentTime}, 426 | diff.Changelog{ 427 | diff.Change{Type: diff.UPDATE, Path: []string{"time"}, From: time.Time{}, To: currentTime}, 428 | }, 429 | nil, 430 | }, 431 | { 432 | "struct-map-update", tstruct{Map: map[string]string{"test": "123"}}, tstruct{Map: map[string]string{"test": "456"}}, 433 | diff.Changelog{ 434 | diff.Change{Type: diff.UPDATE, Path: []string{"map", "test"}, From: "123", To: "456"}, 435 | }, 436 | nil, 437 | }, 438 | { 439 | "struct-string-pointer-update", tstruct{Pointer: sptr("test")}, tstruct{Pointer: sptr("test2")}, 440 | diff.Changelog{ 441 | diff.Change{Type: diff.UPDATE, Path: []string{"pointer"}, From: "test", To: "test2"}, 442 | }, 443 | nil, 444 | }, 445 | { 446 | "struct-nil-string-pointer-update", tstruct{Pointer: nil}, tstruct{Pointer: sptr("test")}, 447 | diff.Changelog{ 448 | diff.Change{Type: diff.UPDATE, Path: []string{"pointer"}, From: nil, To: sptr("test")}, 449 | }, 450 | nil, 451 | }, 452 | { 453 | "struct-generic-slice-insert", tstruct{Values: []string{"one"}}, tstruct{Values: []string{"one", "two"}}, 454 | diff.Changelog{ 455 | diff.Change{Type: diff.CREATE, Path: []string{"values", "1"}, From: nil, To: "two"}, 456 | }, 457 | nil, 458 | }, 459 | { 460 | "struct-identifiable-slice-insert", tstruct{Identifiables: []tistruct{{"one", 1}}}, tstruct{Identifiables: []tistruct{{"one", 1}, {"two", 2}}}, 461 | diff.Changelog{ 462 | diff.Change{Type: diff.CREATE, Path: []string{"identifiables", "two", "name"}, From: nil, To: "two"}, 463 | diff.Change{Type: diff.CREATE, Path: []string{"identifiables", "two", "value"}, From: nil, To: 2}, 464 | }, 465 | nil, 466 | }, 467 | { 468 | "struct-generic-slice-delete", tstruct{Values: []string{"one", "two"}}, tstruct{Values: []string{"one"}}, 469 | diff.Changelog{ 470 | diff.Change{Type: diff.DELETE, Path: []string{"values", "1"}, From: "two", To: nil}, 471 | }, 472 | nil, 473 | }, 474 | { 475 | "struct-identifiable-slice-delete", tstruct{Identifiables: []tistruct{{"one", 1}, {"two", 2}}}, tstruct{Identifiables: []tistruct{{"one", 1}}}, 476 | diff.Changelog{ 477 | diff.Change{Type: diff.DELETE, Path: []string{"identifiables", "two", "name"}, From: "two", To: nil}, 478 | diff.Change{Type: diff.DELETE, Path: []string{"identifiables", "two", "value"}, From: 2, To: nil}, 479 | }, 480 | nil, 481 | }, 482 | { 483 | "struct-unidentifiable-slice-insert-delete", tstruct{Unidentifiables: []tuistruct{{1}, {2}, {3}}}, tstruct{Unidentifiables: []tuistruct{{5}, {2}, {3}, {4}}}, 484 | diff.Changelog{ 485 | diff.Change{Type: diff.UPDATE, Path: []string{"unidentifiables", "0", "value"}, From: 1, To: 5}, 486 | diff.Change{Type: diff.CREATE, Path: []string{"unidentifiables", "3", "value"}, From: nil, To: 4}, 487 | }, 488 | nil, 489 | }, 490 | { 491 | "struct-with-private-value", privateValueStruct{Public: "one", Private: new(sync.RWMutex)}, privateValueStruct{Public: "two", Private: new(sync.RWMutex)}, 492 | diff.Changelog{ 493 | diff.Change{Type: diff.UPDATE, Path: []string{"Public"}, From: "one", To: "two"}, 494 | }, 495 | nil, 496 | }, 497 | { 498 | "mismatched-values-struct-map", map[string]string{"test": "one"}, &tstruct{Identifiables: []tistruct{{"one", 1}}}, 499 | diff.Changelog{}, 500 | diff.ErrTypeMismatch, 501 | }, 502 | { 503 | "omittable", tstruct{Ignored: false}, tstruct{Ignored: true}, 504 | diff.Changelog{}, 505 | nil, 506 | }, 507 | { 508 | "slice", &tstruct{}, &tstruct{Nested: tnstruct{Slice: []tmstruct{{"one", 1}, {"two", 2}}}}, 509 | diff.Changelog{ 510 | diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "0", "foo"}, From: nil, To: "one"}, 511 | diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "0", "bar"}, From: nil, To: 1}, 512 | diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "1", "foo"}, From: nil, To: "two"}, 513 | diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "1", "bar"}, From: nil, To: 2}, 514 | }, 515 | nil, 516 | }, 517 | { 518 | "slice-duplicate-items", []int{1}, []int{1, 1}, 519 | diff.Changelog{ 520 | diff.Change{Type: diff.CREATE, Path: []string{"1"}, From: nil, To: 1}, 521 | }, 522 | nil, 523 | }, 524 | { 525 | "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"}}}, 526 | diff.Changelog{ 527 | diff.Change{Type: diff.UPDATE, Path: []string{"0", "type", "1"}, From: "string", To: "int"}, 528 | diff.Change{Type: diff.CREATE, Path: []string{"1", "\xa4name"}, From: nil, To: "name2"}, 529 | diff.Change{Type: diff.CREATE, Path: []string{"1", "\xa4type"}, From: nil, To: []string{"null", "string"}}, 530 | }, 531 | nil, 532 | }, 533 | { 534 | "map-string-pointer-create", 535 | map[string]*tmstruct{"one": &struct1}, 536 | map[string]*tmstruct{"one": &struct1, "two": &struct2}, 537 | diff.Changelog{ 538 | diff.Change{Type: diff.CREATE, Path: []string{"two", "foo"}, From: nil, To: "two"}, 539 | diff.Change{Type: diff.CREATE, Path: []string{"two", "bar"}, From: nil, To: 2}, 540 | }, 541 | nil, 542 | }, 543 | { 544 | "map-string-pointer-delete", 545 | map[string]*tmstruct{"one": &struct1, "two": &struct2}, 546 | map[string]*tmstruct{"one": &struct1}, 547 | diff.Changelog{ 548 | diff.Change{Type: diff.DELETE, Path: []string{"two", "foo"}, From: "two", To: nil}, 549 | diff.Change{Type: diff.DELETE, Path: []string{"two", "bar"}, From: 2, To: nil}, 550 | }, 551 | nil, 552 | }, 553 | { 554 | "private-struct-field", 555 | tstruct{private: 1}, 556 | tstruct{private: 4}, 557 | diff.Changelog{ 558 | diff.Change{Type: diff.UPDATE, Path: []string{"private"}, From: int64(1), To: int64(4)}, 559 | }, 560 | nil, 561 | }, 562 | { 563 | "embedded-struct-field", 564 | embedstruct{Embedded{Foo: "a", Bar: 2}, true}, 565 | embedstruct{Embedded{Foo: "b", Bar: 3}, false}, 566 | diff.Changelog{ 567 | diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: "a", To: "b"}, 568 | diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: 2, To: 3}, 569 | diff.Change{Type: diff.UPDATE, Path: []string{"baz"}, From: true, To: false}, 570 | }, 571 | nil, 572 | }, 573 | { 574 | "custom-tags", 575 | customTagStruct{Foo: "abc", Bar: 3}, 576 | customTagStruct{Foo: "def", Bar: 4}, 577 | diff.Changelog{ 578 | diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: "abc", To: "def"}, 579 | diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: 3, To: 4}, 580 | }, 581 | nil, 582 | }, 583 | { 584 | "custom-types", 585 | customTypeStruct{Foo: "a", Bar: 1}, 586 | customTypeStruct{Foo: "b", Bar: 2}, 587 | diff.Changelog{ 588 | diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: CustomStringType("a"), To: CustomStringType("b")}, 589 | diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: CustomIntType(1), To: CustomIntType(2)}, 590 | }, 591 | nil, 592 | }, 593 | { 594 | "struct-private-map-create", privateMapStruct{set: map[string]interface{}{"1": struct{}{}, "2": struct{}{}}}, privateMapStruct{set: map[string]interface{}{"1": struct{}{}, "2": struct{}{}, "3": struct{}{}}}, 595 | diff.Changelog{ 596 | diff.Change{Type: diff.CREATE, Path: []string{"set", "3"}, From: nil, To: struct{}{}}, 597 | }, 598 | nil, 599 | }, 600 | { 601 | "struct-private-map-delete", privateMapStruct{set: map[string]interface{}{"1": struct{}{}, "2": struct{}{}, "3": struct{}{}}}, privateMapStruct{set: map[string]interface{}{"1": struct{}{}, "2": struct{}{}}}, 602 | diff.Changelog{ 603 | diff.Change{Type: diff.DELETE, Path: []string{"set", "3"}, From: struct{}{}, To: nil}, 604 | }, 605 | nil, 606 | }, 607 | { 608 | "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}}, 609 | diff.Changelog{ 610 | diff.Change{Type: diff.CREATE, Path: []string{"set", "3"}, From: nil, To: nil}, 611 | }, 612 | nil, 613 | }, 614 | { 615 | "slice-of-struct-with-slice", 616 | []tnstruct{{[]tmstruct{struct1, struct2}}, {[]tmstruct{struct2, struct2}}}, 617 | []tnstruct{{[]tmstruct{struct2, struct2}}, {[]tmstruct{struct2, struct1}}}, 618 | diff.Changelog{}, 619 | nil, 620 | }, 621 | } 622 | 623 | for _, tc := range cases { 624 | t.Run(tc.Name, func(t *testing.T) { 625 | 626 | var options []func(d *diff.Differ) error 627 | switch tc.Name { 628 | case "mixed-slice-map", "nil-map", "map-nil": 629 | options = append(options, diff.StructMapKeySupport()) 630 | case "embedded-struct-field": 631 | options = append(options, diff.FlattenEmbeddedStructs()) 632 | case "custom-tags": 633 | options = append(options, diff.TagName("json")) 634 | } 635 | cl, err := diff.Diff(tc.A, tc.B, options...) 636 | 637 | assert.Equal(t, tc.Error, err) 638 | require.Equal(t, len(tc.Changelog), len(cl)) 639 | 640 | for i, c := range cl { 641 | assert.Equal(t, tc.Changelog[i].Type, c.Type) 642 | assert.Equal(t, tc.Changelog[i].Path, c.Path) 643 | assert.Equal(t, tc.Changelog[i].From, c.From) 644 | assert.Equal(t, tc.Changelog[i].To, c.To) 645 | } 646 | }) 647 | } 648 | } 649 | 650 | func TestDiffSliceOrdering(t *testing.T) { 651 | cases := []struct { 652 | Name string 653 | A, B interface{} 654 | Changelog diff.Changelog 655 | Error error 656 | }{ 657 | { 658 | "int-slice-insert-in-middle", []int{1, 2, 4}, []int{1, 2, 3, 4}, 659 | diff.Changelog{ 660 | diff.Change{Type: diff.UPDATE, Path: []string{"2"}, From: 4, To: 3}, 661 | diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: 4}, 662 | }, 663 | nil, 664 | }, 665 | { 666 | "int-slice-delete", []int{1, 2, 3}, []int{1, 3}, 667 | diff.Changelog{ 668 | diff.Change{Type: diff.UPDATE, Path: []string{"1"}, From: 2, To: 3}, 669 | diff.Change{Type: diff.DELETE, Path: []string{"2"}, From: 3}, 670 | }, 671 | nil, 672 | }, 673 | { 674 | "int-slice-insert-delete", []int{1, 2, 3}, []int{1, 3, 4}, 675 | diff.Changelog{ 676 | diff.Change{Type: diff.UPDATE, Path: []string{"1"}, From: 2, To: 3}, 677 | diff.Change{Type: diff.UPDATE, Path: []string{"2"}, From: 3, To: 4}, 678 | }, 679 | nil, 680 | }, 681 | { 682 | "int-slice-reorder", []int{1, 2, 3}, []int{1, 3, 2}, 683 | diff.Changelog{ 684 | diff.Change{Type: diff.UPDATE, Path: []string{"1"}, From: 2, To: 3}, 685 | diff.Change{Type: diff.UPDATE, Path: []string{"2"}, From: 3, To: 2}, 686 | }, 687 | nil, 688 | }, 689 | { 690 | "string-slice-delete", []string{"1", "2", "3"}, []string{"1", "3"}, 691 | diff.Changelog{ 692 | diff.Change{Type: diff.UPDATE, Path: []string{"1"}, From: "2", To: "3"}, 693 | diff.Change{Type: diff.DELETE, Path: []string{"2"}, From: "3"}, 694 | }, 695 | nil, 696 | }, 697 | { 698 | "string-slice-insert-delete", []string{"1", "2", "3"}, []string{"1", "3", "4"}, 699 | diff.Changelog{ 700 | diff.Change{Type: diff.UPDATE, Path: []string{"1"}, From: "2", To: "3"}, 701 | diff.Change{Type: diff.UPDATE, Path: []string{"2"}, From: "3", To: "4"}, 702 | }, 703 | nil, 704 | }, 705 | { 706 | "string-slice-reorder", []string{"1", "2", "3"}, []string{"1", "3", "2"}, 707 | diff.Changelog{ 708 | diff.Change{Type: diff.UPDATE, Path: []string{"1"}, From: "2", To: "3"}, 709 | diff.Change{Type: diff.UPDATE, Path: []string{"2"}, From: "3", To: "2"}, 710 | }, 711 | nil, 712 | }, 713 | { 714 | "nested-slice-delete", map[string][]int{"a": {1, 2, 3}}, map[string][]int{"a": {1, 3}}, 715 | diff.Changelog{ 716 | diff.Change{Type: diff.UPDATE, Path: []string{"a", "1"}, From: 2, To: 3}, 717 | diff.Change{Type: diff.DELETE, Path: []string{"a", "2"}, From: 3}, 718 | }, 719 | nil, 720 | }, 721 | } 722 | 723 | for _, tc := range cases { 724 | t.Run(tc.Name, func(t *testing.T) { 725 | d, err := diff.NewDiffer(diff.SliceOrdering(true)) 726 | require.Nil(t, err) 727 | cl, err := d.Diff(tc.A, tc.B) 728 | 729 | assert.Equal(t, tc.Error, err) 730 | require.Equal(t, len(tc.Changelog), len(cl)) 731 | 732 | for i, c := range cl { 733 | assert.Equal(t, tc.Changelog[i].Type, c.Type) 734 | assert.Equal(t, tc.Changelog[i].Path, c.Path) 735 | assert.Equal(t, tc.Changelog[i].From, c.From) 736 | assert.Equal(t, tc.Changelog[i].To, c.To) 737 | } 738 | }) 739 | } 740 | 741 | } 742 | 743 | func TestFilter(t *testing.T) { 744 | cases := []struct { 745 | Name string 746 | Filter []string 747 | Expected [][]string 748 | }{ 749 | {"simple", []string{"item-1", "subitem"}, [][]string{{"item-1", "subitem"}}}, 750 | {"regex", []string{"item-*"}, [][]string{{"item-1", "subitem"}, {"item-2", "subitem"}}}, 751 | } 752 | 753 | cl := diff.Changelog{ 754 | {Path: []string{"item-1", "subitem"}}, 755 | {Path: []string{"item-2", "subitem"}}, 756 | } 757 | 758 | for _, tc := range cases { 759 | t.Run(tc.Name, func(t *testing.T) { 760 | ncl := cl.Filter(tc.Filter) 761 | assert.Len(t, ncl, len(tc.Expected)) 762 | for i, e := range tc.Expected { 763 | assert.Equal(t, e, ncl[i].Path) 764 | } 765 | }) 766 | } 767 | } 768 | 769 | func TestFilterOut(t *testing.T) { 770 | cases := []struct { 771 | Name string 772 | Filter []string 773 | Expected [][]string 774 | }{ 775 | {"simple", []string{"item-1", "subitem"}, [][]string{{"item-2", "subitem"}}}, 776 | {"regex", []string{"item-*"}, [][]string{}}, 777 | } 778 | 779 | cl := diff.Changelog{ 780 | {Path: []string{"item-1", "subitem"}}, 781 | {Path: []string{"item-2", "subitem"}}, 782 | } 783 | 784 | for _, tc := range cases { 785 | t.Run(tc.Name, func(t *testing.T) { 786 | ncl := cl.FilterOut(tc.Filter) 787 | assert.Len(t, ncl, len(tc.Expected)) 788 | for i, e := range tc.Expected { 789 | assert.Equal(t, e, ncl[i].Path) 790 | } 791 | }) 792 | } 793 | } 794 | 795 | func TestStructValues(t *testing.T) { 796 | cases := []struct { 797 | Name string 798 | ChangeType string 799 | X interface{} 800 | Changelog diff.Changelog 801 | Error error 802 | }{ 803 | { 804 | "struct-create", diff.CREATE, tstruct{ID: "xxxxx", Name: "something", Value: 1, Values: []string{"one", "two", "three"}}, 805 | diff.Changelog{ 806 | diff.Change{Type: diff.CREATE, Path: []string{"id"}, From: nil, To: "xxxxx"}, 807 | diff.Change{Type: diff.CREATE, Path: []string{"name"}, From: nil, To: "something"}, 808 | diff.Change{Type: diff.CREATE, Path: []string{"value"}, From: nil, To: 1}, 809 | diff.Change{Type: diff.CREATE, Path: []string{"values", "0"}, From: nil, To: "one"}, 810 | diff.Change{Type: diff.CREATE, Path: []string{"values", "1"}, From: nil, To: "two"}, 811 | diff.Change{Type: diff.CREATE, Path: []string{"values", "2"}, From: nil, To: "three"}, 812 | }, 813 | nil, 814 | }, 815 | { 816 | "struct-delete", diff.DELETE, tstruct{ID: "xxxxx", Name: "something", Value: 1, Values: []string{"one", "two", "three"}}, 817 | diff.Changelog{ 818 | diff.Change{Type: diff.DELETE, Path: []string{"id"}, From: "xxxxx", To: nil}, 819 | diff.Change{Type: diff.DELETE, Path: []string{"name"}, From: "something", To: nil}, 820 | diff.Change{Type: diff.DELETE, Path: []string{"value"}, From: 1, To: nil}, 821 | diff.Change{Type: diff.DELETE, Path: []string{"values", "0"}, From: "one", To: nil}, 822 | diff.Change{Type: diff.DELETE, Path: []string{"values", "1"}, From: "two", To: nil}, 823 | diff.Change{Type: diff.DELETE, Path: []string{"values", "2"}, From: "three", To: nil}, 824 | }, 825 | nil, 826 | }, 827 | } 828 | 829 | for _, tc := range cases { 830 | t.Run(tc.Name, func(t *testing.T) { 831 | cl, err := diff.StructValues(tc.ChangeType, []string{}, tc.X) 832 | 833 | assert.Equal(t, tc.Error, err) 834 | assert.Equal(t, len(tc.Changelog), len(cl)) 835 | 836 | for i, c := range cl { 837 | assert.Equal(t, tc.Changelog[i].Type, c.Type) 838 | assert.Equal(t, tc.Changelog[i].Path, c.Path) 839 | assert.Equal(t, tc.Changelog[i].From, c.From) 840 | assert.Equal(t, tc.Changelog[i].To, c.To) 841 | } 842 | }) 843 | } 844 | } 845 | 846 | func TestDifferReuse(t *testing.T) { 847 | d, err := diff.NewDiffer() 848 | require.Nil(t, err) 849 | 850 | cl, err := d.Diff([]string{"1", "2", "3"}, []string{"1"}) 851 | require.Nil(t, err) 852 | 853 | require.Len(t, cl, 2) 854 | 855 | assert.Equal(t, "2", cl[0].From) 856 | assert.Equal(t, nil, cl[0].To) 857 | assert.Equal(t, "3", cl[1].From) 858 | assert.Equal(t, nil, cl[1].To) 859 | 860 | cl, err = d.Diff([]string{"a", "b"}, []string{"a", "c"}) 861 | require.Nil(t, err) 862 | 863 | require.Len(t, cl, 1) 864 | 865 | assert.Equal(t, "b", cl[0].From) 866 | assert.Equal(t, "c", cl[0].To) 867 | } 868 | 869 | func TestDiffingOptions(t *testing.T) { 870 | d, err := diff.NewDiffer(diff.SliceOrdering(false)) 871 | require.Nil(t, err) 872 | 873 | assert.False(t, d.SliceOrdering) 874 | 875 | cl, err := d.Diff([]int{1, 2, 3}, []int{1, 3, 2}) 876 | require.Nil(t, err) 877 | 878 | assert.Len(t, cl, 0) 879 | 880 | d, err = diff.NewDiffer(diff.SliceOrdering(true)) 881 | require.Nil(t, err) 882 | 883 | assert.True(t, d.SliceOrdering) 884 | 885 | cl, err = d.Diff([]int{1, 2, 3}, []int{1, 3, 2}) 886 | require.Nil(t, err) 887 | 888 | assert.Len(t, cl, 2) 889 | 890 | // some other options.. 891 | } 892 | 893 | func TestDiffPrivateField(t *testing.T) { 894 | cl, err := diff.Diff(tstruct{private: 1}, tstruct{private: 3}) 895 | require.Nil(t, err) 896 | assert.Len(t, cl, 1) 897 | } 898 | 899 | type testType string 900 | type testTypeDiffer struct { 901 | DiffFunc (func(path []string, a, b reflect.Value, p interface{}) error) 902 | } 903 | 904 | func (o *testTypeDiffer) InsertParentDiffer(dfunc func(path []string, a, b reflect.Value, p interface{}) error) { 905 | o.DiffFunc = dfunc 906 | } 907 | 908 | func (o *testTypeDiffer) Match(a, b reflect.Value) bool { 909 | return diff.AreType(a, b, reflect.TypeOf(testType(""))) 910 | } 911 | func (o *testTypeDiffer) Diff(dt diff.DiffType, df diff.DiffFunc, cl *diff.Changelog, path []string, a, b reflect.Value, parent interface{}) error { 912 | if a.String() != "custom" && b.String() != "match" { 913 | cl.Add(diff.UPDATE, path, a.Interface(), b.Interface()) 914 | } 915 | return nil 916 | } 917 | 918 | func TestCustomDiffer(t *testing.T) { 919 | type custom struct { 920 | T testType 921 | } 922 | 923 | d, err := diff.NewDiffer( 924 | diff.CustomValueDiffers( 925 | &testTypeDiffer{}, 926 | ), 927 | ) 928 | require.Nil(t, err) 929 | 930 | cl, err := d.Diff(custom{"custom"}, custom{"match"}) 931 | require.Nil(t, err) 932 | 933 | assert.Len(t, cl, 0) 934 | 935 | d, err = diff.NewDiffer( 936 | diff.CustomValueDiffers( 937 | &testTypeDiffer{}, 938 | ), 939 | ) 940 | require.Nil(t, err) 941 | 942 | cl, err = d.Diff(custom{"same"}, custom{"same"}) 943 | require.Nil(t, err) 944 | 945 | assert.Len(t, cl, 1) 946 | } 947 | 948 | type testStringInterceptorDiffer struct { 949 | DiffFunc (func(path []string, a, b reflect.Value, p interface{}) error) 950 | } 951 | 952 | func (o *testStringInterceptorDiffer) InsertParentDiffer(dfunc func(path []string, a, b reflect.Value, p interface{}) error) { 953 | o.DiffFunc = dfunc 954 | } 955 | 956 | func (o *testStringInterceptorDiffer) Match(a, b reflect.Value) bool { 957 | return diff.AreType(a, b, reflect.TypeOf(testType(""))) 958 | } 959 | func (o *testStringInterceptorDiffer) Diff(dt diff.DiffType, df diff.DiffFunc, cl *diff.Changelog, path []string, a, b reflect.Value, parent interface{}) error { 960 | if dt.String() == "STRING" { 961 | // intercept the data 962 | aValue, aOk := a.Interface().(testType) 963 | bValue, bOk := b.Interface().(testType) 964 | 965 | if aOk && bOk { 966 | if aValue == "avalue" { 967 | aValue = testType(strings.ToUpper(string(aValue))) 968 | a = reflect.ValueOf(aValue) 969 | } 970 | 971 | if bValue == "bvalue" { 972 | bValue = testType(strings.ToUpper(string(aValue))) 973 | b = reflect.ValueOf(bValue) 974 | } 975 | } 976 | } 977 | 978 | // continue the diff logic passing the updated a/b values 979 | return df(path, a, b, parent) 980 | } 981 | 982 | func TestStringInterceptorDiffer(t *testing.T) { 983 | d, err := diff.NewDiffer( 984 | diff.CustomValueDiffers( 985 | &testStringInterceptorDiffer{}, 986 | ), 987 | ) 988 | require.Nil(t, err) 989 | 990 | cl, err := d.Diff(testType("avalue"), testType("bvalue")) 991 | require.Nil(t, err) 992 | 993 | assert.Len(t, cl, 0) 994 | } 995 | 996 | type RecursiveTestStruct struct { 997 | Id int 998 | Children []RecursiveTestStruct 999 | } 1000 | 1001 | type recursiveTestStructDiffer struct { 1002 | DiffFunc (func(path []string, a, b reflect.Value, p interface{}) error) 1003 | } 1004 | 1005 | func (o *recursiveTestStructDiffer) InsertParentDiffer(dfunc func(path []string, a, b reflect.Value, p interface{}) error) { 1006 | o.DiffFunc = dfunc 1007 | } 1008 | 1009 | func (o *recursiveTestStructDiffer) Match(a, b reflect.Value) bool { 1010 | return diff.AreType(a, b, reflect.TypeOf(RecursiveTestStruct{})) 1011 | } 1012 | 1013 | func (o *recursiveTestStructDiffer) Diff(dt diff.DiffType, df diff.DiffFunc, cl *diff.Changelog, path []string, a, b reflect.Value, parent interface{}) error { 1014 | if a.Kind() == reflect.Invalid { 1015 | cl.Add(diff.CREATE, path, nil, b.Interface()) 1016 | return nil 1017 | } 1018 | if b.Kind() == reflect.Invalid { 1019 | cl.Add(diff.DELETE, path, a.Interface(), nil) 1020 | return nil 1021 | } 1022 | var awt, bwt RecursiveTestStruct 1023 | awt, _ = a.Interface().(RecursiveTestStruct) 1024 | bwt, _ = b.Interface().(RecursiveTestStruct) 1025 | if awt.Id != bwt.Id { 1026 | cl.Add(diff.UPDATE, path, a.Interface(), b.Interface()) 1027 | } 1028 | for i := 0; i < a.NumField(); i++ { 1029 | field := a.Type().Field(i) 1030 | tname := field.Name 1031 | if tname != "Children" { 1032 | continue 1033 | } 1034 | af := a.Field(i) 1035 | bf := b.FieldByName(field.Name) 1036 | fpath := copyAppend(path, tname) 1037 | err := o.DiffFunc(fpath, af, bf, nil) 1038 | if err != nil { 1039 | return err 1040 | } 1041 | } 1042 | return nil 1043 | } 1044 | 1045 | func TestRecursiveCustomDiffer(t *testing.T) { 1046 | treeA := RecursiveTestStruct{ 1047 | Id: 1, 1048 | Children: []RecursiveTestStruct{}, 1049 | } 1050 | 1051 | treeB := RecursiveTestStruct{ 1052 | Id: 1, 1053 | Children: []RecursiveTestStruct{ 1054 | { 1055 | Id: 4, 1056 | Children: []RecursiveTestStruct{}, 1057 | }, 1058 | }, 1059 | } 1060 | d, err := diff.NewDiffer( 1061 | diff.CustomValueDiffers( 1062 | &recursiveTestStructDiffer{}, 1063 | ), 1064 | ) 1065 | require.Nil(t, err) 1066 | cl, err := d.Diff(treeA, treeB) 1067 | require.Nil(t, err) 1068 | assert.Len(t, cl, 1) 1069 | } 1070 | 1071 | func TestHandleDifferentTypes(t *testing.T) { 1072 | cases := []struct { 1073 | Name string 1074 | A, B interface{} 1075 | Changelog diff.Changelog 1076 | Error error 1077 | HandleTypeMismatch bool 1078 | }{ 1079 | { 1080 | "type-change-not-allowed-error", 1081 | 1, "1", 1082 | nil, 1083 | diff.ErrTypeMismatch, 1084 | false, 1085 | }, 1086 | { 1087 | "type-change-not-allowed-error-struct", 1088 | struct { 1089 | p1 string 1090 | p2 int 1091 | }{"1", 1}, 1092 | struct { 1093 | p1 string 1094 | p2 string 1095 | }{"1", "1"}, 1096 | nil, 1097 | diff.ErrTypeMismatch, 1098 | false, 1099 | }, 1100 | { 1101 | "type-change-allowed", 1102 | 1, "1", 1103 | diff.Changelog{ 1104 | diff.Change{Type: diff.UPDATE, Path: []string{}, From: 1, To: "1"}, 1105 | }, 1106 | nil, 1107 | true, 1108 | }, 1109 | { 1110 | "type-change-allowed-struct", 1111 | struct { 1112 | P1 string 1113 | P2 int 1114 | P3 map[string]string 1115 | }{"1", 1, map[string]string{"1": "1"}}, 1116 | struct { 1117 | P1 string 1118 | P2 string 1119 | P3 string 1120 | }{"1", "1", "1"}, 1121 | diff.Changelog{ 1122 | diff.Change{Type: diff.UPDATE, Path: []string{"P2"}, From: 1, To: "1"}, 1123 | diff.Change{Type: diff.UPDATE, Path: []string{"P3"}, From: map[string]string{"1": "1"}, To: "1"}, 1124 | }, 1125 | nil, 1126 | true, 1127 | }, 1128 | } 1129 | 1130 | for _, tc := range cases { 1131 | t.Run(tc.Name, func(t *testing.T) { 1132 | d, err := diff.NewDiffer(diff.AllowTypeMismatch(tc.HandleTypeMismatch)) 1133 | require.Nil(t, err) 1134 | cl, err := d.Diff(tc.A, tc.B) 1135 | 1136 | assert.Equal(t, tc.Error, err) 1137 | require.Equal(t, len(tc.Changelog), len(cl)) 1138 | 1139 | for i, c := range cl { 1140 | assert.Equal(t, tc.Changelog[i].Type, c.Type) 1141 | assert.Equal(t, tc.Changelog[i].Path, c.Path) 1142 | assert.Equal(t, tc.Changelog[i].From, c.From) 1143 | assert.Equal(t, tc.Changelog[i].To, c.To) 1144 | } 1145 | }) 1146 | } 1147 | } 1148 | 1149 | func copyAppend(src []string, elems ...string) []string { 1150 | dst := make([]string, len(src)+len(elems)) 1151 | copy(dst, src) 1152 | for i := len(src); i < len(src)+len(elems); i++ { 1153 | dst[i] = elems[i-len(src)] 1154 | } 1155 | return dst 1156 | } 1157 | --------------------------------------------------------------------------------