diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index ac4f6a2..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: 2 -jobs: - build: - docker: - - image: circleci/golang:1.11 - working_directory: /home/circleci/.go_workspace/src/github.com/r3labs/diff - steps: - - checkout - - run: - name: Install Dependencies - command: make dev-deps - - run: - name: Code Analysis - command: make lint - - run: - name: Unit Tests - command: make test diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b9b9965 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,18 @@ +name: CI + +on: [push, workflow_dispatch] + +jobs: + Test: + runs-on: ubuntu-latest + + container: + image: golang + + steps: + - uses: actions/checkout@v2 + + - name: Test + run: | + make deps + go test ./... diff --git a/.gitignore b/.gitignore index a1338d6..e04e61e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ +.idea/ + +patchflags_string.go diff --git a/Makefile b/Makefile index f16fa85..f119f8c 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,11 @@ install: go install -v ${LDFLAGS} +deps: + go get github.com/stretchr/testify + test: @go test -v -cover ./... cover: @go test -coverprofile cover.out - -dev-deps: - @go get -u github.com/stretchr/testify - @go get github.com/alecthomas/gometalinter - @gometalinter --install > /dev/null - -lint: - @gometalinter --vendor --disable-all --enable=errcheck --enable=golint --enable=vet ./... diff --git a/README.md b/README.md index 0248536..97aee65 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,16 @@ -# diff +# Diff [![PkgGoDev](https://pkg.go.dev/badge/github.com/r3labs/diff)](https://pkg.go.dev/github.com/r3labs/diff) [![Go Report Card](https://goreportcard.com/badge/github.com/r3labs/diff)](https://goreportcard.com/report/github.com/r3labs/diff) [![Build Status](https://travis-ci.com/r3labs/diff.svg?branch=master)](https://travis-ci.com/r3labs/diff) A library for diffing golang structures and values. Utilizing field tags and reflection, it is able to compare two structures of the same type and create a changelog of all modified values. The produced changelog can easily be serialized to json. -## Build status - -* Master [![CircleCI](https://circleci.com/gh/r3labs/diff/tree/master.svg?style=svg)](https://circleci.com/gh/r3labs/diff/tree/master) +NOTE: All active development now takes place on the v3 branch. ## Installation +For version 3: ``` -go get github.com/r3labs/diff +go get github.com/r3labs/diff/v3 ``` ## Changelog Format @@ -51,23 +50,32 @@ Change{ A diffable value can be/contain any of the following types: -* struct -* slice -* string -* int -* bool -* map -* pointer -### Tags +| Type | Supported | +| ------------ | --------- | +| struct | ✔ | +| slice | ✔ | +| string | ✔ | +| int | ✔ | +| bool | ✔ | +| map | ✔ | +| pointer | ✔ | +| custom types | ✔ | -In order for struct fields to be compared, they must be tagged with a given name. All tag values are prefixed with `diff`. i.e. `diff:"items"`. -* `-` : In the event that you want to exclude a value from the diff, you can use the tag `diff:"-"` and the field will be ignored. +Please see the docs for more supported types, options and features. -* `identifier` : If you need to compare arrays by a matching identifier and not based on order, you can specify the `identifier` tag. If an identifiable element is found in both the from and to structures, they will be directly compared. i.e. `diff:"name,identifier"` +### Tags -* `immutable` : Will omit this struct field from diffing. When using `diff.StructValues()` these values will be added to the returned changelog. It's usecase is for when we have nothing to compare a struct to and want to show all of its relevant values. +In order for struct fields to be compared, they must be tagged with a given name. All tag values are prefixed with `diff`. i.e. `diff:"items"`. + +| Tag | Usage | +| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `-` | Excludes a value from being diffed | +| `identifier` | If you need to compare arrays by a matching identifier and not based on order, you can specify the `identifier` tag. If an identifiable element is found in both the from and to structures, they will be directly compared. i.e. `diff:"name, identifier"` | +| `immutable` | Will omit this struct field from diffing. When using `diff.StructValues()` these values will be added to the returned changelog. It's use case is for when we have nothing to compare a struct to and want to show all of its relevant values. | +| `nocreate` | The default patch action is to allocate instances in the target strut, map or slice should they not exist. Adding this flag will tell patch to skip elements that it would otherwise need to allocate. This is separate from immutable, which is also honored while patching. | +| `omitunequal` | Patching is a 'best effort' operation, and will by default attempt to update the 'correct' member of the target even if the underlying value has already changed to something other than the value in the change log 'from'. This tag will selectively ignore values that are not a 100% match. | ## Usage @@ -76,7 +84,7 @@ In order for struct fields to be compared, they must be tagged with a given name Diffing a basic set of values can be accomplished using the diff functions. Any items that specify a "diff" tag using a name will be compared. ```go -import "github.com/r3labs/diff" +import "github.com/r3labs/diff/v3" type Order struct { ID string `diff:"id"` @@ -115,10 +123,35 @@ When marshalling the changelog to json, the output will look like: ### Options and Configuration -You can create a new instance of a differ that allows options to be set. +Options can be set on the differ at call time which effect how diff acts when building the change log. +```go +import "github.com/r3labs/diff/v3" + +type Order struct { + ID string `diff:"id"` + Items []int `diff:"items"` +} + +func main() { + a := Order{ + ID: "1234", + Items: []int{1, 2, 3, 4}, + } + + b := Order{ + ID: "1234", + Items: []int{1, 2, 4}, + } + + changelog, err := diff.Diff(a, b, diff.DisableStructValues(), diff.AllowTypeMismatch(true)) + ... +} +``` + +You can also create a new instance of a differ that allows options to be set. ```go -import "github.com/r3labs/diff" +import "github.com/r3labs/diff/v3" type Order struct { ID string `diff:"id"` @@ -136,10 +169,10 @@ func main() { Items: []int{1, 2, 4}, } - d, err := diff.NewDiffer(diff.SliceOrdering(true)) - if err != nil { - panic(err) - } + d, err := diff.NewDiffer(diff.SliceOrdering(true)) + if err != nil { + panic(err) + } changelog, err := d.Diff(a, b) ... @@ -150,7 +183,121 @@ Supported options are: `SliceOrdering` ensures that the ordering of items in a slice is taken into account +`DiscardComplexOrigin` is a directive to diff to omit additional origin information about structs. This alters the behavior of patch and can lead to some pitfalls and non-intuitive behavior if used. On the other hand, it can significantly reduce the memory footprint of large complex diffs. + +`AllowTypeMismatch` is a global directive to either allow (true) or not to allow (false) patch apply the changes if 'from' is not equal. This is effectively a global version of the omitunequal tag. + +`Filter` provides a callback that allows you to determine which fields the differ descends into + +`DisableStructValues` disables populating a separate change for each item in a struct, where the struct is being compared to a nil Value. + +`TagName` sets the tag name to use when getting field names and options. + +### Patch and merge support +Diff additionally supports merge and patch. Similar in concept to text patching / merging the Patch function, given +a change log and a target instance will make a _best effort_ to apply the changes in the change log to the variable +pointed to. The intention is that the target pointer is of the same type however, that doesn't necessarily have to be +true. For example, two slices of differing structs may be similar enough to apply changes to in a polymorphic way, and +patch will certainly try. + +The patch function doesn't actually fail, and even if there are errors, it may succeed sufficiently for the task at hand. +To accommodate this patch keeps track of each change log option it attempts to apply and reports the details of what +happened for further scrutiny. + +```go +import "github.com/r3labs/diff/v3" + +type Order struct { + ID string `diff:"id"` + Items []int `diff:"items"` +} + +func main() { + a := Order{ + ID: "1234", + Items: []int{1, 2, 3, 4}, + } + b := Order{ + ID: "1234", + Items: []int{1, 2, 4}, + } + + c := Order{} + changelog, err := diff.Diff(a, b) + + patchlog := diff.Patch(changelog, &c) + //Note the lack of an error. Patch is best effort and uses flags to indicate actions taken + //and keeps any errors encountered along the way for review + fmt.Printf("Encountered %d errors while patching", patchlog.ErrorCount()) + ... +} +``` + +Instances of differ with options set can also be used when patching. + +```go +package main + +import "github.com/r3labs/diff/v3" + +type Order struct { + ID string `json:"id"` + Items []int `json:"items"` +} + +func main() { + a := Order{ + ID: "1234", + Items: []int{1, 2, 3, 4}, + } + + b := Order{ + ID: "1234", + Items: []int{1, 2, 4}, + } + + d, _ := diff.NewDiffer(diff.TagName("json")) + + changelog, _ := d.Diff(a, b) + + d.Patch(changelog, &a) + // reflect.DeepEqual(a, b) == true +} + +``` + +As a convenience, there is a Merge function that allows one to take three interfaces and perform all the tasks at the same +time. + +```go +import "github.com/r3labs/diff/v3" + +type Order struct { + ID string `diff:"id"` + Items []int `diff:"items"` +} + +func main() { + a := Order{ + ID: "1234", + Items: []int{1, 2, 3, 4}, + } + + b := Order{ + ID: "1234", + Items: []int{1, 2, 4}, + } + + c := Order{} + patchlog, err := diff.Merge(a, b, &c) + if err != nil { + fmt.Printf("Error encountered while diffing a & b") + } + fmt.Printf("Encountered %d errors while patching", patchlog.ErrorCount()) + ... +} +``` ## Running Tests ``` diff --git a/change_value.go b/change_value.go new file mode 100644 index 0000000..b0f3a73 --- /dev/null +++ b/change_value.go @@ -0,0 +1,209 @@ +package diff + +import ( + "fmt" + "reflect" +) + +//ChangeValue is a specialized struct for monitoring patching +type ChangeValue struct { + parent *reflect.Value + target *reflect.Value + flags PatchFlags + change *Change + err error + pos int + index int + key reflect.Value +} + +//swap swaps out the target as we move down the path. Note that a nil +// check is foregone here due to the fact we control usage. +func (c *ChangeValue) swap(newTarget *reflect.Value) { + if newTarget.IsValid() { + c.ClearFlag(FlagInvalidTarget) + c.parent = c.target + c.target = newTarget + c.pos++ + } +} + +// Sets a flag on the node and saves the change +func (c *ChangeValue) SetFlag(flag PatchFlags) { + if c != nil { + c.flags = c.flags | flag + } +} + +//ClearFlag removes just a single flag +func (c *ChangeValue) ClearFlag(flag PatchFlags) { + if c != nil { + c.flags = c.flags &^ flag + } +} + +//HasFlag indicates if a flag is set on the node. returns false if node is bad +func (c *ChangeValue) HasFlag(flag PatchFlags) bool { + return (c.flags & flag) != 0 +} + +//IsValid echo for is valid +func (c *ChangeValue) IsValid() bool { + if c != nil { + return c.target.IsValid() || !c.HasFlag(FlagInvalidTarget) + } + return false +} + +//ParentKind - helps keep us nil safe +func (c ChangeValue) ParentKind() reflect.Kind { + if c.parent != nil { + return c.parent.Kind() + } + return reflect.Invalid +} + +//ParentLen is a nil safe parent length check +func (c ChangeValue) ParentLen() (ret int) { + if c.parent != nil && + (c.parent.Kind() == reflect.Slice || + c.parent.Kind() == reflect.Map) { + ret = c.parent.Len() + } + return +} + +//ParentSet - nil safe parent set +func (c *ChangeValue) ParentSet(value reflect.Value, convertCompatibleTypes bool) { + if c != nil && c.parent != nil { + defer func() { + if r := recover(); r != nil { + c.SetFlag(FlagParentSetFailed) + } + }() + + if convertCompatibleTypes { + if !value.Type().ConvertibleTo(c.parent.Type()) { + c.AddError(fmt.Errorf("Value of type %s is not convertible to %s", value.Type().String(), c.parent.Type().String())) + c.SetFlag(FlagParentSetFailed) + return + } + c.parent.Set(value.Convert(c.parent.Type())) + } else { + c.parent.Set(value) + } + c.SetFlag(FlagParentSetApplied) + } +} + +//Len echo for len +func (c ChangeValue) Len() int { + return c.target.Len() +} + +//Set echos reflect set +func (c *ChangeValue) Set(value reflect.Value, convertCompatibleTypes bool) { + if c == nil { + return + } + + defer func() { + if r := recover(); r != nil { + switch e := r.(type) { + case string: + c.AddError(NewError(e)) + case *reflect.ValueError: + c.AddError(NewError(e.Error())) + } + + c.SetFlag(FlagFailed) + } + }() + + if c.HasFlag(OptionImmutable) { + c.SetFlag(FlagIgnored) + return + } + + if convertCompatibleTypes { + if c.target.Kind() == reflect.Ptr && value.Kind() != reflect.Ptr { + if !value.IsValid() { + c.target.Set(reflect.Zero(c.target.Type())) + c.SetFlag(FlagApplied) + return + } else if !value.Type().ConvertibleTo(c.target.Elem().Type()) { + c.AddError(fmt.Errorf("Value of type %s is not convertible to %s", value.Type().String(), c.target.Type().String())) + c.SetFlag(FlagFailed) + return + } + + tv := reflect.New(c.target.Elem().Type()) + tv.Elem().Set(value.Convert(c.target.Elem().Type())) + c.target.Set(tv) + } else { + if !value.Type().ConvertibleTo(c.target.Type()) { + c.AddError(fmt.Errorf("Value of type %s is not convertible to %s", value.Type().String(), c.target.Type().String())) + c.SetFlag(FlagFailed) + return + } + + c.target.Set(value.Convert(c.target.Type())) + } + } else { + if value.IsValid() { + if c.target.Kind() == reflect.Ptr && value.Kind() != reflect.Ptr { + tv := reflect.New(value.Type()) + tv.Elem().Set(value) + c.target.Set(tv) + } else { + c.target.Set(value) + } + } else if c.target.Kind() == reflect.Ptr { + c.target.Set(reflect.Zero(c.target.Type())) + } else if !c.target.IsZero() { + t := c.target.Elem() + t.Set(reflect.Zero(t.Type())) + } + } + c.SetFlag(FlagApplied) +} + +//Index echo for index +func (c ChangeValue) Index(i int) reflect.Value { + return c.target.Index(i) +} + +//ParentIndex - get us the parent version, nil safe +func (c ChangeValue) ParentIndex(i int) (ret reflect.Value) { + if c.parent != nil { + ret = c.parent.Index(i) + } + return +} + +//Instance a new element of type for target. Taking the +//copy of the complex origin avoids the 'lack of data' issue +//present when allocating complex structs with slices and +//arrays +func (c ChangeValue) NewElement() reflect.Value { + ret := c.change.parent + if ret != nil { + return reflect.ValueOf(ret) + } + return reflect.New(c.target.Type().Elem()).Elem() +} + +//NewArrayElement gives us a dynamically typed new element +func (c ChangeValue) NewArrayElement() reflect.Value { + c.target.Set(reflect.Append(*c.target, c.NewElement())) + c.SetFlag(FlagCreated) + return c.Index(c.Len() - 1) +} + +//AddError appends errors to this change value +func (c *ChangeValue) AddError(err error) *ChangeValue { + if c != nil { + c.err = err + } + return c +} diff --git a/diff.go b/diff.go index 35fe0c2..3c2ba17 100644 --- a/diff.go +++ b/diff.go @@ -10,13 +10,8 @@ import ( "reflect" "strconv" "strings" -) -var ( - // ErrTypeMismatch Compared types do not match - ErrTypeMismatch = errors.New("types do not match") - // ErrInvalidChangeType The specified change values are not unsupported - ErrInvalidChangeType = errors.New("change type must be one of 'create' or 'delete'") + "github.com/vmihailenco/msgpack/v5" ) const ( @@ -28,11 +23,69 @@ const ( DELETE = "delete" ) +// DiffType represents an enum with all the supported diff types +type DiffType uint8 + +const ( + UNSUPPORTED DiffType = iota + STRUCT + SLICE + ARRAY + STRING + BOOL + INT + UINT + FLOAT + MAP + PTR + INTERFACE +) + +func (t DiffType) String() string { + switch t { + case STRUCT: + return "STRUCT" + case SLICE: + return "SLICE" + case ARRAY: + return "ARRAY" + case STRING: + return "STRING" + case BOOL: + return "BOOL" + case INT: + return "INT" + case UINT: + return "UINT" + case FLOAT: + return "FLOAT" + case MAP: + return "MAP" + case PTR: + return "PTR" + case INTERFACE: + return "INTERFACE" + default: + return "UNSUPPORTED" + } +} + +// DiffFunc represents the built-in diff functions +type DiffFunc func([]string, reflect.Value, reflect.Value, interface{}) error + // Differ a configurable diff instance type Differ struct { - SliceOrdering bool - DisableStructValues bool - cl Changelog + TagName string + SliceOrdering bool + DisableStructValues bool + customValueDiffers []ValueDiffer + cl Changelog + AllowTypeMismatch bool + DiscardParent bool + StructMapKeys bool + FlattenEmbeddedStructs bool + ConvertCompatibleTypes bool + Filter FilterFunc } // Changelog stores a list of changed items @@ -40,10 +93,18 @@ type Changelog []Change // Change stores information about a changed item type Change struct { - Type string `json:"type"` - Path []string `json:"path"` - From interface{} `json:"from"` - To interface{} `json:"to"` + Type string `json:"type"` + Path []string `json:"path"` + From interface{} `json:"from"` + To interface{} `json:"to"` + parent interface{} `json:"parent"` +} + +// ValueDiffer is an interface for custom differs +type ValueDiffer interface { + Match(a, b reflect.Value) bool + Diff(dt DiffType, df DiffFunc, cl *Changelog, path []string, a, b reflect.Value, parent interface{}) error + InsertParentDiffer(dfunc func(path []string, a, b reflect.Value, p interface{}) error) } // Changed returns true if both values differ @@ -53,15 +114,20 @@ func Changed(a, b interface{}) bool { } // Diff returns a changelog of all mutated values from both -func Diff(a, b interface{}) (Changelog, error) { - var d Differ - - return d.cl, d.diff([]string{}, reflect.ValueOf(a), reflect.ValueOf(b)) +func Diff(a, b interface{}, opts ...func(d *Differ) error) (Changelog, error) { + d, err := NewDiffer(opts...) + if err != nil { + return nil, err + } + return d.Diff(a, b) } // NewDiffer creates a new configurable diffing object func NewDiffer(opts ...func(d *Differ) error) (*Differ, error) { - var d Differ + d := Differ{ + TagName: "diff", + DiscardParent: false, + } for _, opt := range opts { err := opt(&d) @@ -73,22 +139,31 @@ func NewDiffer(opts ...func(d *Differ) error) (*Differ, error) { return &d, nil } +// FilterFunc is a function that determines whether to descend into a struct field. +// parent is the struct being examined and field is a field on that struct. path +// is the path to the field from the root of the diff. +type FilterFunc func(path []string, parent reflect.Type, field reflect.StructField) bool + // StructValues gets all values from a struct // values are stored as "created" or "deleted" entries in the changelog, // depending on the change type specified func StructValues(t string, path []string, s interface{}) (Changelog, error) { - var d Differ + d := Differ{ + TagName: "diff", + DiscardParent: false, + } + v := reflect.ValueOf(s) return d.cl, d.structValues(t, path, v) } -// Filter filter changes based on path. Paths may contain valid regexp to match items -func (cl *Changelog) Filter(path []string) Changelog { +// FilterOut filter out the changes based on path. Paths may contain valid regexp to match items +func (cl *Changelog) FilterOut(path []string) Changelog { var ncl Changelog for _, c := range *cl { - if pathmatch(path, c.Path) { + if !pathmatch(path, c.Path) { ncl = append(ncl, c) } } @@ -96,54 +171,113 @@ func (cl *Changelog) Filter(path []string) Changelog { return ncl } -// Diff returns a changelog of all mutated values from both -func (d *Differ) Diff(a, b interface{}) (Changelog, error) { - return d.cl, d.diff([]string{}, reflect.ValueOf(a), reflect.ValueOf(b)) -} +// Filter filter changes based on path. Paths may contain valid regexp to match items +func (cl *Changelog) Filter(path []string) Changelog { + var ncl Changelog -func (d *Differ) diff(path []string, a, b reflect.Value) error { - // check if types match or are - if invalid(a, b) { - return ErrTypeMismatch + for _, c := range *cl { + if pathmatch(path, c.Path) { + ncl = append(ncl, c) + } } + return ncl +} + +func (d *Differ) getDiffType(a, b reflect.Value) (DiffType, DiffFunc) { switch { case are(a, b, reflect.Struct, reflect.Invalid): - return d.diffStruct(path, a, b) + return STRUCT, d.diffStruct case are(a, b, reflect.Slice, reflect.Invalid): - return d.diffSlice(path, a, b) + return SLICE, d.diffSlice + case are(a, b, reflect.Array, reflect.Invalid): + return ARRAY, d.diffSlice case are(a, b, reflect.String, reflect.Invalid): - return d.diffString(path, a, b) + return STRING, d.diffString case are(a, b, reflect.Bool, reflect.Invalid): - return d.diffBool(path, a, b) + return BOOL, d.diffBool case are(a, b, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Invalid): - return d.diffInt(path, a, b) + return INT, d.diffInt case are(a, b, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Invalid): - return d.diffUint(path, a, b) + return UINT, d.diffUint case are(a, b, reflect.Float32, reflect.Float64, reflect.Invalid): - return d.diffFloat(path, a, b) + return FLOAT, d.diffFloat case are(a, b, reflect.Map, reflect.Invalid): - return d.diffMap(path, a, b) + return MAP, d.diffMap case are(a, b, reflect.Ptr, reflect.Invalid): - return d.diffPtr(path, a, b) + return PTR, d.diffPtr case are(a, b, reflect.Interface, reflect.Invalid): - return d.diffInterface(path, a, b) + return INTERFACE, d.diffInterface default: + return UNSUPPORTED, nil + } +} + +// Diff returns a changelog of all mutated values from both +func (d *Differ) Diff(a, b interface{}) (Changelog, error) { + // reset the state of the diff + d.cl = Changelog{} + + return d.cl, d.diff([]string{}, reflect.ValueOf(a), reflect.ValueOf(b), nil) +} + +func (d *Differ) diff(path []string, a, b reflect.Value, parent interface{}) error { + + //look and see if we need to discard the parent + if parent != nil { + if d.DiscardParent || reflect.TypeOf(parent).Kind() != reflect.Struct { + parent = nil + } + } + + // check if types match or are + if invalid(a, b) { + if d.AllowTypeMismatch { + d.cl.Add(UPDATE, path, a.Interface(), b.Interface()) + return nil + } + return ErrTypeMismatch + } + + // get the diff type and the corresponding built-int diff function to handle this type + diffType, diffFunc := d.getDiffType(a, b) + + // first go through custom diff functions + if len(d.customValueDiffers) > 0 { + for _, vd := range d.customValueDiffers { + if vd.Match(a, b) { + err := vd.Diff(diffType, diffFunc, &d.cl, path, a, b, parent) + if err != nil { + return err + } + return nil + } + } + } + + // then built-in diff functions + if diffType == UNSUPPORTED { return errors.New("unsupported type: " + a.Kind().String()) } + + return diffFunc(path, a, b, parent) } -func (cl *Changelog) add(t string, path []string, from, to interface{}) { - (*cl) = append((*cl), Change{ +func (cl *Changelog) Add(t string, path []string, ftco ...interface{}) { + change := Change{ Type: t, Path: path, - From: from, - To: to, - }) + From: ftco[0], + To: ftco[1], + } + if len(ftco) > 2 { + change.parent = ftco[2] + } + (*cl) = append((*cl), change) } -func tagName(f reflect.StructField) string { - t := f.Tag.Get("diff") +func tagName(tag string, f reflect.StructField) string { + t := f.Tag.Get(tag) parts := strings.Split(t, ",") if len(parts) < 1 { @@ -153,9 +287,13 @@ func tagName(f reflect.StructField) string { return parts[0] } -func identifier(v reflect.Value) interface{} { +func identifier(tag string, v reflect.Value) interface{} { + if v.Kind() != reflect.Struct { + return nil + } + for i := 0; i < v.NumField(); i++ { - if hasTagOption(v.Type().Field(i), "identifier") { + if hasTagOption(tag, v.Type().Field(i), "identifier") { return v.Field(i).Interface() } } @@ -163,8 +301,8 @@ func identifier(v reflect.Value) interface{} { return nil } -func hasTagOption(f reflect.StructField, opt string) bool { - parts := strings.Split(f.Tag.Get("diff"), ",") +func hasTagOption(tag string, f reflect.StructField, opt string) bool { + parts := strings.Split(f.Tag.Get(tag), ",") if len(parts) < 2 { return false } @@ -194,12 +332,27 @@ func swapChange(t string, c Change) Change { return nc } +func idComplex(v interface{}) string { + switch v := v.(type) { + case string: + return v + case int: + return strconv.Itoa(v) + default: + b, err := msgpack.Marshal(v) + if err != nil { + panic(err) + } + return string(b) + } + +} func idstring(v interface{}) string { - switch v.(type) { + switch v := v.(type) { case string: - return v.(string) + return v case int: - return strconv.Itoa(v.(int)) + return strconv.Itoa(v) default: return fmt.Sprint(v) } @@ -235,7 +388,7 @@ func are(a, b reflect.Value, kinds ...reflect.Kind) bool { return amatch && bmatch } -func areType(a, b reflect.Value, types ...reflect.Type) bool { +func AreType(a, b reflect.Value, types ...reflect.Type) bool { var amatch, bmatch bool for _, t := range types { diff --git a/diff_bool.go b/diff_bool.go index 86c3cb4..0e30503 100644 --- a/diff_bool.go +++ b/diff_bool.go @@ -6,14 +6,14 @@ package diff import "reflect" -func (d *Differ) diffBool(path []string, a, b reflect.Value) error { +func (d *Differ) diffBool(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { - d.cl.add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, exportInterface(b)) return nil } if b.Kind() == reflect.Invalid { - d.cl.add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, exportInterface(a), nil) return nil } @@ -22,7 +22,7 @@ func (d *Differ) diffBool(path []string, a, b reflect.Value) error { } if a.Bool() != b.Bool() { - d.cl.add(UPDATE, path, a.Interface(), b.Interface()) + d.cl.Add(UPDATE, path, exportInterface(a), exportInterface(b), parent) } return nil diff --git a/diff_comparative.go b/diff_comparative.go index 83cad41..7359d17 100644 --- a/diff_comparative.go +++ b/diff_comparative.go @@ -8,10 +8,14 @@ import ( "reflect" ) -func (d *Differ) diffComparative(path []string, c *ComparativeList) error { +func (d *Differ) diffComparative(path []string, c *ComparativeList, parent interface{}) error { for _, k := range c.keys { - fpath := copyAppend(path, idstring(k)) + id := idstring(k) + if d.StructMapKeys { + id = idComplex(k) + } + fpath := copyAppend(path, id) nv := reflect.ValueOf(nil) if c.m[k].A == nil { @@ -22,7 +26,7 @@ func (d *Differ) diffComparative(path []string, c *ComparativeList) error { c.m[k].B = &nv } - err := d.diff(fpath, *c.m[k].A, *c.m[k].B) + err := d.diff(fpath, *c.m[k].A, *c.m[k].B, parent) if err != nil { return err } @@ -31,13 +35,13 @@ func (d *Differ) diffComparative(path []string, c *ComparativeList) error { return nil } -func comparative(a, b reflect.Value) bool { +func (d *Differ) comparative(a, b reflect.Value) bool { if a.Len() > 0 { ae := a.Index(0) ak := getFinalValue(ae) if ak.Kind() == reflect.Struct { - if identifier(ak) != nil { + if identifier(d.TagName, ak) != nil { return true } } @@ -48,7 +52,7 @@ func comparative(a, b reflect.Value) bool { bk := getFinalValue(be) if bk.Kind() == reflect.Struct { - if identifier(bk) != nil { + if identifier(d.TagName, bk) != nil { return true } } diff --git a/diff_examples_test.go b/diff_examples_test.go index d492b7b..e9d6e8c 100644 --- a/diff_examples_test.go +++ b/diff_examples_test.go @@ -1,9 +1,487 @@ -package diff +package diff_test import ( "fmt" + "math/big" + "reflect" + + "github.com/r3labs/diff/v3" ) +//Try to do a bunch of stuff that will result in some or all failures +//when trying to apply either a valid or invalid changelog +func ExamplePatchWithErrors() { + + type Fruit struct { + ID int `diff:"ID" json:"Identifier"` + Name string `diff:"name"` + Healthy bool `diff:"-"` + Nutrients []string `diff:"nutrients"` + Labels map[string]int `diff:"labs"` + } + + type Bat struct { + ID string `diff:"ID"` + Name string `diff:"-"` + } + + a := Fruit{ + ID: 1, + Name: "Green Apple", + Healthy: true, + Nutrients: []string{ + "vitamin a", + "vitamin b", + "vitamin c", + "vitamin d", + }, + Labels: make(map[string]int), + } + a.Labels["likes"] = 10 + a.Labels["colors"] = 2 + + b := Fruit{ + ID: 2, + Name: "Red Apple", + Healthy: true, + Nutrients: []string{ + "vitamin c", + "vitamin d", + "vitamin e", + }, + Labels: make(map[string]int), + } + b.Labels["forests"] = 1223 + b.Labels["colors"] = 1222 + + c := Fruit{ + Labels: make(map[string]int), + Nutrients: []string{ + "vitamin c", + "vitamin d", + "vitamin a", + }, + } + c.Labels["likes"] = 21 + c.Labels["colors"] = 42 + + d := Bat{ + ID: "first", + Name: "second", + } + + changelog, err := diff.Diff(a, b) + if err != nil { + panic(err) + } + + //This fails in total because c is not assignable (passed by Value) + patchLog := diff.Patch(changelog, c) + + //this also demonstrated the nested errors with 'next' + + errors := patchLog[0].Errors.(*diff.DiffError) + + //we can also continue to nest errors if we like + message := errors.WithCause(diff.NewError("This is a custom message")). + WithCause(fmt.Errorf("this is an error from somewhere else but still compatible")). + Error() + + //invoke a few failures, i.e. bad changelog + changelog[2].Path[1] = "bad index" + changelog[3].Path[0] = "bad struct field" + + patchLog = diff.Patch(changelog, &c) + + patchLog, _ = diff.Merge(a, nil, &c) + + patchLog, _ = diff.Merge(a, d, &c) + + //try patching a string + patchLog = diff.Patch(changelog, message) + + //test an invalid change Value + var bad *diff.ChangeValue + if bad.IsValid() { + fmt.Print("this should never happen") + } + + //Output: +} + +//ExampleMerge demonstrates how to use the Merge function +func ExampleMerge() { + type Fruit struct { + ID int `diff:"ID" json:"Identifier"` + Name string `diff:"name"` + Healthy bool `diff:"healthy"` + Nutrients []string `diff:"nutrients,create,omitunequal"` + Labels map[string]int `diff:"labs,create"` + } + + a := Fruit{ + ID: 1, + Name: "Green Apple", + Healthy: true, + Nutrients: []string{ + "vitamin a", + "vitamin b", + "vitamin c", + "vitamin d", + }, + Labels: make(map[string]int), + } + a.Labels["likes"] = 10 + a.Labels["colors"] = 2 + + b := Fruit{ + ID: 2, + Name: "Red Apple", + Healthy: true, + Nutrients: []string{ + "vitamin c", + "vitamin d", + "vitamin e", + }, + Labels: make(map[string]int), + } + b.Labels["forests"] = 1223 + b.Labels["colors"] = 1222 + + c := Fruit{ + Labels: make(map[string]int), + Nutrients: []string{ + "vitamin a", + "vitamin c", + "vitamin d", + }, + } + c.Labels["likes"] = 21 + c.Labels["colors"] = 42 + + //the only error that can happen here comes from the diff step + patchLog, _ := diff.Merge(a, b, &c) + + //Note that unlike our patch version we've not included 'create' in the + //tag for nutrients. This will omit "vitamin e" from ending up in c + fmt.Printf("%#v", len(patchLog)) + + //Output: 8 +} + +//ExamplePrimitiveSlice demonstrates working with arrays and primitive values +func ExamplePrimitiveSlice() { + sla := []string{ + "this", + "is", + "a", + "simple", + } + + slb := []string{ + "slice", + "That", + "can", + "be", + "diff'ed", + } + + slc := []string{ + "ok", + } + + patch, err := diff.Diff(sla, slb, diff.StructMapKeySupport()) + if err != nil { + fmt.Print("failed to diff sla and slb") + } + cl := diff.Patch(patch, &slc) + + //now the other way, round + sla = []string{ + "slice", + "That", + "can", + "be", + "diff'ed", + } + slb = []string{ + "this", + "is", + "a", + "simple", + } + + patch, err = diff.Diff(sla, slb) + if err != nil { + fmt.Print("failed to diff sla and slb") + } + cl = diff.Patch(patch, &slc) + + //and finally a clean view + sla = []string{ + "slice", + "That", + "can", + "be", + "diff'ed", + } + slb = []string{} + + patch, err = diff.Diff(sla, slb) + if err != nil { + fmt.Print("failed to diff sla and slb") + } + cl = diff.Patch(patch, &slc) + + fmt.Printf("%d changes made to string array; %v", len(cl), slc) + + //Output: 5 changes made to string array; [simple a] +} + +//ExampleComplexMapPatch demonstrates how to use the Patch function for complex slices +//NOTE: There is a potential pitfall here, take a close look at b[2]. If patching the +// original, the operation will work intuitively however, in a merge situation we +// may not get everything we expect because it's a true diff between a and b and +// the diff log will not contain enough information to fully recreate b from an +// empty slice. This is exemplified in that the test "colors" is dropped in element +// 3 of c. Change "colors" to "color" and see what happens. Keep in mind this only +// happens when we need to allocate a new complex element. In normal operations we +// fix for this by keeping a copy of said element in the diff log (as parent) and +// allocate such an element as a whole copy prior to applying any updates? +// +// The new default is to carry this information forward, we invoke this pitfall +// by creating such a situation and explicitly telling diff to discard the parent +// In memory constrained environments if the developer is careful, they can use +// the discard feature but unless you REALLY understand what's happening here, use +// the default. +func ExampleComplexSlicePatch() { + + type Content struct { + Text string `diff:",create"` + Number int `diff:",create"` + } + type Attributes struct { + Labels []Content `diff:",create"` + } + + a := Attributes{ + Labels: []Content{ + { + Text: "likes", + Number: 10, + }, + { + Text: "forests", + Number: 10, + }, + { + Text: "colors", + Number: 2, + }, + }, + } + + b := Attributes{ + Labels: []Content{ + { + Text: "forests", + Number: 14, + }, + { + Text: "location", + Number: 0x32, + }, + { + Text: "colors", + Number: 1222, + }, + { + Text: "trees", + Number: 34, + }, + }, + } + c := Attributes{} + + changelog, err := diff.Diff(a, b, diff.DiscardComplexOrigin(), diff.StructMapKeySupport()) + if err != nil { + panic(err) + } + + patchLog := diff.Patch(changelog, &c) + + fmt.Printf("Patched %d entries and encountered %d errors", len(patchLog), patchLog.ErrorCount()) + + //Output: Patched 7 entries and encountered 3 errors +} + +//ExampleComplexMapPatch demonstrates how to use the Patch function for complex slices. +func ExampleComplexMapPatch() { + + type Key struct { + Value string + weight int + } + type Content struct { + Text string + Number float64 + WholeNumber int + } + type Attributes struct { + Labels map[Key]Content + } + + a := Attributes{ + Labels: make(map[Key]Content), + } + a.Labels[Key{Value: "likes"}] = Content{ + WholeNumber: 10, + Number: 23.4, + } + + a.Labels[Key{Value: "colors"}] = Content{ + WholeNumber: 2, + } + + b := Attributes{ + Labels: make(map[Key]Content), + } + b.Labels[Key{Value: "forests"}] = Content{ + Text: "Sherwood", + } + b.Labels[Key{Value: "colors"}] = Content{ + Number: 1222, + } + b.Labels[Key{Value: "latitude"}] = Content{ + Number: 38.978797, + } + b.Labels[Key{Value: "longitude"}] = Content{ + Number: -76.490986, + } + + //c := Attributes{} + c := Attributes{ + Labels: make(map[Key]Content), + } + c.Labels[Key{Value: "likes"}] = Content{ + WholeNumber: 210, + Number: 23.4453, + } + + changelog, err := diff.Diff(a, b) + if err != nil { + panic(err) + } + + patchLog := diff.Patch(changelog, &c) + + fmt.Printf("%#v", len(patchLog)) + + //Output: 7 +} + +//ExamplePatch demonstrates how to use the Patch function +func ExamplePatch() { + + type Key struct { + value string + weight int + } + type Cycle struct { + Name string `diff:"name,create"` + Count int `diff:"count,create"` + } + type Fruit struct { + ID int `diff:"ID" json:"Identifier"` + Name string `diff:"name"` + Healthy bool `diff:"healthy"` + Nutrients []string `diff:"nutrients,create,omitunequal"` + Labels map[Key]Cycle `diff:"labs,create"` + Cycles []Cycle `diff:"cycles,immutable"` + Weights []int + } + + a := Fruit{ + ID: 1, + Name: "Green Apple", + Healthy: true, + Nutrients: []string{ + "vitamin a", + "vitamin b", + "vitamin c", + "vitamin d", + }, + Labels: make(map[Key]Cycle), + } + a.Labels[Key{value: "likes"}] = Cycle{ + Count: 10, + } + a.Labels[Key{value: "colors"}] = Cycle{ + Count: 2, + } + + b := Fruit{ + ID: 2, + Name: "Red Apple", + Healthy: true, + Nutrients: []string{ + "vitamin c", + "vitamin d", + "vitamin e", + }, + Labels: make(map[Key]Cycle), + Weights: []int{ + 1, + 2, + 3, + 4, + }, + } + b.Labels[Key{value: "forests"}] = Cycle{ + Count: 1223, + } + b.Labels[Key{value: "colors"}] = Cycle{ + Count: 1222, + } + + c := Fruit{ + //Labels: make(map[string]int), + Nutrients: []string{ + "vitamin a", + "vitamin c", + "vitamin d", + }, + } + //c.Labels["likes"] = 21 + + d := a + d.Cycles = []Cycle{ + Cycle{ + Name: "First", + Count: 45, + }, + Cycle{ + Name: "Third", + Count: 4, + }, + } + d.Nutrients = append(d.Nutrients, "minerals") + + changelog, err := diff.Diff(a, b) + if err != nil { + panic(err) + } + + patchLog := diff.Patch(changelog, &c) + + changelog, _ = diff.Diff(a, d) + patchLog = diff.Patch(changelog, &c) + + fmt.Printf("%#v", len(patchLog)) + + //Output: 1 +} + func ExampleDiff() { type Tag struct { Name string `diff:"name,identifier"` @@ -55,7 +533,7 @@ func ExampleDiff() { }, } - changelog, err := Diff(a, b) + changelog, err := diff.Diff(a, b) if err != nil { panic(err) } @@ -63,3 +541,71 @@ func ExampleDiff() { fmt.Printf("%#v", changelog) // Produces: diff.Changelog{diff.Change{Type:"update", Path:[]string{"id"}, From:1, To:2}, diff.Change{Type:"update", Path:[]string{"name"}, From:"Green Apple", To:"Red Apple"}, diff.Change{Type:"create", Path:[]string{"nutrients", "2"}, From:interface {}(nil), To:"vitamin e"}, diff.Change{Type:"create", Path:[]string{"tags", "popularity"}, From:interface {}(nil), To:main.Tag{Name:"popularity", Value:"high"}}} } + +func ExampleFilter() { + type Tag struct { + Name string `diff:"name,identifier"` + Value string `diff:"value"` + } + + type Fruit struct { + ID int `diff:"id"` + Name string `diff:"name"` + Healthy bool `diff:"healthy"` + Nutrients []string `diff:"nutrients"` + Tags []Tag `diff:"tags"` + } + + a := Fruit{ + ID: 1, + Name: "Green Apple", + Healthy: true, + Nutrients: []string{ + "vitamin c", + "vitamin d", + }, + } + + b := Fruit{ + ID: 2, + Name: "Red Apple", + Healthy: true, + Nutrients: []string{ + "vitamin c", + "vitamin d", + "vitamin e", + }, + } + + d, err := diff.NewDiffer(diff.Filter(func(path []string, parent reflect.Type, field reflect.StructField) bool { + return field.Name != "Name" + })) + if err != nil { + panic(err) + } + + changelog, err := d.Diff(a, b) + if err != nil { + panic(err) + } + + fmt.Printf("%#v", changelog) + // Output: diff.Changelog{diff.Change{Type:"update", Path:[]string{"id"}, From:1, To:2, parent:diff_test.Fruit{ID:1, Name:"Green Apple", Healthy:true, Nutrients:[]string{"vitamin c", "vitamin d"}, Tags:[]diff_test.Tag(nil)}}, diff.Change{Type:"create", Path:[]string{"nutrients", "2"}, From:interface {}(nil), To:"vitamin e", parent:interface {}(nil)}} +} + +func ExamplePrivatePtr() { + type number struct { + value *big.Int + exp int32 + } + a := number{} + b := number{value: big.NewInt(111)} + + changelog, err := diff.Diff(a, b) + if err != nil { + panic(err) + } + + fmt.Printf("%#v", changelog) + // Output: diff.Changelog{diff.Change{Type:"update", Path:[]string{"value"}, From:interface {}(nil), To:111, parent:diff_test.number{value:(*big.Int)(nil), exp:0}}} +} diff --git a/diff_float.go b/diff_float.go index 602f97d..9494365 100644 --- a/diff_float.go +++ b/diff_float.go @@ -8,14 +8,14 @@ import ( "reflect" ) -func (d *Differ) diffFloat(path []string, a, b reflect.Value) error { +func (d *Differ) diffFloat(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { - d.cl.add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, exportInterface(b)) return nil } if b.Kind() == reflect.Invalid { - d.cl.add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, exportInterface(a), nil) return nil } @@ -24,7 +24,11 @@ func (d *Differ) diffFloat(path []string, a, b reflect.Value) error { } if a.Float() != b.Float() { - d.cl.add(UPDATE, path, a.Interface(), b.Interface()) + if a.CanInterface() { + d.cl.Add(UPDATE, path, exportInterface(a), exportInterface(b), parent) + } else { + d.cl.Add(UPDATE, path, a.Float(), b.Float(), parent) + } } return nil diff --git a/diff_int.go b/diff_int.go index b436a78..3658bf7 100644 --- a/diff_int.go +++ b/diff_int.go @@ -8,14 +8,14 @@ import ( "reflect" ) -func (d *Differ) diffInt(path []string, a, b reflect.Value) error { +func (d *Differ) diffInt(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { - d.cl.add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, exportInterface(b)) return nil } if b.Kind() == reflect.Invalid { - d.cl.add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, exportInterface(a), nil) return nil } @@ -24,7 +24,11 @@ func (d *Differ) diffInt(path []string, a, b reflect.Value) error { } if a.Int() != b.Int() { - d.cl.add(UPDATE, path, a.Interface(), b.Interface()) + if a.CanInterface() { + d.cl.Add(UPDATE, path, exportInterface(a), exportInterface(b), parent) + } else { + d.cl.Add(UPDATE, path, a.Int(), b.Int(), parent) + } } return nil diff --git a/diff_interface.go b/diff_interface.go index 5ddedf8..ef6cde8 100644 --- a/diff_interface.go +++ b/diff_interface.go @@ -6,14 +6,14 @@ package diff import "reflect" -func (d *Differ) diffInterface(path []string, a, b reflect.Value) error { +func (d *Differ) diffInterface(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { - d.cl.add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, exportInterface(b)) return nil } if b.Kind() == reflect.Invalid { - d.cl.add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, exportInterface(a), nil) return nil } @@ -26,14 +26,14 @@ func (d *Differ) diffInterface(path []string, a, b reflect.Value) error { } if a.IsNil() { - d.cl.add(UPDATE, path, nil, b.Interface()) + d.cl.Add(UPDATE, path, nil, exportInterface(b), parent) return nil } if b.IsNil() { - d.cl.add(UPDATE, path, a.Interface(), nil) + d.cl.Add(UPDATE, path, exportInterface(a), nil, parent) return nil } - return d.diff(path, a.Elem(), b.Elem()) + return d.diff(path, a.Elem(), b.Elem(), parent) } diff --git a/diff_map.go b/diff_map.go index 815cbeb..675ff93 100644 --- a/diff_map.go +++ b/diff_map.go @@ -7,9 +7,11 @@ package diff import ( "fmt" "reflect" + + "github.com/vmihailenco/msgpack/v5" ) -func (d *Differ) diffMap(path []string, a, b reflect.Value) error { +func (d *Differ) diffMap(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { return d.mapValues(CREATE, path, b) } @@ -22,15 +24,15 @@ func (d *Differ) diffMap(path []string, a, b reflect.Value) error { for _, k := range a.MapKeys() { ae := a.MapIndex(k) - c.addA(k.Interface(), &ae) + c.addA(exportInterface(k), &ae) } for _, k := range b.MapKeys() { be := b.MapIndex(k) - c.addB(k.Interface(), &be) + c.addB(exportInterface(k), &be) } - return d.diffComparative(path, c) + return d.diffComparative(path, c, exportInterface(a)) } func (d *Differ) mapValues(t string, path []string, a reflect.Value) error { @@ -52,7 +54,17 @@ func (d *Differ) mapValues(t string, path []string, a reflect.Value) error { ae := a.MapIndex(k) xe := x.MapIndex(k) - err := d.diff(append(path, fmt.Sprint(k.Interface())), xe, ae) + var err error + if d.StructMapKeys { + //it's not enough to turn k to a string, we need to able to marshal a type when + //we apply it in patch so... we'll marshal it to JSON + var b []byte + if b, err = msgpack.Marshal(k.Interface()); err == nil { + err = d.diff(append(path, string(b)), xe, ae, a.Interface()) + } + } else { + err = d.diff(append(path, fmt.Sprint(k.Interface())), xe, ae, a.Interface()) + } if err != nil { return err } diff --git a/diff_pointer.go b/diff_pointer.go index 342f782..7c9d875 100644 --- a/diff_pointer.go +++ b/diff_pointer.go @@ -6,20 +6,29 @@ package diff import ( "reflect" + "unsafe" ) -func (d *Differ) diffPtr(path []string, a, b reflect.Value) error { +var isExportFlag uintptr = (1 << 5) | (1 << 6) + +func (d *Differ) diffPtr(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() != b.Kind() { if a.Kind() == reflect.Invalid { if !b.IsNil() { - return d.diff(path, reflect.ValueOf(nil), reflect.Indirect(b)) + return d.diff(path, reflect.ValueOf(nil), reflect.Indirect(b), parent) } + + d.cl.Add(CREATE, path, nil, exportInterface(b), parent) + return nil } if b.Kind() == reflect.Invalid { if !a.IsNil() { - return d.diff(path, reflect.Indirect(a), reflect.ValueOf(nil)) + return d.diff(path, reflect.Indirect(a), reflect.ValueOf(nil), parent) } + + d.cl.Add(DELETE, path, exportInterface(a), nil, parent) + return nil } return ErrTypeMismatch @@ -30,14 +39,22 @@ func (d *Differ) diffPtr(path []string, a, b reflect.Value) error { } if a.IsNil() { - d.cl.add(UPDATE, path, nil, b.Interface()) + d.cl.Add(UPDATE, path, nil, exportInterface(b), parent) return nil } if b.IsNil() { - d.cl.add(UPDATE, path, a.Interface(), nil) + d.cl.Add(UPDATE, path, exportInterface(a), nil, parent) return nil } - return d.diff(path, reflect.Indirect(a), reflect.Indirect(b)) + return d.diff(path, reflect.Indirect(a), reflect.Indirect(b), parent) +} + +func exportInterface(v reflect.Value) interface{} { + if !v.CanInterface() { + flagTmp := (*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + 2*unsafe.Sizeof(uintptr(0)))) + *flagTmp = (*flagTmp) & (^isExportFlag) + } + return v.Interface() } diff --git a/diff_slice.go b/diff_slice.go index 04d07cc..3fd281b 100644 --- a/diff_slice.go +++ b/diff_slice.go @@ -8,12 +8,22 @@ import ( "reflect" ) -func (d *Differ) diffSlice(path []string, a, b reflect.Value) error { +func (d *Differ) diffSlice(path []string, a, b reflect.Value, parent interface{}) error { + if a.Kind() == reflect.Invalid { + d.cl.Add(CREATE, path, nil, exportInterface(b)) + return nil + } + + if b.Kind() == reflect.Invalid { + d.cl.Add(DELETE, path, exportInterface(a), nil) + return nil + } + if a.Kind() != b.Kind() { return ErrTypeMismatch } - if comparative(a, b) { + if d.comparative(a, b) { return d.diffSliceComparative(path, a, b) } @@ -27,7 +37,7 @@ func (d *Differ) diffSliceGeneric(path []string, a, b reflect.Value) error { for i := 0; i < a.Len(); i++ { ae := a.Index(i) - if (d.SliceOrdering && !hasAtSameIndex(b, ae, i)) || (!d.SliceOrdering && !slice.has(b, ae)) { + if (d.SliceOrdering && !hasAtSameIndex(b, ae, i)) || (!d.SliceOrdering && !slice.has(b, ae, d)) { missing.addA(i, &ae) } } @@ -36,7 +46,7 @@ func (d *Differ) diffSliceGeneric(path []string, a, b reflect.Value) error { for i := 0; i < b.Len(); i++ { be := b.Index(i) - if (d.SliceOrdering && !hasAtSameIndex(a, be, i)) || (!d.SliceOrdering && !slice.has(a, be)) { + if (d.SliceOrdering && !hasAtSameIndex(a, be, i)) || (!d.SliceOrdering && !slice.has(a, be, d)) { missing.addB(i, &be) } } @@ -46,7 +56,7 @@ func (d *Differ) diffSliceGeneric(path []string, a, b reflect.Value) error { return nil } - return d.diffComparative(path, missing) + return d.diffComparative(path, missing, exportInterface(a)) } func (d *Differ) diffSliceComparative(path []string, a, b reflect.Value) error { @@ -56,7 +66,7 @@ func (d *Differ) diffSliceComparative(path []string, a, b reflect.Value) error { ae := a.Index(i) ak := getFinalValue(ae) - id := identifier(ak) + id := identifier(d.TagName, ak) if id != nil { c.addA(id, &ae) } @@ -66,21 +76,21 @@ func (d *Differ) diffSliceComparative(path []string, a, b reflect.Value) error { be := b.Index(i) bk := getFinalValue(be) - id := identifier(bk) + id := identifier(d.TagName, bk) if id != nil { c.addB(id, &be) } } - return d.diffComparative(path, c) + return d.diffComparative(path, c, exportInterface(a)) } -// keeps track of elements that have already been matched, to stop duplicate matches from occuring +// keeps track of elements that have already been matched, to stop duplicate matches from occurring type sliceTracker []bool -func (st *sliceTracker) has(s, v reflect.Value) bool { +func (st *sliceTracker) has(s, v reflect.Value, d *Differ) bool { if len(*st) != s.Len() { - (*st) = make([]bool, s.Len(), s.Len()) + (*st) = make([]bool, s.Len()) } for i := 0; i < s.Len(); i++ { @@ -90,7 +100,17 @@ func (st *sliceTracker) has(s, v reflect.Value) bool { } x := s.Index(i) - if reflect.DeepEqual(x.Interface(), v.Interface()) { + + var nd Differ + nd.Filter = d.Filter + nd.customValueDiffers = d.customValueDiffers + + err := nd.diff([]string{}, x, v, nil) + if err != nil { + continue + } + + if len(nd.cl) == 0 { (*st)[i] = true return true } @@ -111,10 +131,10 @@ func getFinalValue(t reflect.Value) reflect.Value { } func hasAtSameIndex(s, v reflect.Value, atIndex int) bool { - // check the element in the slice at atIndex to see if it matches value, if it is a valid index into the slice + // check the element in the slice at atIndex to see if it matches Value, if it is a valid index into the slice if atIndex < s.Len() { x := s.Index(atIndex) - return reflect.DeepEqual(x.Interface(), v.Interface()) + return reflect.DeepEqual(exportInterface(x), exportInterface(v)) } return false diff --git a/diff_string.go b/diff_string.go index 52dd911..74182e2 100644 --- a/diff_string.go +++ b/diff_string.go @@ -6,14 +6,14 @@ package diff import "reflect" -func (d *Differ) diffString(path []string, a, b reflect.Value) error { +func (d *Differ) diffString(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { - d.cl.add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, exportInterface(b)) return nil } if b.Kind() == reflect.Invalid { - d.cl.add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, exportInterface(a), nil) return nil } @@ -22,7 +22,12 @@ func (d *Differ) diffString(path []string, a, b reflect.Value) error { } if a.String() != b.String() { - d.cl.add(UPDATE, path, a.String(), b.String()) + if a.CanInterface() { + // If a and/or b is of a type that is an alias for String, store that type in changelog + d.cl.Add(UPDATE, path, exportInterface(a), exportInterface(b), parent) + } else { + d.cl.Add(UPDATE, path, a.String(), b.String(), parent) + } } return nil diff --git a/diff_struct.go b/diff_struct.go index 16e7faa..fb14c57 100644 --- a/diff_struct.go +++ b/diff_struct.go @@ -9,14 +9,14 @@ import ( "time" ) -func (d *Differ) diffStruct(path []string, a, b reflect.Value) error { - if areType(a, b, reflect.TypeOf(time.Time{})) { +func (d *Differ) diffStruct(path []string, a, b reflect.Value, parent interface{}) error { + if AreType(a, b, reflect.TypeOf(time.Time{})) { return d.diffTime(path, a, b) } if a.Kind() == reflect.Invalid { if d.DisableStructValues { - d.cl.add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, exportInterface(b)) return nil } return d.structValues(CREATE, path, b) @@ -24,7 +24,7 @@ func (d *Differ) diffStruct(path []string, a, b reflect.Value) error { if b.Kind() == reflect.Invalid { if d.DisableStructValues { - d.cl.add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, exportInterface(a), nil) return nil } return d.structValues(DELETE, path, a) @@ -32,9 +32,9 @@ func (d *Differ) diffStruct(path []string, a, b reflect.Value) error { for i := 0; i < a.NumField(); i++ { field := a.Type().Field(i) - tname := tagName(field) + tname := tagName(d.TagName, field) - if tname == "-" || hasTagOption(field, "immutable") { + if tname == "-" || hasTagOption(d.TagName, field, "immutable") { continue } @@ -45,9 +45,21 @@ func (d *Differ) diffStruct(path []string, a, b reflect.Value) error { af := a.Field(i) bf := b.FieldByName(field.Name) - fpath := copyAppend(path, tname) + fpath := path + if !(d.FlattenEmbeddedStructs && field.Anonymous) { + fpath = copyAppend(fpath, tname) + } + + if d.Filter != nil && !d.Filter(fpath, a.Type(), field) { + continue + } - err := d.diff(fpath, af, bf) + // skip private fields + if !a.CanInterface() { + continue + } + + err := d.diff(fpath, af, bf, exportInterface(a)) if err != nil { return err } @@ -58,6 +70,8 @@ func (d *Differ) diffStruct(path []string, a, b reflect.Value) error { func (d *Differ) structValues(t string, path []string, a reflect.Value) error { var nd Differ + nd.Filter = d.Filter + nd.customValueDiffers = d.customValueDiffers if t != CREATE && t != DELETE { return ErrInvalidChangeType @@ -76,7 +90,7 @@ func (d *Differ) structValues(t string, path []string, a reflect.Value) error { for i := 0; i < a.NumField(); i++ { field := a.Type().Field(i) - tname := tagName(field) + tname := tagName(d.TagName, field) if tname == "-" { continue @@ -91,7 +105,11 @@ func (d *Differ) structValues(t string, path []string, a reflect.Value) error { fpath := copyAppend(path, tname) - err := nd.diff(fpath, xf, af) + if nd.Filter != nil && !nd.Filter(fpath, a.Type(), field) { + continue + } + + err := nd.diff(fpath, xf, af, exportInterface(a)) if err != nil { return err } diff --git a/diff_test.go b/diff_test.go index c364f0b..4008623 100644 --- a/diff_test.go +++ b/diff_test.go @@ -2,12 +2,16 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package diff +package diff_test import ( + "reflect" + "strings" + "sync" "testing" "time" + "github.com/r3labs/diff/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -35,6 +39,37 @@ type tmstruct struct { Bar int `diff:"bar"` } +type Embedded struct { + Foo string `diff:"foo"` + Bar int `diff:"bar"` +} + +type embedstruct struct { + Embedded + Baz bool `diff:"baz"` +} + +type customTagStruct struct { + Foo string `json:"foo"` + Bar int `json:"bar"` +} + +type privateValueStruct struct { + Public string + Private *sync.RWMutex +} + +type privateMapStruct struct { + set map[string]interface{} +} + +type CustomStringType string +type CustomIntType int +type customTypeStruct struct { + Foo CustomStringType `diff:"foo"` + Bar CustomIntType `diff:"bar"` +} + type tstruct struct { ID string `diff:"id,immutable"` Name string `diff:"name"` @@ -48,6 +83,7 @@ type tstruct struct { Identifiables []tistruct `diff:"identifiables"` Unidentifiables []tuistruct `diff:"unidentifiables"` Nested tnstruct `diff:"nested"` + private int `diff:"private"` } func sptr(s string) *string { @@ -58,287 +94,439 @@ func TestDiff(t *testing.T) { cases := []struct { Name string A, B interface{} - Changelog Changelog + Changelog diff.Changelog Error error }{ + { + "uint-slice-insert", []uint{1, 2, 3}, []uint{1, 2, 3, 4}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: uint(4)}, + }, + nil, + }, + { + "uint-array-insert", [3]uint{1, 2, 3}, [4]uint{1, 2, 3, 4}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: uint(4)}, + }, + nil, + }, { "int-slice-insert", []int{1, 2, 3}, []int{1, 2, 3, 4}, - Changelog{ - Change{Type: CREATE, Path: []string{"3"}, To: 4}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: 4}, + }, + nil, + }, + { + "int-array-insert", [3]int{1, 2, 3}, [4]int{1, 2, 3, 4}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: 4}, + }, + nil, + }, + { + "uint-slice-delete", []uint{1, 2, 3}, []uint{1, 3}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: uint(2)}, + }, + nil, + }, + { + "uint-array-delete", [3]uint{1, 2, 3}, [2]uint{1, 3}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: uint(2)}, }, nil, }, { "int-slice-delete", []int{1, 2, 3}, []int{1, 3}, - Changelog{ - Change{Type: DELETE, Path: []string{"1"}, From: 2}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: 2}, + }, + nil, + }, + { + "uint-slice-insert-delete", []uint{1, 2, 3}, []uint{1, 3, 4}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: uint(2)}, + diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: uint(4)}, + }, + nil, + }, + { + "uint-slice-array-delete", [3]uint{1, 2, 3}, [3]uint{1, 3, 4}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: uint(2)}, + diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: uint(4)}, }, nil, }, { "int-slice-insert-delete", []int{1, 2, 3}, []int{1, 3, 4}, - Changelog{ - Change{Type: DELETE, Path: []string{"1"}, From: 2}, - Change{Type: CREATE, Path: []string{"2"}, To: 4}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: 2}, + diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: 4}, }, nil, }, { "string-slice-insert", []string{"1", "2", "3"}, []string{"1", "2", "3", "4"}, - Changelog{ - Change{Type: CREATE, Path: []string{"3"}, To: "4"}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: "4"}, + }, + nil, + }, + { + "string-array-insert", [3]string{"1", "2", "3"}, [4]string{"1", "2", "3", "4"}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: "4"}, }, nil, }, { "string-slice-delete", []string{"1", "2", "3"}, []string{"1", "3"}, - Changelog{ - Change{Type: DELETE, Path: []string{"1"}, From: "2"}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"}, + }, + nil, + }, + { + "string-slice-delete", [3]string{"1", "2", "3"}, [2]string{"1", "3"}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"}, }, nil, }, { "string-slice-insert-delete", []string{"1", "2", "3"}, []string{"1", "3", "4"}, - Changelog{ - Change{Type: DELETE, Path: []string{"1"}, From: "2"}, - Change{Type: CREATE, Path: []string{"2"}, To: "4"}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"}, + diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: "4"}, + }, + nil, + }, + { + "string-array-insert-delete", [3]string{"1", "2", "3"}, [3]string{"1", "3", "4"}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"}, + diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: "4"}, }, nil, }, { "comparable-slice-insert", []tistruct{{"one", 1}}, []tistruct{{"one", 1}, {"two", 2}}, - Changelog{ - Change{Type: CREATE, Path: []string{"two", "name"}, To: "two"}, - Change{Type: CREATE, Path: []string{"two", "value"}, To: 2}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"two", "name"}, To: "two"}, + diff.Change{Type: diff.CREATE, Path: []string{"two", "value"}, To: 2}, + }, + nil, + }, + { + "comparable-array-insert", [1]tistruct{{"one", 1}}, [2]tistruct{{"one", 1}, {"two", 2}}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"two", "name"}, To: "two"}, + diff.Change{Type: diff.CREATE, Path: []string{"two", "value"}, To: 2}, }, nil, }, { "comparable-slice-delete", []tistruct{{"one", 1}, {"two", 2}}, []tistruct{{"one", 1}}, - Changelog{ - Change{Type: DELETE, Path: []string{"two", "name"}, From: "two"}, - Change{Type: DELETE, Path: []string{"two", "value"}, From: 2}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"two", "name"}, From: "two"}, + diff.Change{Type: diff.DELETE, Path: []string{"two", "value"}, From: 2}, + }, + nil, + }, + { + "comparable-array-delete", [2]tistruct{{"one", 1}, {"two", 2}}, [1]tistruct{{"one", 1}}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"two", "name"}, From: "two"}, + diff.Change{Type: diff.DELETE, Path: []string{"two", "value"}, From: 2}, }, nil, }, { "comparable-slice-update", []tistruct{{"one", 1}}, []tistruct{{"one", 50}}, - Changelog{ - Change{Type: UPDATE, Path: []string{"one", "value"}, From: 1, To: 50}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"one", "value"}, From: 1, To: 50}, + }, + nil, + }, + { + "comparable-array-update", [1]tistruct{{"one", 1}}, [1]tistruct{{"one", 50}}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"one", "value"}, From: 1, To: 50}, }, nil, }, { "map-slice-insert", []map[string]string{{"test": "123"}}, []map[string]string{{"test": "123", "tset": "456"}}, - Changelog{ - Change{Type: CREATE, Path: []string{"0", "tset"}, To: "456"}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"0", "tset"}, To: "456"}, + }, + nil, + }, + { + "map-array-insert", [1]map[string]string{{"test": "123"}}, [1]map[string]string{{"test": "123", "tset": "456"}}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"0", "tset"}, To: "456"}, }, nil, }, { "map-slice-update", []map[string]string{{"test": "123"}}, []map[string]string{{"test": "456"}}, - Changelog{ - Change{Type: UPDATE, Path: []string{"0", "test"}, From: "123", To: "456"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"0", "test"}, From: "123", To: "456"}, + }, + nil, + }, + { + "map-array-update", [1]map[string]string{{"test": "123"}}, [1]map[string]string{{"test": "456"}}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"0", "test"}, From: "123", To: "456"}, }, nil, }, { "map-slice-delete", []map[string]string{{"test": "123", "tset": "456"}}, []map[string]string{{"test": "123"}}, - Changelog{ - Change{Type: DELETE, Path: []string{"0", "tset"}, From: "456"}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"0", "tset"}, From: "456"}, + }, + nil, + }, + { + "map-array-delete", [1]map[string]string{{"test": "123", "tset": "456"}}, [1]map[string]string{{"test": "123"}}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"0", "tset"}, From: "456"}, }, nil, }, { "map-interface-slice-update", []map[string]interface{}{{"test": nil}}, []map[string]interface{}{{"test": "456"}}, - Changelog{ - Change{Type: UPDATE, Path: []string{"0", "test"}, From: nil, To: "456"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"0", "test"}, From: nil, To: "456"}, + }, + nil, + }, + { + "map-interface-array-update", [1]map[string]interface{}{{"test": nil}}, [1]map[string]interface{}{{"test": "456"}}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"0", "test"}, From: nil, To: "456"}, }, nil, }, { "map-nil", map[string]string{"one": "test"}, nil, - Changelog{ - Change{Type: DELETE, Path: []string{"one"}, From: "test", To: nil}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"\xa3one"}, From: "test", To: nil}, }, nil, }, { "nil-map", nil, map[string]string{"one": "test"}, - Changelog{ - Change{Type: CREATE, Path: []string{"one"}, From: nil, To: "test"}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"\xa3one"}, From: nil, To: "test"}, }, nil, }, { "nested-map-insert", map[string]map[string]string{"a": {"test": "123"}}, map[string]map[string]string{"a": {"test": "123", "tset": "456"}}, - Changelog{ - Change{Type: CREATE, Path: []string{"a", "tset"}, To: "456"}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"a", "tset"}, To: "456"}, }, nil, }, { "nested-map-interface-insert", map[string]map[string]interface{}{"a": {"test": "123"}}, map[string]map[string]interface{}{"a": {"test": "123", "tset": "456"}}, - Changelog{ - Change{Type: CREATE, Path: []string{"a", "tset"}, To: "456"}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"a", "tset"}, To: "456"}, }, nil, }, { "nested-map-update", map[string]map[string]string{"a": {"test": "123"}}, map[string]map[string]string{"a": {"test": "456"}}, - Changelog{ - Change{Type: UPDATE, Path: []string{"a", "test"}, From: "123", To: "456"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"a", "test"}, From: "123", To: "456"}, }, nil, }, { "nested-map-delete", map[string]map[string]string{"a": {"test": "123"}}, map[string]map[string]string{"a": {}}, - Changelog{ - Change{Type: DELETE, Path: []string{"a", "test"}, From: "123", To: nil}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"a", "test"}, From: "123", To: nil}, + }, + nil, + }, + { + "nested-slice-insert", map[string][]int{"a": {1, 2, 3}}, map[string][]int{"a": {1, 2, 3, 4}}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"a", "3"}, To: 4}, + }, + nil, + }, + { + "nested-array-insert", map[string][3]int{"a": {1, 2, 3}}, map[string][4]int{"a": {1, 2, 3, 4}}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"a", "3"}, To: 4}, + }, + nil, + }, + { + "nested-slice-update", map[string][]int{"a": {1, 2, 3}}, map[string][]int{"a": {1, 4, 3}}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"a", "1"}, From: 2, To: 4}, }, nil, }, { - "nested-slice-insert", map[string][]int{"a": []int{1, 2, 3}}, map[string][]int{"a": []int{1, 2, 3, 4}}, - Changelog{ - Change{Type: CREATE, Path: []string{"a", "3"}, To: 4}, + "nested-array-update", map[string][3]int{"a": {1, 2, 3}}, map[string][3]int{"a": {1, 4, 3}}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"a", "1"}, From: 2, To: 4}, }, nil, }, { - "nested-slice-update", map[string][]int{"a": []int{1, 2, 3}}, map[string][]int{"a": []int{1, 4, 3}}, - Changelog{ - Change{Type: UPDATE, Path: []string{"a", "1"}, From: 2, To: 4}, + "nested-slice-delete", map[string][]int{"a": {1, 2, 3}}, map[string][]int{"a": {1, 3}}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"a", "1"}, From: 2, To: nil}, }, nil, }, { - "nested-slice-delete", map[string][]int{"a": []int{1, 2, 3}}, map[string][]int{"a": []int{1, 3}}, - Changelog{ - Change{Type: DELETE, Path: []string{"a", "1"}, From: 2, To: nil}, + "nested-array-delete", map[string][3]int{"a": {1, 2, 3}}, map[string][2]int{"a": {1, 3}}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"a", "1"}, From: 2, To: nil}, }, nil, }, { "struct-string-update", tstruct{Name: "one"}, tstruct{Name: "two"}, - Changelog{ - Change{Type: UPDATE, Path: []string{"name"}, From: "one", To: "two"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"name"}, From: "one", To: "two"}, }, nil, }, { "struct-int-update", tstruct{Value: 1}, tstruct{Value: 50}, - Changelog{ - Change{Type: UPDATE, Path: []string{"value"}, From: 1, To: 50}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"value"}, From: 1, To: 50}, }, nil, }, { "struct-bool-update", tstruct{Bool: true}, tstruct{Bool: false}, - Changelog{ - Change{Type: UPDATE, Path: []string{"bool"}, From: true, To: false}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"bool"}, From: true, To: false}, }, nil, }, { "struct-time-update", tstruct{}, tstruct{Time: currentTime}, - Changelog{ - Change{Type: UPDATE, Path: []string{"time"}, From: time.Time{}, To: currentTime}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"time"}, From: time.Time{}, To: currentTime}, }, nil, }, { "struct-map-update", tstruct{Map: map[string]string{"test": "123"}}, tstruct{Map: map[string]string{"test": "456"}}, - Changelog{ - Change{Type: UPDATE, Path: []string{"map", "test"}, From: "123", To: "456"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"map", "test"}, From: "123", To: "456"}, }, nil, }, { "struct-string-pointer-update", tstruct{Pointer: sptr("test")}, tstruct{Pointer: sptr("test2")}, - Changelog{ - Change{Type: UPDATE, Path: []string{"pointer"}, From: "test", To: "test2"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"pointer"}, From: "test", To: "test2"}, }, nil, }, { "struct-nil-string-pointer-update", tstruct{Pointer: nil}, tstruct{Pointer: sptr("test")}, - Changelog{ - Change{Type: UPDATE, Path: []string{"pointer"}, From: nil, To: sptr("test")}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"pointer"}, From: nil, To: sptr("test")}, }, nil, }, { "struct-generic-slice-insert", tstruct{Values: []string{"one"}}, tstruct{Values: []string{"one", "two"}}, - Changelog{ - Change{Type: CREATE, Path: []string{"values", "1"}, From: nil, To: "two"}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"values", "1"}, From: nil, To: "two"}, }, nil, }, { "struct-identifiable-slice-insert", tstruct{Identifiables: []tistruct{{"one", 1}}}, tstruct{Identifiables: []tistruct{{"one", 1}, {"two", 2}}}, - Changelog{ - Change{Type: CREATE, Path: []string{"identifiables", "two", "name"}, From: nil, To: "two"}, - Change{Type: CREATE, Path: []string{"identifiables", "two", "value"}, From: nil, To: 2}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"identifiables", "two", "name"}, From: nil, To: "two"}, + diff.Change{Type: diff.CREATE, Path: []string{"identifiables", "two", "value"}, From: nil, To: 2}, }, nil, }, { "struct-generic-slice-delete", tstruct{Values: []string{"one", "two"}}, tstruct{Values: []string{"one"}}, - Changelog{ - Change{Type: DELETE, Path: []string{"values", "1"}, From: "two", To: nil}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"values", "1"}, From: "two", To: nil}, }, nil, }, { "struct-identifiable-slice-delete", tstruct{Identifiables: []tistruct{{"one", 1}, {"two", 2}}}, tstruct{Identifiables: []tistruct{{"one", 1}}}, - Changelog{ - Change{Type: DELETE, Path: []string{"identifiables", "two", "name"}, From: "two", To: nil}, - Change{Type: DELETE, Path: []string{"identifiables", "two", "value"}, From: 2, To: nil}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"identifiables", "two", "name"}, From: "two", To: nil}, + diff.Change{Type: diff.DELETE, Path: []string{"identifiables", "two", "value"}, From: 2, To: nil}, }, nil, }, { "struct-unidentifiable-slice-insert-delete", tstruct{Unidentifiables: []tuistruct{{1}, {2}, {3}}}, tstruct{Unidentifiables: []tuistruct{{5}, {2}, {3}, {4}}}, - Changelog{ - Change{Type: UPDATE, Path: []string{"unidentifiables", "0", "value"}, From: 1, To: 5}, - Change{Type: CREATE, Path: []string{"unidentifiables", "3", "value"}, From: nil, To: 4}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"unidentifiables", "0", "value"}, From: 1, To: 5}, + diff.Change{Type: diff.CREATE, Path: []string{"unidentifiables", "3", "value"}, From: nil, To: 4}, + }, + nil, + }, + { + "struct-with-private-value", privateValueStruct{Public: "one", Private: new(sync.RWMutex)}, privateValueStruct{Public: "two", Private: new(sync.RWMutex)}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"Public"}, From: "one", To: "two"}, }, nil, }, { "mismatched-values-struct-map", map[string]string{"test": "one"}, &tstruct{Identifiables: []tistruct{{"one", 1}}}, - Changelog{}, - ErrTypeMismatch, + diff.Changelog{}, + diff.ErrTypeMismatch, }, { "omittable", tstruct{Ignored: false}, tstruct{Ignored: true}, - Changelog{}, + diff.Changelog{}, nil, }, { "slice", &tstruct{}, &tstruct{Nested: tnstruct{Slice: []tmstruct{{"one", 1}, {"two", 2}}}}, - Changelog{ - Change{Type: CREATE, Path: []string{"nested", "slice", "0", "foo"}, From: nil, To: "one"}, - Change{Type: CREATE, Path: []string{"nested", "slice", "0", "bar"}, From: nil, To: 1}, - Change{Type: CREATE, Path: []string{"nested", "slice", "1", "foo"}, From: nil, To: "two"}, - Change{Type: CREATE, Path: []string{"nested", "slice", "1", "bar"}, From: nil, To: 2}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "0", "foo"}, From: nil, To: "one"}, + diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "0", "bar"}, From: nil, To: 1}, + diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "1", "foo"}, From: nil, To: "two"}, + diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "1", "bar"}, From: nil, To: 2}, }, nil, }, { "slice-duplicate-items", []int{1}, []int{1, 1}, - Changelog{ - Change{Type: CREATE, Path: []string{"1"}, From: nil, To: 1}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"1"}, From: nil, To: 1}, }, nil, }, { "mixed-slice-map", []map[string]interface{}{{"name": "name1", "type": []string{"null", "string"}}}, []map[string]interface{}{{"name": "name1", "type": []string{"null", "int"}}, {"name": "name2", "type": []string{"null", "string"}}}, - Changelog{ - Change{Type: UPDATE, Path: []string{"0", "type", "1"}, From: "string", To: "int"}, - Change{Type: CREATE, Path: []string{"1", "name"}, From: nil, To: "name2"}, - Change{Type: CREATE, Path: []string{"1", "type"}, From: nil, To: []string{"null", "string"}}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"0", "type", "1"}, From: "string", To: "int"}, + diff.Change{Type: diff.CREATE, Path: []string{"1", "\xa4name"}, From: nil, To: "name2"}, + diff.Change{Type: diff.CREATE, Path: []string{"1", "\xa4type"}, From: nil, To: []string{"null", "string"}}, }, nil, }, @@ -346,9 +534,9 @@ func TestDiff(t *testing.T) { "map-string-pointer-create", map[string]*tmstruct{"one": &struct1}, map[string]*tmstruct{"one": &struct1, "two": &struct2}, - Changelog{ - Change{Type: CREATE, Path: []string{"two", "foo"}, From: nil, To: "two"}, - Change{Type: CREATE, Path: []string{"two", "bar"}, From: nil, To: 2}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"two", "foo"}, From: nil, To: "two"}, + diff.Change{Type: diff.CREATE, Path: []string{"two", "bar"}, From: nil, To: 2}, }, nil, }, @@ -356,17 +544,95 @@ func TestDiff(t *testing.T) { "map-string-pointer-delete", map[string]*tmstruct{"one": &struct1, "two": &struct2}, map[string]*tmstruct{"one": &struct1}, - Changelog{ - Change{Type: DELETE, Path: []string{"two", "foo"}, From: "two", To: nil}, - Change{Type: DELETE, Path: []string{"two", "bar"}, From: 2, To: nil}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"two", "foo"}, From: "two", To: nil}, + diff.Change{Type: diff.DELETE, Path: []string{"two", "bar"}, From: 2, To: nil}, + }, + nil, + }, + { + "private-struct-field", + tstruct{private: 1}, + tstruct{private: 4}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"private"}, From: int64(1), To: int64(4)}, }, nil, }, + { + "embedded-struct-field", + embedstruct{Embedded{Foo: "a", Bar: 2}, true}, + embedstruct{Embedded{Foo: "b", Bar: 3}, false}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: "a", To: "b"}, + diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: 2, To: 3}, + diff.Change{Type: diff.UPDATE, Path: []string{"baz"}, From: true, To: false}, + }, + nil, + }, + { + "custom-tags", + customTagStruct{Foo: "abc", Bar: 3}, + customTagStruct{Foo: "def", Bar: 4}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: "abc", To: "def"}, + diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: 3, To: 4}, + }, + nil, + }, + { + "custom-types", + customTypeStruct{Foo: "a", Bar: 1}, + customTypeStruct{Foo: "b", Bar: 2}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: CustomStringType("a"), To: CustomStringType("b")}, + diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: CustomIntType(1), To: CustomIntType(2)}, + }, + nil, + }, + { + "struct-private-map-create", privateMapStruct{set: map[string]interface{}{"1": struct{}{}, "2": struct{}{}}}, privateMapStruct{set: map[string]interface{}{"1": struct{}{}, "2": struct{}{}, "3": struct{}{}}}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"set", "3"}, From: nil, To: struct{}{}}, + }, + nil, + }, + { + "struct-private-map-delete", privateMapStruct{set: map[string]interface{}{"1": struct{}{}, "2": struct{}{}, "3": struct{}{}}}, privateMapStruct{set: map[string]interface{}{"1": struct{}{}, "2": struct{}{}}}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"set", "3"}, From: struct{}{}, To: nil}, + }, + nil, + }, + { + "struct-private-map-nil-values", privateMapStruct{set: map[string]interface{}{"1": nil, "2": nil}}, privateMapStruct{set: map[string]interface{}{"1": nil, "2": nil, "3": nil}}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"set", "3"}, From: nil, To: nil}, + }, + nil, + }, + { + "slice-of-struct-with-slice", + []tnstruct{{[]tmstruct{struct1, struct2}}, {[]tmstruct{struct2, struct2}}}, + []tnstruct{{[]tmstruct{struct2, struct2}}, {[]tmstruct{struct2, struct1}}}, + diff.Changelog{}, + nil, + }, } for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { - cl, err := Diff(tc.A, tc.B) + + var options []func(d *diff.Differ) error + switch tc.Name { + case "mixed-slice-map", "nil-map", "map-nil": + options = append(options, diff.StructMapKeySupport()) + case "embedded-struct-field": + options = append(options, diff.FlattenEmbeddedStructs()) + case "custom-tags": + options = append(options, diff.TagName("json")) + } + cl, err := diff.Diff(tc.A, tc.B, options...) assert.Equal(t, tc.Error, err) require.Equal(t, len(tc.Changelog), len(cl)) @@ -385,70 +651,70 @@ func TestDiffSliceOrdering(t *testing.T) { cases := []struct { Name string A, B interface{} - Changelog Changelog + Changelog diff.Changelog Error error }{ { "int-slice-insert-in-middle", []int{1, 2, 4}, []int{1, 2, 3, 4}, - Changelog{ - Change{Type: UPDATE, Path: []string{"2"}, From: 4, To: 3}, - Change{Type: CREATE, Path: []string{"3"}, To: 4}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"2"}, From: 4, To: 3}, + diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: 4}, }, nil, }, { "int-slice-delete", []int{1, 2, 3}, []int{1, 3}, - Changelog{ - Change{Type: UPDATE, Path: []string{"1"}, From: 2, To: 3}, - Change{Type: DELETE, Path: []string{"2"}, From: 3}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"1"}, From: 2, To: 3}, + diff.Change{Type: diff.DELETE, Path: []string{"2"}, From: 3}, }, nil, }, { "int-slice-insert-delete", []int{1, 2, 3}, []int{1, 3, 4}, - Changelog{ - Change{Type: UPDATE, Path: []string{"1"}, From: 2, To: 3}, - Change{Type: UPDATE, Path: []string{"2"}, From: 3, To: 4}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"1"}, From: 2, To: 3}, + diff.Change{Type: diff.UPDATE, Path: []string{"2"}, From: 3, To: 4}, }, nil, }, { "int-slice-reorder", []int{1, 2, 3}, []int{1, 3, 2}, - Changelog{ - Change{Type: UPDATE, Path: []string{"1"}, From: 2, To: 3}, - Change{Type: UPDATE, Path: []string{"2"}, From: 3, To: 2}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"1"}, From: 2, To: 3}, + diff.Change{Type: diff.UPDATE, Path: []string{"2"}, From: 3, To: 2}, }, nil, }, { "string-slice-delete", []string{"1", "2", "3"}, []string{"1", "3"}, - Changelog{ - Change{Type: UPDATE, Path: []string{"1"}, From: "2", To: "3"}, - Change{Type: DELETE, Path: []string{"2"}, From: "3"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"1"}, From: "2", To: "3"}, + diff.Change{Type: diff.DELETE, Path: []string{"2"}, From: "3"}, }, nil, }, { "string-slice-insert-delete", []string{"1", "2", "3"}, []string{"1", "3", "4"}, - Changelog{ - Change{Type: UPDATE, Path: []string{"1"}, From: "2", To: "3"}, - Change{Type: UPDATE, Path: []string{"2"}, From: "3", To: "4"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"1"}, From: "2", To: "3"}, + diff.Change{Type: diff.UPDATE, Path: []string{"2"}, From: "3", To: "4"}, }, nil, }, { "string-slice-reorder", []string{"1", "2", "3"}, []string{"1", "3", "2"}, - Changelog{ - Change{Type: UPDATE, Path: []string{"1"}, From: "2", To: "3"}, - Change{Type: UPDATE, Path: []string{"2"}, From: "3", To: "2"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"1"}, From: "2", To: "3"}, + diff.Change{Type: diff.UPDATE, Path: []string{"2"}, From: "3", To: "2"}, }, nil, }, { - "nested-slice-delete", map[string][]int{"a": []int{1, 2, 3}}, map[string][]int{"a": []int{1, 3}}, - Changelog{ - Change{Type: UPDATE, Path: []string{"a", "1"}, From: 2, To: 3}, - Change{Type: DELETE, Path: []string{"a", "2"}, From: 3}, + "nested-slice-delete", map[string][]int{"a": {1, 2, 3}}, map[string][]int{"a": {1, 3}}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"a", "1"}, From: 2, To: 3}, + diff.Change{Type: diff.DELETE, Path: []string{"a", "2"}, From: 3}, }, nil, }, @@ -456,7 +722,7 @@ func TestDiffSliceOrdering(t *testing.T) { for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { - d, err := NewDiffer(SliceOrdering(true)) + d, err := diff.NewDiffer(diff.SliceOrdering(true)) require.Nil(t, err) cl, err := d.Diff(tc.A, tc.B) @@ -484,7 +750,7 @@ func TestFilter(t *testing.T) { {"regex", []string{"item-*"}, [][]string{{"item-1", "subitem"}, {"item-2", "subitem"}}}, } - cl := Changelog{ + cl := diff.Changelog{ {Path: []string{"item-1", "subitem"}}, {Path: []string{"item-2", "subitem"}}, } @@ -500,35 +766,61 @@ func TestFilter(t *testing.T) { } } +func TestFilterOut(t *testing.T) { + cases := []struct { + Name string + Filter []string + Expected [][]string + }{ + {"simple", []string{"item-1", "subitem"}, [][]string{{"item-2", "subitem"}}}, + {"regex", []string{"item-*"}, [][]string{}}, + } + + cl := diff.Changelog{ + {Path: []string{"item-1", "subitem"}}, + {Path: []string{"item-2", "subitem"}}, + } + + for _, tc := range cases { + t.Run(tc.Name, func(t *testing.T) { + ncl := cl.FilterOut(tc.Filter) + assert.Len(t, ncl, len(tc.Expected)) + for i, e := range tc.Expected { + assert.Equal(t, e, ncl[i].Path) + } + }) + } +} + func TestStructValues(t *testing.T) { cases := []struct { Name string ChangeType string X interface{} - Changelog Changelog + Changelog diff.Changelog Error error }{ { - "struct-create", CREATE, tstruct{ID: "xxxxx", Name: "something", Value: 1, Values: []string{"one", "two", "three"}}, - Changelog{ - Change{Type: CREATE, Path: []string{"id"}, From: nil, To: "xxxxx"}, - Change{Type: CREATE, Path: []string{"name"}, From: nil, To: "something"}, - Change{Type: CREATE, Path: []string{"value"}, From: nil, To: 1}, - Change{Type: CREATE, Path: []string{"values", "0"}, From: nil, To: "one"}, - Change{Type: CREATE, Path: []string{"values", "1"}, From: nil, To: "two"}, - Change{Type: CREATE, Path: []string{"values", "2"}, From: nil, To: "three"}, + "struct-create", diff.CREATE, tstruct{ID: "xxxxx", Name: "something", Value: 1, Values: []string{"one", "two", "three"}}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"id"}, From: nil, To: "xxxxx"}, + diff.Change{Type: diff.CREATE, Path: []string{"name"}, From: nil, To: "something"}, + diff.Change{Type: diff.CREATE, Path: []string{"value"}, From: nil, To: 1}, + diff.Change{Type: diff.CREATE, Path: []string{"values", "0"}, From: nil, To: "one"}, + diff.Change{Type: diff.CREATE, Path: []string{"values", "1"}, From: nil, To: "two"}, + diff.Change{Type: diff.CREATE, Path: []string{"values", "2"}, From: nil, To: "three"}, }, nil, }, { - "struct-delete", DELETE, tstruct{ID: "xxxxx", Name: "something", Value: 1, Values: []string{"one", "two", "three"}}, - Changelog{ - Change{Type: DELETE, Path: []string{"id"}, From: "xxxxx", To: nil}, - Change{Type: DELETE, Path: []string{"name"}, From: "something", To: nil}, - Change{Type: DELETE, Path: []string{"value"}, From: 1, To: nil}, - Change{Type: DELETE, Path: []string{"values", "0"}, From: "one", To: nil}, - Change{Type: DELETE, Path: []string{"values", "1"}, From: "two", To: nil}, - Change{Type: DELETE, Path: []string{"values", "2"}, From: "three", To: nil}, + "struct-delete", diff.DELETE, tstruct{ID: "xxxxx", Name: "something", Value: 1, Values: []string{"one", "two", "three"}}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"id"}, From: "xxxxx", To: nil}, + diff.Change{Type: diff.DELETE, Path: []string{"name"}, From: "something", To: nil}, + diff.Change{Type: diff.DELETE, Path: []string{"value"}, From: 1, To: nil}, + diff.Change{Type: diff.DELETE, Path: []string{"values", "0"}, From: "one", To: nil}, + diff.Change{Type: diff.DELETE, Path: []string{"values", "1"}, From: "two", To: nil}, + diff.Change{Type: diff.DELETE, Path: []string{"values", "2"}, From: "three", To: nil}, }, nil, }, @@ -536,7 +828,7 @@ func TestStructValues(t *testing.T) { for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { - cl, err := StructValues(tc.ChangeType, []string{}, tc.X) + cl, err := diff.StructValues(tc.ChangeType, []string{}, tc.X) assert.Equal(t, tc.Error, err) assert.Equal(t, len(tc.Changelog), len(cl)) @@ -551,8 +843,31 @@ func TestStructValues(t *testing.T) { } } +func TestDifferReuse(t *testing.T) { + d, err := diff.NewDiffer() + require.Nil(t, err) + + cl, err := d.Diff([]string{"1", "2", "3"}, []string{"1"}) + require.Nil(t, err) + + require.Len(t, cl, 2) + + assert.Equal(t, "2", cl[0].From) + assert.Equal(t, nil, cl[0].To) + assert.Equal(t, "3", cl[1].From) + assert.Equal(t, nil, cl[1].To) + + cl, err = d.Diff([]string{"a", "b"}, []string{"a", "c"}) + require.Nil(t, err) + + require.Len(t, cl, 1) + + assert.Equal(t, "b", cl[0].From) + assert.Equal(t, "c", cl[0].To) +} + func TestDiffingOptions(t *testing.T) { - d, err := NewDiffer(SliceOrdering(false)) + d, err := diff.NewDiffer(diff.SliceOrdering(false)) require.Nil(t, err) assert.False(t, d.SliceOrdering) @@ -562,7 +877,7 @@ func TestDiffingOptions(t *testing.T) { assert.Len(t, cl, 0) - d, err = NewDiffer(SliceOrdering(true)) + d, err = diff.NewDiffer(diff.SliceOrdering(true)) require.Nil(t, err) assert.True(t, d.SliceOrdering) @@ -574,3 +889,268 @@ func TestDiffingOptions(t *testing.T) { // some other options.. } + +func TestDiffPrivateField(t *testing.T) { + cl, err := diff.Diff(tstruct{private: 1}, tstruct{private: 3}) + require.Nil(t, err) + assert.Len(t, cl, 1) +} + +type testType string +type testTypeDiffer struct { + DiffFunc (func(path []string, a, b reflect.Value, p interface{}) error) +} + +func (o *testTypeDiffer) InsertParentDiffer(dfunc func(path []string, a, b reflect.Value, p interface{}) error) { + o.DiffFunc = dfunc +} + +func (o *testTypeDiffer) Match(a, b reflect.Value) bool { + return diff.AreType(a, b, reflect.TypeOf(testType(""))) +} +func (o *testTypeDiffer) Diff(dt diff.DiffType, df diff.DiffFunc, cl *diff.Changelog, path []string, a, b reflect.Value, parent interface{}) error { + if a.String() != "custom" && b.String() != "match" { + cl.Add(diff.UPDATE, path, a.Interface(), b.Interface()) + } + return nil +} + +func TestCustomDiffer(t *testing.T) { + type custom struct { + T testType + } + + d, err := diff.NewDiffer( + diff.CustomValueDiffers( + &testTypeDiffer{}, + ), + ) + require.Nil(t, err) + + cl, err := d.Diff(custom{"custom"}, custom{"match"}) + require.Nil(t, err) + + assert.Len(t, cl, 0) + + d, err = diff.NewDiffer( + diff.CustomValueDiffers( + &testTypeDiffer{}, + ), + ) + require.Nil(t, err) + + cl, err = d.Diff(custom{"same"}, custom{"same"}) + require.Nil(t, err) + + assert.Len(t, cl, 1) +} + +type testStringInterceptorDiffer struct { + DiffFunc (func(path []string, a, b reflect.Value, p interface{}) error) +} + +func (o *testStringInterceptorDiffer) InsertParentDiffer(dfunc func(path []string, a, b reflect.Value, p interface{}) error) { + o.DiffFunc = dfunc +} + +func (o *testStringInterceptorDiffer) Match(a, b reflect.Value) bool { + return diff.AreType(a, b, reflect.TypeOf(testType(""))) +} +func (o *testStringInterceptorDiffer) Diff(dt diff.DiffType, df diff.DiffFunc, cl *diff.Changelog, path []string, a, b reflect.Value, parent interface{}) error { + if dt.String() == "STRING" { + // intercept the data + aValue, aOk := a.Interface().(testType) + bValue, bOk := b.Interface().(testType) + + if aOk && bOk { + if aValue == "avalue" { + aValue = testType(strings.ToUpper(string(aValue))) + a = reflect.ValueOf(aValue) + } + + if bValue == "bvalue" { + bValue = testType(strings.ToUpper(string(aValue))) + b = reflect.ValueOf(bValue) + } + } + } + + // continue the diff logic passing the updated a/b values + return df(path, a, b, parent) +} + +func TestStringInterceptorDiffer(t *testing.T) { + d, err := diff.NewDiffer( + diff.CustomValueDiffers( + &testStringInterceptorDiffer{}, + ), + ) + require.Nil(t, err) + + cl, err := d.Diff(testType("avalue"), testType("bvalue")) + require.Nil(t, err) + + assert.Len(t, cl, 0) +} + +type RecursiveTestStruct struct { + Id int + Children []RecursiveTestStruct +} + +type recursiveTestStructDiffer struct { + DiffFunc (func(path []string, a, b reflect.Value, p interface{}) error) +} + +func (o *recursiveTestStructDiffer) InsertParentDiffer(dfunc func(path []string, a, b reflect.Value, p interface{}) error) { + o.DiffFunc = dfunc +} + +func (o *recursiveTestStructDiffer) Match(a, b reflect.Value) bool { + return diff.AreType(a, b, reflect.TypeOf(RecursiveTestStruct{})) +} + +func (o *recursiveTestStructDiffer) Diff(dt diff.DiffType, df diff.DiffFunc, cl *diff.Changelog, path []string, a, b reflect.Value, parent interface{}) error { + if a.Kind() == reflect.Invalid { + cl.Add(diff.CREATE, path, nil, b.Interface()) + return nil + } + if b.Kind() == reflect.Invalid { + cl.Add(diff.DELETE, path, a.Interface(), nil) + return nil + } + var awt, bwt RecursiveTestStruct + awt, _ = a.Interface().(RecursiveTestStruct) + bwt, _ = b.Interface().(RecursiveTestStruct) + if awt.Id != bwt.Id { + cl.Add(diff.UPDATE, path, a.Interface(), b.Interface()) + } + for i := 0; i < a.NumField(); i++ { + field := a.Type().Field(i) + tname := field.Name + if tname != "Children" { + continue + } + af := a.Field(i) + bf := b.FieldByName(field.Name) + fpath := copyAppend(path, tname) + err := o.DiffFunc(fpath, af, bf, nil) + if err != nil { + return err + } + } + return nil +} + +func TestRecursiveCustomDiffer(t *testing.T) { + treeA := RecursiveTestStruct{ + Id: 1, + Children: []RecursiveTestStruct{}, + } + + treeB := RecursiveTestStruct{ + Id: 1, + Children: []RecursiveTestStruct{ + { + Id: 4, + Children: []RecursiveTestStruct{}, + }, + }, + } + d, err := diff.NewDiffer( + diff.CustomValueDiffers( + &recursiveTestStructDiffer{}, + ), + ) + require.Nil(t, err) + cl, err := d.Diff(treeA, treeB) + require.Nil(t, err) + assert.Len(t, cl, 1) +} + +func TestHandleDifferentTypes(t *testing.T) { + cases := []struct { + Name string + A, B interface{} + Changelog diff.Changelog + Error error + HandleTypeMismatch bool + }{ + { + "type-change-not-allowed-error", + 1, "1", + nil, + diff.ErrTypeMismatch, + false, + }, + { + "type-change-not-allowed-error-struct", + struct { + p1 string + p2 int + }{"1", 1}, + struct { + p1 string + p2 string + }{"1", "1"}, + nil, + diff.ErrTypeMismatch, + false, + }, + { + "type-change-allowed", + 1, "1", + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{}, From: 1, To: "1"}, + }, + nil, + true, + }, + { + "type-change-allowed-struct", + struct { + P1 string + P2 int + P3 map[string]string + }{"1", 1, map[string]string{"1": "1"}}, + struct { + P1 string + P2 string + P3 string + }{"1", "1", "1"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"P2"}, From: 1, To: "1"}, + diff.Change{Type: diff.UPDATE, Path: []string{"P3"}, From: map[string]string{"1": "1"}, To: "1"}, + }, + nil, + true, + }, + } + + for _, tc := range cases { + t.Run(tc.Name, func(t *testing.T) { + d, err := diff.NewDiffer(diff.AllowTypeMismatch(tc.HandleTypeMismatch)) + require.Nil(t, err) + cl, err := d.Diff(tc.A, tc.B) + + assert.Equal(t, tc.Error, err) + require.Equal(t, len(tc.Changelog), len(cl)) + + for i, c := range cl { + assert.Equal(t, tc.Changelog[i].Type, c.Type) + assert.Equal(t, tc.Changelog[i].Path, c.Path) + assert.Equal(t, tc.Changelog[i].From, c.From) + assert.Equal(t, tc.Changelog[i].To, c.To) + } + }) + } +} + +func copyAppend(src []string, elems ...string) []string { + dst := make([]string, len(src)+len(elems)) + copy(dst, src) + for i := len(src); i < len(src)+len(elems); i++ { + dst[i] = elems[i-len(src)] + } + return dst +} diff --git a/diff_time.go b/diff_time.go index 49f82c0..4275e4a 100644 --- a/diff_time.go +++ b/diff_time.go @@ -6,16 +6,17 @@ package diff import ( "reflect" + "time" ) func (d *Differ) diffTime(path []string, a, b reflect.Value) error { if a.Kind() == reflect.Invalid { - d.cl.add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, exportInterface(b)) return nil } if b.Kind() == reflect.Invalid { - d.cl.add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, exportInterface(a), nil) return nil } @@ -23,8 +24,12 @@ func (d *Differ) diffTime(path []string, a, b reflect.Value) error { return ErrTypeMismatch } - if a.Interface() != b.Interface() { - d.cl.add(UPDATE, path, a.Interface(), b.Interface()) + // Marshal and unmarshal time type will lose accuracy. Using unix nano to compare time type. + au := exportInterface(a).(time.Time).UnixNano() + bu := exportInterface(b).(time.Time).UnixNano() + + if au != bu { + d.cl.Add(UPDATE, path, exportInterface(a), exportInterface(b)) } return nil diff --git a/diff_uint.go b/diff_uint.go index 45d0319..fbe133f 100644 --- a/diff_uint.go +++ b/diff_uint.go @@ -8,14 +8,14 @@ import ( "reflect" ) -func (d *Differ) diffUint(path []string, a, b reflect.Value) error { +func (d *Differ) diffUint(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { - d.cl.add(CREATE, path, nil, b.Interface()) + d.cl.Add(CREATE, path, nil, exportInterface(b)) return nil } if b.Kind() == reflect.Invalid { - d.cl.add(DELETE, path, a.Interface(), nil) + d.cl.Add(DELETE, path, exportInterface(a), nil) return nil } @@ -24,7 +24,11 @@ func (d *Differ) diffUint(path []string, a, b reflect.Value) error { } if a.Uint() != b.Uint() { - d.cl.add(UPDATE, path, a.Interface(), b.Interface()) + if a.CanInterface() { + d.cl.Add(UPDATE, path, exportInterface(a), exportInterface(b), parent) + } else { + d.cl.Add(UPDATE, path, a.Uint(), b.Uint(), parent) + } } return nil diff --git a/error.go b/error.go new file mode 100644 index 0000000..0acc13f --- /dev/null +++ b/error.go @@ -0,0 +1,74 @@ +package diff + +import ( + "fmt" +) + +var ( + // ErrTypeMismatch Compared types do not match + ErrTypeMismatch = NewError("types do not match") + // ErrInvalidChangeType The specified change values are not unsupported + ErrInvalidChangeType = NewError("change type must be one of 'create' or 'delete'") +) + +//our own version of an error, which can wrap others +type DiffError struct { + count int + message string + next error +} + +//Unwrap implement 1.13 unwrap feature for compatibility +func (s *DiffError) Unwrap() error { + return s.next +} + +//Error implements the error interface +func (s DiffError) Error() string { + cause := "" + if s.next != nil { + cause = s.next.Error() + } + return fmt.Sprintf(" %s (cause count %d)\n%s", s.message, s.count, cause) +} + +//AppendCause appends a new cause error to the chain +func (s *DiffError) WithCause(err error) *DiffError { + if s != nil && err != nil { + s.count++ + if s.next != nil { + if v, ok := err.(DiffError); ok { + s.next = v.WithCause(s.next) + } else if v, ok := err.(*DiffError); ok { + s.next = v.WithCause(s.next) + } else { + v = &DiffError{ + message: "auto wrapped error", + next: err, + } + s.next = v.WithCause(s.next) + } + } else { + s.next = err + } + } + return s +} + +//NewErrorf just give me a plain error with formatting +func NewErrorf(format string, messages ...interface{}) *DiffError { + return &DiffError{ + message: fmt.Sprintf(format, messages...), + } +} + +//NewError just give me a plain error +func NewError(message string, causes ...error) *DiffError { + s := &DiffError{ + message: message, + } + for _, cause := range causes { + s.WithCause(cause) // nolint: errcheck + } + return s +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ccf2fc9 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/r3labs/diff/v3 + +go 1.13 + +require ( + github.com/stretchr/testify v1.6.1 + github.com/vmihailenco/msgpack/v5 v5.3.5 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d48fa03 --- /dev/null +++ b/go.sum @@ -0,0 +1,15 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/options.go b/options.go index 627d2b8..fadbe86 100644 --- a/options.go +++ b/options.go @@ -1,5 +1,21 @@ package diff +// ConvertTypes enables values that are convertible to the target type to be converted when patching +func ConvertCompatibleTypes() func(d *Differ) error { + return func(d *Differ) error { + d.ConvertCompatibleTypes = true + return nil + } +} + +// FlattenEmbeddedStructs determines whether fields of embedded structs should behave as if they are directly under the parent +func FlattenEmbeddedStructs() func(d *Differ) error { + return func(d *Differ) error { + d.FlattenEmbeddedStructs = true + return nil + } +} + // SliceOrdering determines whether the ordering of items in a slice results in a change func SliceOrdering(enabled bool) func(d *Differ) error { return func(d *Differ) error { @@ -8,7 +24,15 @@ func SliceOrdering(enabled bool) func(d *Differ) error { } } -// DisableStructValues disables populating a seperate change for each item in a struct, +// TagName sets the tag name to use when getting field names and options +func TagName(tag string) func(d *Differ) error { + return func(d *Differ) error { + d.TagName = tag + return nil + } +} + +// DisableStructValues disables populating a separate change for each item in a struct, // where the struct is being compared to a nil value func DisableStructValues() func(d *Differ) error { return func(d *Differ) error { @@ -16,3 +40,54 @@ func DisableStructValues() func(d *Differ) error { return nil } } + +// CustomValueDiffers allows you to register custom differs for specific types +func CustomValueDiffers(vd ...ValueDiffer) func(d *Differ) error { + return func(d *Differ) error { + d.customValueDiffers = append(d.customValueDiffers, vd...) + for k := range d.customValueDiffers { + d.customValueDiffers[k].InsertParentDiffer(d.diff) + } + return nil + } +} + +// AllowTypeMismatch changed behaviour to report value as "updated" when its type has changed instead of error +func AllowTypeMismatch(enabled bool) func(d *Differ) error { + return func(d *Differ) error { + d.AllowTypeMismatch = enabled + return nil + } +} + +//StructMapKeySupport - Changelog paths do not provided structured object values for maps that contain complex +//keys (such as other structs). You must enable this support via an option and it then uses msgpack to encode +//path elements that are structs. If you don't have this on, and try to patch, your apply will fail for that +//element. +func StructMapKeySupport() func(d *Differ) error { + return func(d *Differ) error { + d.StructMapKeys = true + return nil + } +} + +//DiscardComplexOrigin - by default, we are now keeping the complex struct associated with a create entry. +//This allows us to fix the merge to new object issue of not having enough change log details when allocating +//new objects. This however is a trade off of memory size and complexity vs correctness which is often only +//necessary when embedding structs in slices and arrays. It memory constrained environments, it may be desirable +//to turn this feature off however from a computational perspective, keeping the complex origin is actually quite +//cheap so, make sure you're extremely clear on the pitfalls of turning this off prior to doing so. +func DiscardComplexOrigin() func(d *Differ) error { + return func(d *Differ) error { + d.DiscardParent = true + return nil + } +} + +// Filter allows you to determine which fields the differ descends into +func Filter(f FilterFunc) func(d *Differ) error { + return func(d *Differ) error { + d.Filter = f + return nil + } +} diff --git a/patch.go b/patch.go new file mode 100644 index 0000000..5814c53 --- /dev/null +++ b/patch.go @@ -0,0 +1,237 @@ +package diff + +import ( + "reflect" +) + +/** + This is a method of applying a changelog to a value or struct. change logs + should be generated with Diff and never manually created. This DOES NOT + apply fuzzy logic as would be in the case of a text patch. It does however + have a few additional features added to our struct tags. + + 1) create. This tag on a struct field indicates that the patch should + create the value if it's not there. I.e. if it's nil. This works for + pointers, maps and slices. + + 2) omitunequal. Generally, you don't want to do this, the expectation is + that if an item isn't there, you want to add it. For example, if your + diff shows an array element at index 6 is a string 'hello' but your target + only has 3 elements, none of them matching... you want to add 'hello' + regardless of the index. (think in a distributed context, another process + may have deleted more than one entry and 'hello' may no longer be in that + indexed spot. + + So given this scenario, the default behavior is to scan for the previous + value and replace it anyway, or simply append the new value. For maps the + default behavior is to simply add the key if it doesn't match. + + However, if you don't like the default behavior, and add the omitunequal + tag to your struct, patch will *NOT* update an array or map with the key + or array value unless they key or index contains a 'match' to the + previous value. In which case it will skip over that change. + + Patch is implemented as a best effort algorithm. That means you can receive + multiple nested errors and still successfully have a modified target. This + may even be acceptable depending on your use case. So keep in mind, just + because err != nil *DOESN'T* mean that the patch didn't accomplish your goal + in setting those changes that are actually available. For example, you may + diff two structs of the same type, then attempt to apply to an entirely + different struct that is similar in constitution (think interface here) and + you may in fact get all of the values populated you wished to anyway. +*/ + +//Not strictly necessary but might be nice in some cases +//go:generate stringer -type=PatchFlags +type PatchFlags uint32 + +const ( + OptionCreate PatchFlags = 1 << iota + OptionNoCreate + OptionOmitUnequal + OptionImmutable + FlagInvalidTarget + FlagApplied + FlagFailed + FlagCreated + FlagIgnored + FlagDeleted + FlagUpdated + FlagParentSetApplied + FlagParentSetFailed +) + +//PatchLogEntry defines how a DiffLog entry was applied +type PatchLogEntry struct { + Path []string `json:"path"` + From interface{} `json:"from"` + To interface{} `json:"to"` + Flags PatchFlags `json:"flags"` + Errors error `json:"errors"` +} +type PatchLog []PatchLogEntry + +//HasFlag - convenience function for users +func (p PatchLogEntry) HasFlag(flag PatchFlags) bool { + return (p.Flags & flag) != 0 +} + +//Applied - returns true if all change log entries were actually +// applied, regardless of if any errors were encountered +func (p PatchLog) Applied() bool { + if p.HasErrors() { + for _, ple := range p { + if !ple.HasFlag(FlagApplied) { + return false + } + } + } + return true +} + +//HasErrors - indicates if a patch log contains any errors +func (p PatchLog) HasErrors() (ret bool) { + for _, ple := range p { + if ple.Errors != nil { + ret = true + } + } + return +} + +//ErrorCount -- counts the number of errors encountered while patching +func (p PatchLog) ErrorCount() (ret uint) { + for _, ple := range p { + if ple.Errors != nil { + ret++ + } + } + return +} + +func Merge(original interface{}, changed interface{}, target interface{}) (PatchLog, error) { + d, _ := NewDiffer() + return d.Merge(original, changed, target) +} + +// Merge is a convenience function that diffs, the original and changed items +// and merges said changes with target all in one call. +func (d *Differ) Merge(original interface{}, changed interface{}, target interface{}) (PatchLog, error) { + StructMapKeySupport()(d) // nolint: errcheck + if cl, err := d.Diff(original, changed); err == nil { + return Patch(cl, target), nil + } else { + return nil, err + } +} + +func Patch(cl Changelog, target interface{}) (ret PatchLog) { + d, _ := NewDiffer() + return d.Patch(cl, target) +} + +//Patch... the missing feature. +func (d *Differ) Patch(cl Changelog, target interface{}) (ret PatchLog) { + for _, c := range cl { + ret = append(ret, NewPatchLogEntry(NewChangeValue(d, c, target))) + } + return ret +} + +//NewPatchLogEntry converts our complicated reflection based struct to +//a simpler format for the consumer +func NewPatchLogEntry(cv *ChangeValue) PatchLogEntry { + return PatchLogEntry{ + Path: cv.change.Path, + From: cv.change.From, + To: cv.change.To, + Flags: cv.flags, + Errors: cv.err, + } +} + +//NewChangeValue idiomatic constructor (also invokes render) +func NewChangeValue(d *Differ, c Change, target interface{}) (ret *ChangeValue) { + val := reflect.ValueOf(target) + ret = &ChangeValue{ + target: &val, + change: &c, + } + d.renderChangeTarget(ret) + return +} + +//renderChangeValue applies 'path' in change to target. nil check is foregone +// here as we control usage +func (d *Differ) renderChangeTarget(c *ChangeValue) { + //This particular change element may potentially have the immutable flag + if c.HasFlag(OptionImmutable) { + c.AddError(NewError("Option immutable set, cannot apply change")) + return + } //the we always set a failure, and only unset if we successfully render the element + c.SetFlag(FlagInvalidTarget) + + //substitute and solve for t (path) + switch c.target.Kind() { + + //path element that is a map + case reflect.Map: + //map elements are 'copies' and immutable so if we set the new value to the + //map prior to editing the value, it will fail to stick. To fix this, we + //defer the safe until the stack unwinds + m, k, v := d.renderMap(c) + defer d.updateMapEntry(c, m, k, v) + + //path element that is a slice + case reflect.Slice: + d.renderSlice(c) + + //walking a path means dealing with real elements + case reflect.Interface, reflect.Ptr: + if c.target.IsNil() { + n := reflect.New(c.target.Type().Elem()) + c.target.Set(n) + c.target = &n + d.renderChangeTarget(c) + return + } + + el := c.target.Elem() + c.target = &el + c.ClearFlag(FlagInvalidTarget) + + //path element that is a struct + case reflect.Struct: + d.patchStruct(c) + } + + //if for some reason, rendering this element fails, c will no longer be valid + //we are best effort though, so we keep on trucking + if !c.IsValid() { + c.AddError(NewErrorf("Unable to access path position %d. Target field is invalid", c.pos)) + } + + //we've taken care of this path element, are there any more? if so, process + //else, let's take some action + if c.pos < len(c.change.Path) && !c.HasFlag(FlagInvalidTarget) { + d.renderChangeTarget(c) + + } else { //we're at the end of the line... set the Value + switch c.change.Type { + case DELETE: + switch c.ParentKind() { + case reflect.Slice: + d.deleteSliceEntry(c) + case reflect.Struct: + d.deleteStructEntry(c) + default: + c.SetFlag(FlagIgnored) + } + case UPDATE, CREATE: + // this is generic because... we only deal in primitives here. AND + // the diff format To field already contains the correct type. + c.Set(reflect.ValueOf(c.change.To), d.ConvertCompatibleTypes) + c.SetFlag(FlagUpdated) + } + } +} diff --git a/patch_map.go b/patch_map.go new file mode 100644 index 0000000..6c3f035 --- /dev/null +++ b/patch_map.go @@ -0,0 +1,106 @@ +package diff + +import ( + "errors" + "reflect" + + "github.com/vmihailenco/msgpack/v5" +) + +// renderMap - handle map rendering for patch +func (d *Differ) renderMap(c *ChangeValue) (m, k, v *reflect.Value) { + //we must tease out the type of the key, we use the msgpack from diff to recreate the key + kt := c.target.Type().Key() + field := reflect.New(kt) + + if d.StructMapKeys { + if err := msgpack.Unmarshal([]byte(c.change.Path[c.pos]), field.Interface()); err != nil { + c.SetFlag(FlagIgnored) + c.AddError(NewError("Unable to unmarshal path element to target type for key in map", err)) + return + } + c.key = field.Elem() + } else { + c.key = reflect.ValueOf(c.change.Path[c.pos]) + } + + if c.target.IsNil() && c.target.IsValid() { + c.target.Set(reflect.MakeMap(c.target.Type())) + } + + // we need to check that MapIndex does not panic here + // when the key type is not a string + defer func() { + if err := recover(); err != nil { + switch x := err.(type) { + case error: + c.AddError(NewError("Unable to unmarshal path element to target type for key in map", x)) + case string: + c.AddError(NewError("Unable to unmarshal path element to target type for key in map", errors.New(x))) + } + c.SetFlag(FlagIgnored) + } + }() + + x := c.target.MapIndex(c.key) + + if !x.IsValid() && c.change.Type != DELETE && !c.HasFlag(OptionNoCreate) { + x = c.NewElement() + } + if x.IsValid() { //Map elements come out as read only so we must convert + nv := reflect.New(x.Type()).Elem() + nv.Set(x) + x = nv + } + + if x.IsValid() && !reflect.DeepEqual(c.change.From, x.Interface()) && + c.HasFlag(OptionOmitUnequal) { + c.SetFlag(FlagIgnored) + c.AddError(NewError("target change doesn't match original")) + return + } + mp := *c.target //these may change out from underneath us as we recurse + key := c.key //so we make copies and pass back pointers to them + c.swap(&x) + + return &mp, &key, &x + +} + +// updateMapEntry - deletes are special, they are handled differently based on options +// +// container type etc. We have to have special handling for each +// type. Set values are more generic even if they must be instanced +func (d *Differ) updateMapEntry(c *ChangeValue, m, k, v *reflect.Value) { + if k == nil || m == nil { + return + } + + switch c.change.Type { + case DELETE: + if c.HasFlag(FlagDeleted) { + return + } + + if !m.CanSet() && v.IsValid() && v.Kind() == reflect.Struct { + for x := 0; x < v.NumField(); x++ { + if !v.Field(x).IsZero() { + m.SetMapIndex(*k, *v) + return + } + } //if all the fields are zero, remove from map + } + + m.SetMapIndex(*k, reflect.Value{}) + c.SetFlag(FlagDeleted) + + case CREATE: + m.SetMapIndex(*k, *v) + c.SetFlag(FlagCreated) + + case UPDATE: + m.SetMapIndex(*k, *v) + c.SetFlag(FlagUpdated) + + } +} diff --git a/patch_slice.go b/patch_slice.go new file mode 100644 index 0000000..9a703e5 --- /dev/null +++ b/patch_slice.go @@ -0,0 +1,78 @@ +package diff + +/** + Types are being split out to more closely follow the library structure already + in place. Keeps the file simpler as well. +*/ +import ( + "reflect" + "strconv" +) + +//renderSlice - handle slice rendering for patch +func (d *Differ) renderSlice(c *ChangeValue) { + + var err error + field := c.change.Path[c.pos] + + //field better be an index of the slice + if c.index, err = strconv.Atoi(field); err != nil { + //if struct element is has identifier, use it instead + if identifier(d.TagName, reflect.Zero(c.target.Type().Elem())) != nil { + for c.index = 0; c.index < c.Len(); c.index++ { + if identifier(d.TagName, c.Index(c.index)) == field { + break + } + } + } else { + c.AddError(NewErrorf("invalid index in path. %s is not a number", field). + WithCause(err)) + } + } + var x reflect.Value + if c.Len() > c.index { + x = c.Index(c.index) + } else if c.change.Type == CREATE && !c.HasFlag(OptionNoCreate) { + x = c.NewArrayElement() + } + if !x.IsValid() { + if !c.HasFlag(OptionOmitUnequal) { + c.AddError(NewErrorf("Value index %d is invalid", c.index). + WithCause(NewError("scanning for Value index"))) + for c.index = 0; c.index < c.Len(); c.index++ { + y := c.Index(c.index) + if reflect.DeepEqual(y, c.change.From) { + c.AddError(NewErrorf("Value changed index to %d", c.index)) + x = y + break + } + } + } + } + if !x.IsValid() && c.change.Type != DELETE && !c.HasFlag(OptionNoCreate) { + x = c.NewArrayElement() + } + if !x.IsValid() && c.change.Type == DELETE { + c.index = -1 //no existing element to delete so don't bother + } + c.swap(&x) //containers must swap out the parent Value +} + +//deleteSliceEntry - deletes are special, they are handled differently based on options +// container type etc. We have to have special handling for each +// type. Set values are more generic even if they must be instanced +func (d *Differ) deleteSliceEntry(c *ChangeValue) { + //for a slice with only one element + if c.ParentLen() == 1 && c.index != -1 { + c.ParentSet(reflect.MakeSlice(c.parent.Type(), 0, 0), d.ConvertCompatibleTypes) + c.SetFlag(FlagDeleted) + //for a slice with multiple elements + } else if c.index != -1 { //this is an array delete the element from the parent + c.ParentIndex(c.index).Set(c.ParentIndex(c.ParentLen() - 1)) + c.ParentSet(c.parent.Slice(0, c.ParentLen()-1), d.ConvertCompatibleTypes) + c.SetFlag(FlagDeleted) + //for other slice elements, we ignore + } else { + c.SetFlag(FlagIgnored) + } +} diff --git a/patch_struct.go b/patch_struct.go new file mode 100644 index 0000000..4e2d247 --- /dev/null +++ b/patch_struct.go @@ -0,0 +1,66 @@ +package diff + +import "reflect" + +/** + Types are being split out to more closely follow the library structure already + in place. Keeps the file simpler as well. +*/ + +type structField struct { + f reflect.StructField + v reflect.Value +} + +func getNestedFields(v reflect.Value, flattenEmbedded bool) []structField { + fields := make([]structField, 0) + + for i := 0; i < v.NumField(); i++ { + f := v.Type().Field(i) + fv := v.Field(i) + + if fv.Kind() == reflect.Struct && f.Anonymous && flattenEmbedded { + fields = append(fields, getNestedFields(fv, flattenEmbedded)...) + } else { + fields = append(fields, structField{f, fv}) + } + } + + return fields +} + +//patchStruct - handles the rendering of a struct field +func (d *Differ) patchStruct(c *ChangeValue) { + + field := c.change.Path[c.pos] + + structFields := getNestedFields(*c.target, d.FlattenEmbeddedStructs) + for _, structField := range structFields { + f := structField.f + tname := tagName(d.TagName, f) + if tname == "-" { + continue + } + if tname == field || f.Name == field { + x := structField.v + if hasTagOption(d.TagName, f, "nocreate") { + c.SetFlag(OptionNoCreate) + } + if hasTagOption(d.TagName, f, "omitunequal") { + c.SetFlag(OptionOmitUnequal) + } + if hasTagOption(d.TagName, f, "immutable") { + c.SetFlag(OptionImmutable) + } + c.swap(&x) + break + } + } +} + +//track and zero out struct members +func (d *Differ) deleteStructEntry(c *ChangeValue) { + + //deleting a struct value set's it to the 'basic' type + c.Set(reflect.Zero(c.target.Type()), d.ConvertCompatibleTypes) +} diff --git a/patch_test.go b/patch_test.go new file mode 100644 index 0000000..fe7f8cd --- /dev/null +++ b/patch_test.go @@ -0,0 +1,351 @@ +package diff_test + +import ( + "encoding/json" + "testing" + "time" + + "github.com/r3labs/diff/v3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPatch(t *testing.T) { + cases := []struct { + Name string + A, B interface{} + Changelog diff.Changelog + }{ + { + "uint-slice-insert", &[]uint{1, 2, 3}, &[]uint{1, 2, 3, 4}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: uint(4)}, + }, + }, + { + "int-slice-insert", &[]int{1, 2, 3}, &[]int{1, 2, 3, 4}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: 4}, + }, + }, + { + "uint-slice-delete", &[]uint{1, 2, 3}, &[]uint{1, 3}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: uint(2)}, + }, + }, + { + "int-slice-delete", &[]int{1, 2, 3}, &[]int{1, 3}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: 2}, + }, + }, + { + "uint-slice-insert-delete", &[]uint{1, 2, 3}, &[]uint{1, 3, 4}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: uint(2)}, + diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: uint(4)}, + }, + }, + { + "int-slice-insert-delete", &[]int{1, 2, 3}, &[]int{1, 3, 4}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: 2}, + diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: 4}, + }, + }, + { + "string-slice-insert", &[]string{"1", "2", "3"}, &[]string{"1", "2", "3", "4"}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: "4"}, + }, + }, + { + "string-slice-delete", &[]string{"1", "2", "3"}, &[]string{"1", "3"}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"}, + }, + }, + { + "string-slice-insert-delete", &[]string{"1", "2", "3"}, &[]string{"1", "3", "4"}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"}, + diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: "4"}, + }, + }, + { + "comparable-slice-update", &[]tistruct{{"one", 1}, {"two", 2}}, &[]tistruct{{"one", 1}, {"two", 50}}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"two", "value"}, From: 1, To: 50}, + }, + }, + { + "struct-string-update", &tstruct{Name: "one"}, &tstruct{Name: "two"}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"name"}, From: "one", To: "two"}, + }, + }, + { + "struct-int-update", &tstruct{Value: 1}, &tstruct{Value: 50}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"value"}, From: 1, To: 50}, + }, + }, + { + "struct-bool-update", &tstruct{Bool: true}, &tstruct{Bool: false}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"bool"}, From: true, To: false}, + }, + }, + { + "struct-time-update", &tstruct{}, &tstruct{Time: currentTime}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"time"}, From: time.Time{}, To: currentTime}, + }, + }, + { + "struct-nil-string-pointer-update", &tstruct{Pointer: nil}, &tstruct{Pointer: sptr("test")}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"pointer"}, From: nil, To: sptr("test")}, + }, + }, + { + "struct-string-pointer-update-to-nil", &tstruct{Pointer: sptr("test")}, &tstruct{Pointer: nil}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"pointer"}, From: sptr("test"), To: nil}, + }, + }, { + "struct-generic-slice-insert", &tstruct{Values: []string{"one"}}, &tstruct{Values: []string{"one", "two"}}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"values", "1"}, From: nil, To: "two"}, + }, + }, + { + "struct-generic-slice-delete", &tstruct{Values: []string{"one", "two"}}, &tstruct{Values: []string{"one"}}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"values", "1"}, From: "two", To: nil}, + }, + }, + { + "struct-unidentifiable-slice-insert-delete", &tstruct{Unidentifiables: []tuistruct{{1}, {2}, {3}}}, &tstruct{Unidentifiables: []tuistruct{{5}, {2}, {3}, {4}}}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"unidentifiables", "0", "value"}, From: 1, To: 5}, + diff.Change{Type: diff.CREATE, Path: []string{"unidentifiables", "3", "value"}, From: nil, To: 4}, + }, + }, + { + "slice", &tstruct{}, &tstruct{Nested: tnstruct{Slice: []tmstruct{{"one", 1}, {"two", 2}}}}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "0", "foo"}, From: nil, To: "one"}, + diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "0", "bar"}, From: nil, To: 1}, + diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "1", "foo"}, From: nil, To: "two"}, + diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "1", "bar"}, From: nil, To: 2}, + }, + }, + { + "slice-duplicate-items", &[]int{1}, &[]int{1, 1}, + diff.Changelog{ + diff.Change{Type: diff.CREATE, Path: []string{"1"}, From: nil, To: 1}, + }, + }, + { + "embedded-struct-field", + &embedstruct{Embedded{Foo: "a", Bar: 2}, true}, + &embedstruct{Embedded{Foo: "b", Bar: 3}, false}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: "a", To: "b"}, + diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: 2, To: 3}, + diff.Change{Type: diff.UPDATE, Path: []string{"baz"}, From: true, To: false}, + }, + }, + { + "custom-tags", + &customTagStruct{Foo: "abc", Bar: 3}, + &customTagStruct{Foo: "def", Bar: 4}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: "abc", To: "def"}, + diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: 3, To: 4}, + }, + }, + { + "custom-types", + &customTypeStruct{Foo: "a", Bar: 1}, + &customTypeStruct{Foo: "b", Bar: 2}, + diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: CustomStringType("a"), To: CustomStringType("b")}, + diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: CustomIntType(1), To: CustomIntType(2)}, + }, + }, + { + "map", + map[string]interface{}{"1": "one", "3": "three"}, + map[string]interface{}{"2": "two", "3": "tres"}, + diff.Changelog{ + diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "one", To: nil}, + diff.Change{Type: diff.CREATE, Path: []string{"2"}, From: nil, To: "two"}, + diff.Change{Type: diff.UPDATE, Path: []string{"3"}, From: "three", To: "tres"}, + }, + }, + { + "map-nested-create", + map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active", "attributes": map[string]interface{}{"attrA": "A", "attrB": "B"}}}, + map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active", "attributes": map[string]interface{}{"attrA": "A", "attrB": "B"}, "secondary-attributes": map[string]interface{}{"attrA": "A", "attrB": "B"}}}, + diff.Changelog{ + diff.Change{Type: "create", Path: []string{"details", "secondary-attributes"}, From: nil, To: map[string]interface{}{"attrA": "A", "attrB": "B"}}, + }, + }, + { + "map-nested-update", + map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active", "attributes": map[string]interface{}{"attrA": "A", "attrB": "B"}}}, + map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active", "attributes": map[string]interface{}{"attrA": "C", "attrD": "X"}}}, + diff.Changelog{ + diff.Change{Type: "update", Path: []string{"details", "attributes"}, From: map[string]interface{}{"attrA": "A", "attrB": "B"}, To: map[string]interface{}{"attrA": "C", "attrD": "X"}}, + }, + }, + { + "map-nested-delete", + map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active", "attributes": map[string]interface{}{"attrA": "A", "attrB": "B"}}}, + map[string]interface{}{"firstName": "John", "lastName": "Michael", "createdBy": "TS", "details": map[string]interface{}{"status": "active"}}, + diff.Changelog{ + diff.Change{Type: "delete", Path: []string{"details", "attributes"}, From: map[string]interface{}{"attrA": "A", "attrB": "B"}, To: nil}, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.Name, func(t *testing.T) { + + var options []func(d *diff.Differ) error + switch tc.Name { + case "mixed-slice-map", "nil-map", "map-nil": + options = append(options, diff.StructMapKeySupport()) + case "embedded-struct-field": + options = append(options, diff.FlattenEmbeddedStructs()) + case "custom-tags": + options = append(options, diff.TagName("json")) + } + d, err := diff.NewDiffer(options...) + if err != nil { + panic(err) + } + pl := d.Patch(tc.Changelog, tc.A) + + assert.Equal(t, tc.B, tc.A) + require.Equal(t, len(tc.Changelog), len(pl)) + assert.False(t, pl.HasErrors()) + }) + } + + t.Run("convert-types", func(t *testing.T) { + a := &tmstruct{Foo: "a", Bar: 1} + b := &customTypeStruct{Foo: "b", Bar: 2} + cl := diff.Changelog{ + diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: CustomStringType("a"), To: CustomStringType("b")}, + diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: CustomIntType(1), To: CustomIntType(2)}, + } + + d, err := diff.NewDiffer() + if err != nil { + panic(err) + } + pl := d.Patch(cl, a) + + assert.True(t, pl.HasErrors()) + + d, err = diff.NewDiffer(diff.ConvertCompatibleTypes()) + if err != nil { + panic(err) + } + pl = d.Patch(cl, a) + + assert.False(t, pl.HasErrors()) + assert.Equal(t, string(b.Foo), a.Foo) + assert.Equal(t, int(b.Bar), a.Bar) + require.Equal(t, len(cl), len(pl)) + }) + + t.Run("pointer", func(t *testing.T) { + type tps struct { + S *string + } + + str1 := "before" + str2 := "after" + + t1 := tps{S: &str1} + t2 := tps{S: &str2} + + changelog, err := diff.Diff(t1, t2) + assert.NoError(t, err) + + patchLog := diff.Patch(changelog, &t1) + assert.False(t, patchLog.HasErrors()) + }) + + t.Run("map-to-pointer", func(t *testing.T) { + t1 := make(map[string]*tmstruct) + t2 := map[string]*tmstruct{"after": {Foo: "val"}} + + changelog, err := diff.Diff(t1, t2) + assert.NoError(t, err) + + patchLog := diff.Patch(changelog, &t1) + assert.False(t, patchLog.HasErrors()) + + assert.True(t, len(t2) == len(t1)) + assert.Equal(t, t1["after"], &tmstruct{Foo: "val"}) + }) + + t.Run("map-to-nil-pointer", func(t *testing.T) { + t1 := make(map[string]*tmstruct) + t2 := map[string]*tmstruct{"after": nil} + + changelog, err := diff.Diff(t1, t2) + assert.NoError(t, err) + + patchLog := diff.Patch(changelog, &t1) + assert.False(t, patchLog.HasErrors()) + + assert.Equal(t, len(t2), len(t1)) + assert.Nil(t, t1["after"]) + }) + + t.Run("pointer-with-converted-type", func(t *testing.T) { + type tps struct { + S *int + } + + val1 := 1 + val2 := 2 + + t1 := tps{S: &val1} + t2 := tps{S: &val2} + + changelog, err := diff.Diff(t1, t2) + assert.NoError(t, err) + + js, err := json.Marshal(changelog) + assert.NoError(t, err) + + assert.NoError(t, json.Unmarshal(js, &changelog)) + + d, err := diff.NewDiffer(diff.ConvertCompatibleTypes()) + assert.NoError(t, err) + + assert.Equal(t, 1, *t1.S) + + patchLog := d.Patch(changelog, &t1) + assert.False(t, patchLog.HasErrors()) + assert.Equal(t, 2, *t1.S) + + // test nil pointer + t1 = tps{S: &val1} + t2 = tps{S: nil} + + changelog, err = diff.Diff(t1, t2) + assert.NoError(t, err) + + patchLog = d.Patch(changelog, &t1) + assert.False(t, patchLog.HasErrors()) + }) +}