pax_global_header00006660000000000000000000000064142236425630014521gustar00rootroot0000000000000052 comment=02e874c4da737b046500e271e2d0cb1c01875feb diff-3.0.0/000077500000000000000000000000001422364256300124315ustar00rootroot00000000000000diff-3.0.0/.gitignore000066400000000000000000000004601422364256300144210ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ .idea/ patchflags_string.go diff-3.0.0/.travis.yml000066400000000000000000000001061422364256300145370ustar00rootroot00000000000000language: go go: - "1.14" - master before_script: - make deps diff-3.0.0/CONTRIBUTING.md000066400000000000000000000065051422364256300146700ustar00rootroot00000000000000# Contributing guidelines Looking to contribute something to this project? Here's how you can help: Please take a moment to review this document in order to make the contribution process easy and effective for everyone involved. Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue or assessing patches and features. We also have a [code of conduct](https://ernest.io/conduct). ## Using the issue tracker The issue tracker is the preferred channel for [bug reports](#bug-reports), [features requests](#feature-requests) and [submitting pull requests](#pull-requests), but please respect the following restrictions: * Please **do not** use the issue tracker for personal support requests. * Please **do not** derail issues. Keep the discussion on topic and respect the opinions of others. ## Bug reports A bug is a _demonstrable problem_ that is caused by the code in the repository. Good bug reports are extremely helpful - thank you! Guidelines for bug reports: 1. **Use the GitHub issue search** — check if the issue has already been reported. 2. **Check if the issue has been fixed** — try to reproduce it using the latest `master` or `develop` branch in the repository. 3. **Isolate the problem** — create a reduced test case and a live example. A good bug report shouldn't leave others needing to chase you up for more information. Please try to be as detailed as possible in your report. What is your environment? What steps will reproduce the issue? Which environment experience the problem? What would you expect to be the outcome? All these details will help people to fix any potential bugs. Example: > Short and descriptive example bug report title > > A summary of the issue and the environment in which it occurs. If > suitable, include the steps required to reproduce the bug. > > 1. This is the first step > 2. This is the second step > 3. Further steps, etc. > > `` - a link to the reduced test case > > Any other information you want to share that is relevant to the issue being > reported. This might include the lines of code that you have identified as > causing the bug, and potential solutions (and your opinions on their > merits). ## Feature requests Feature requests are welcome. But take a moment to find out whether your idea fits with the scope and aims of the project. It's up to *you* to make a strong case to convince the project's developers of the merits of this feature. Please provide as much detail and context as possible. ## Pull requests Good pull requests - patches, improvements, new features - are a fantastic help. They should remain focused in scope and avoid containing unrelated commits. [**Please ask first**](https://ernest.io/community) before embarking on any significant pull request (e.g. implementing features, refactoring code, porting to a different language), otherwise you risk spending a lot of time working on something that the project's developers might not want to merge into the project. Please adhere to the coding conventions used throughout a project (indentation, accurate comments, etc.) and any other requirements (such as test coverage). diff-3.0.0/LICENSE000066400000000000000000000405251422364256300134440ustar00rootroot00000000000000Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public 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/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. diff-3.0.0/Makefile000066400000000000000000000002311422364256300140650ustar00rootroot00000000000000install: go install -v ${LDFLAGS} deps: go get github.com/stretchr/testify test: @go test -v -cover ./... cover: @go test -coverprofile cover.out diff-3.0.0/README.md000066400000000000000000000244571422364256300137240ustar00rootroot00000000000000# 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. NOTE: All active development now takes place on the v3 branch. ## Installation For version 3: ``` go get github.com/r3labs/diff/v3 ``` ## Changelog Format When diffing two structures using `Diff`, a changelog will be produced. Any detected changes will populate the changelog array with a Change type: ```go type Change struct { Type string // The type of change detected; can be one of create, update or delete Path []string // The path of the detected change; will contain any field name or array index that was part of the traversal From interface{} // The original value that was present in the "from" structure To interface{} // The new value that was detected as a change in the "to" structure } ``` Given the example below, we are diffing two slices where the third element has been removed: ```go from := []int{1, 2, 3, 4} to := []int{1, 2, 4} changelog, _ := diff.Diff(from, to) ``` The resultant changelog should contain one change: ```go Change{ Type: "delete", Path: ["2"], From: 3, To: nil, } ``` ## Supported Types A diffable value can be/contain any of the following types: | Type | Supported | | ------------ | --------- | | struct | ✔ | | slice | ✔ | | string | ✔ | | int | ✔ | | bool | ✔ | | map | ✔ | | pointer | ✔ | | custom types | ✔ | Please see the docs for more supported types, options and features. ### Tags 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 ### Basic Example 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/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) ... } ``` In this example, the output generated in the changelog will indicate that the third element with a value of '3' was removed from items. When marshalling the changelog to json, the output will look like: ```json [ { "type": "delete", "path": ["items", "2"], "from": 3, "to": null } ] ``` ### Options and Configuration 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/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}, } d, err := diff.NewDiffer(diff.SliceOrdering(true)) if err != nil { panic(err) } changelog, err := d.Diff(a, b) ... } ``` 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 ``` make test ``` ## Contributing Please read through our [contributing guidelines](CONTRIBUTING.md). Included are directions for opening issues, coding standards, and notes on development. Moreover, if your pull request contains patches or features, you must include relevant unit tests. ## Versioning For transparency into our release cycle and in striving to maintain backward compatibility, this project is maintained under [the Semantic Versioning guidelines](http://semver.org/). ## Copyright and License Code and documentation copyright since 2015 r3labs.io authors. Code released under [the Mozilla Public License Version 2.0](LICENSE). diff-3.0.0/change_value.go000066400000000000000000000116341422364256300154060ustar00rootroot00000000000000package 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-3.0.0/comparative.go000066400000000000000000000017731422364256300153020ustar00rootroot00000000000000/* This Source Code Form is subject to the terms of the Mozilla Public * 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 import ( "reflect" ) // Comparative ... type Comparative struct { A, B *reflect.Value } // ComparativeList : stores indexed comparative type ComparativeList struct { m map[interface{}]*Comparative keys []interface{} } // NewComparativeList : returns a new comparative list func NewComparativeList() *ComparativeList { return &ComparativeList{ m: make(map[interface{}]*Comparative), keys: make([]interface{}, 0), } } func (cl *ComparativeList) addA(k interface{}, v *reflect.Value) { if (*cl).m[k] == nil { (*cl).m[k] = &Comparative{} (*cl).keys = append((*cl).keys, k) } (*cl).m[k].A = v } func (cl *ComparativeList) addB(k interface{}, v *reflect.Value) { if (*cl).m[k] == nil { (*cl).m[k] = &Comparative{} (*cl).keys = append((*cl).keys, k) } (*cl).m[k].B = v } diff-3.0.0/diff.go000066400000000000000000000215171422364256300136760ustar00rootroot00000000000000/* This Source Code Form is subject to the terms of the Mozilla Public * 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 import ( "errors" "fmt" "reflect" "strconv" "strings" "github.com/vmihailenco/msgpack" ) const ( // CREATE represents when an element has been added CREATE = "create" // UPDATE represents when an element has been updated UPDATE = "update" // DELETE represents when an element has been removed DELETE = "delete" ) // DiffType represents an enum with all the supported diff types type DiffType uint8 const ( UNSUPPORTED DiffType = iota STRUCT SLICE ARRAY STRING BOOL INT UINT FLOAT MAP PTR INTERFACE ) func (t DiffType) String() string { switch t { case STRUCT: return "STRUCT" case SLICE: return "SLICE" case ARRAY: return "ARRAY" case STRING: return "STRING" case BOOL: return "BOOL" case INT: return "INT" case UINT: return "UINT" case FLOAT: return "FLOAT" case MAP: return "MAP" case PTR: return "PTR" case INTERFACE: return "INTERFACE" default: return "UNSUPPORTED" } } // DiffFunc represents the built-in diff functions type DiffFunc func([]string, reflect.Value, reflect.Value, interface{}) error // Differ a configurable diff instance type Differ struct { TagName string 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 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"` 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 func Changed(a, b interface{}) bool { cl, _ := Diff(a, b) return len(cl) > 0 } // Diff returns a changelog of all mutated values from both 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) { d := Differ{ TagName: "diff", DiscardParent: false, } for _, opt := range opts { err := opt(&d) if err != nil { return nil, err } } 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) { d := Differ{ TagName: "diff", DiscardParent: false, } v := reflect.ValueOf(s) return d.cl, d.structValues(t, path, v) } // FilterOut filter out the changes based on path. Paths may contain valid regexp to match items func (cl *Changelog) FilterOut(path []string) Changelog { var ncl Changelog for _, c := range *cl { if !pathmatch(path, c.Path) { ncl = append(ncl, c) } } return ncl } // Filter filter changes based on path. Paths may contain valid regexp to match items func (cl *Changelog) Filter(path []string) Changelog { var ncl Changelog 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 STRUCT, d.diffStruct case are(a, b, reflect.Slice, reflect.Invalid): return SLICE, d.diffSlice case are(a, b, reflect.Array, reflect.Invalid): return ARRAY, d.diffSlice case are(a, b, reflect.String, reflect.Invalid): return STRING, d.diffString case are(a, b, reflect.Bool, reflect.Invalid): return BOOL, d.diffBool case are(a, b, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Invalid): return INT, d.diffInt case are(a, b, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Invalid): return UINT, d.diffUint case are(a, b, reflect.Float32, reflect.Float64, reflect.Invalid): return FLOAT, d.diffFloat case are(a, b, reflect.Map, reflect.Invalid): return MAP, d.diffMap case are(a, b, reflect.Ptr, reflect.Invalid): return PTR, d.diffPtr case are(a, b, reflect.Interface, reflect.Invalid): return INTERFACE, d.diffInterface default: return UNSUPPORTED, nil } } // Diff returns a changelog of all mutated values from both func (d *Differ) Diff(a, b interface{}) (Changelog, error) { // reset the state of the diff 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, ftco ...interface{}) { change := Change{ Type: t, Path: path, From: ftco[0], To: ftco[1], } if len(ftco) > 2 { change.parent = ftco[2] } (*cl) = append((*cl), change) } func tagName(tag string, f reflect.StructField) string { t := f.Tag.Get(tag) parts := strings.Split(t, ",") if len(parts) < 1 { return "-" } return parts[0] } func identifier(tag string, v reflect.Value) interface{} { if v.Kind() != reflect.Struct { return nil } for i := 0; i < v.NumField(); i++ { if hasTagOption(tag, v.Type().Field(i), "identifier") { return v.Field(i).Interface() } } return nil } func hasTagOption(tag string, f reflect.StructField, opt string) bool { parts := strings.Split(f.Tag.Get(tag), ",") if len(parts) < 2 { return false } for _, option := range parts[1:] { if option == opt { return true } } return false } func swapChange(t string, c Change) Change { nc := Change{ Type: t, Path: c.Path, } switch t { case CREATE: nc.To = c.To case DELETE: nc.From = c.To } 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 := v.(type) { case string: return v case int: return strconv.Itoa(v) default: return fmt.Sprint(v) } } func invalid(a, b reflect.Value) bool { if a.Kind() == b.Kind() { return false } if a.Kind() == reflect.Invalid { return false } if b.Kind() == reflect.Invalid { return false } return true } func are(a, b reflect.Value, kinds ...reflect.Kind) bool { var amatch, bmatch bool for _, k := range kinds { if a.Kind() == k { amatch = true } if b.Kind() == k { bmatch = true } } return amatch && bmatch } func AreType(a, b reflect.Value, types ...reflect.Type) bool { var amatch, bmatch bool for _, t := range types { if a.Kind() != reflect.Invalid { if a.Type() == t { amatch = true } } if b.Kind() != reflect.Invalid { if b.Type() == t { bmatch = true } } } return amatch && bmatch } 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-3.0.0/diff_bool.go000066400000000000000000000012771422364256300147120ustar00rootroot00000000000000/* This Source Code Form is subject to the terms of the Mozilla Public * 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 import "reflect" func (d *Differ) diffBool(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { d.cl.Add(CREATE, path, nil, 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 a.Bool() != b.Bool() { d.cl.Add(UPDATE, path, exportInterface(a), exportInterface(b), parent) } return nil } diff-3.0.0/diff_comparative.go000066400000000000000000000021241422364256300162610ustar00rootroot00000000000000/* This Source Code Form is subject to the terms of the Mozilla Public * 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 import ( "reflect" ) func (d *Differ) diffComparative(path []string, c *ComparativeList, parent interface{}) error { for _, k := range c.keys { id := idstring(k) if d.StructMapKeys { id = idComplex(k) } fpath := copyAppend(path, id) nv := reflect.ValueOf(nil) if c.m[k].A == nil { c.m[k].A = &nv } if c.m[k].B == nil { c.m[k].B = &nv } err := d.diff(fpath, *c.m[k].A, *c.m[k].B, parent) if err != nil { return err } } return nil } 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(d.TagName, ak) != nil { return true } } } if b.Len() > 0 { be := b.Index(0) bk := getFinalValue(be) if bk.Kind() == reflect.Struct { if identifier(d.TagName, bk) != nil { return true } } } return false } diff-3.0.0/diff_examples_test.go000066400000000000000000000307021422364256300166270ustar00rootroot00000000000000package 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"` 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", }, Tags: []Tag{ { Name: "kind", Value: "fruit", }, }, } b := Fruit{ ID: 2, Name: "Red Apple", Healthy: true, Nutrients: []string{ "vitamin c", "vitamin d", "vitamin e", }, Tags: []Tag{ { Name: "popularity", Value: "high", }, { Name: "kind", Value: "fruit", }, }, } changelog, err := diff.Diff(a, b) if err != nil { panic(err) } 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-3.0.0/diff_float.go000066400000000000000000000014471422364256300150630ustar00rootroot00000000000000/* This Source Code Form is subject to the terms of the Mozilla Public * 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 import ( "reflect" ) func (d *Differ) diffFloat(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 a.Float() != b.Float() { 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-3.0.0/diff_int.go000066400000000000000000000014351422364256300145450ustar00rootroot00000000000000/* This Source Code Form is subject to the terms of the Mozilla Public * 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 import ( "reflect" ) func (d *Differ) diffInt(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 a.Int() != b.Int() { 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-3.0.0/diff_interface.go000066400000000000000000000015451422364256300157150ustar00rootroot00000000000000/* This Source Code Form is subject to the terms of the Mozilla Public * 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 import "reflect" func (d *Differ) diffInterface(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { d.cl.Add(CREATE, path, nil, 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 a.IsNil() && b.IsNil() { return nil } if a.IsNil() { d.cl.Add(UPDATE, path, nil, exportInterface(b), parent) return nil } if b.IsNil() { d.cl.Add(UPDATE, path, exportInterface(a), nil, parent) return nil } return d.diff(path, a.Elem(), b.Elem(), parent) } diff-3.0.0/diff_map.go000066400000000000000000000034171422364256300145320ustar00rootroot00000000000000/* This Source Code Form is subject to the terms of the Mozilla Public * 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 import ( "fmt" "reflect" "github.com/vmihailenco/msgpack" ) func (d *Differ) diffMap(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { return d.mapValues(CREATE, path, b) } if b.Kind() == reflect.Invalid { return d.mapValues(DELETE, path, a) } c := NewComparativeList() for _, k := range a.MapKeys() { ae := a.MapIndex(k) c.addA(exportInterface(k), &ae) } for _, k := range b.MapKeys() { be := b.MapIndex(k) c.addB(exportInterface(k), &be) } return d.diffComparative(path, c, exportInterface(a)) } func (d *Differ) mapValues(t string, path []string, a reflect.Value) error { if t != CREATE && t != DELETE { return ErrInvalidChangeType } if a.Kind() == reflect.Ptr { a = reflect.Indirect(a) } if a.Kind() != reflect.Map { return ErrTypeMismatch } x := reflect.New(a.Type()).Elem() for _, k := range a.MapKeys() { ae := a.MapIndex(k) xe := x.MapIndex(k) 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 } } for i := 0; i < len(d.cl); i++ { // only swap changes on the relevant map if pathmatch(path, d.cl[i].Path) { d.cl[i] = swapChange(t, d.cl[i]) } } return nil } diff-3.0.0/diff_pointer.go000066400000000000000000000026111422364256300154300ustar00rootroot00000000000000/* This Source Code Form is subject to the terms of the Mozilla Public * 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 import ( "reflect" "unsafe" ) var isExportFlag uintptr = (1 << 5) | (1 << 6) func (d *Differ) diffPtr(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() != b.Kind() { if a.Kind() == reflect.Invalid { if !b.IsNil() { return d.diff(path, reflect.ValueOf(nil), reflect.Indirect(b), parent) } d.cl.Add(CREATE, path, nil, exportInterface(b), parent) return nil } if b.Kind() == reflect.Invalid { if !a.IsNil() { return d.diff(path, reflect.Indirect(a), reflect.ValueOf(nil), parent) } d.cl.Add(DELETE, path, exportInterface(a), nil, parent) return nil } return ErrTypeMismatch } if a.IsNil() && b.IsNil() { return nil } if a.IsNil() { d.cl.Add(UPDATE, path, nil, exportInterface(b), parent) return nil } if b.IsNil() { d.cl.Add(UPDATE, path, exportInterface(a), nil, parent) return nil } return d.diff(path, reflect.Indirect(a), reflect.Indirect(b), parent) } func exportInterface(v reflect.Value) interface{} { if !v.CanInterface() { flagTmp := (*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + 2*unsafe.Sizeof(uintptr(0)))) *flagTmp = (*flagTmp) & (^isExportFlag) } return v.Interface() } diff-3.0.0/diff_slice.go000066400000000000000000000057341422364256300150600ustar00rootroot00000000000000/* This Source Code Form is subject to the terms of the Mozilla Public * 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 import ( "reflect" ) 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 d.comparative(a, b) { return d.diffSliceComparative(path, a, b) } return d.diffSliceGeneric(path, a, b) } func (d *Differ) diffSliceGeneric(path []string, a, b reflect.Value) error { missing := NewComparativeList() slice := sliceTracker{} for i := 0; i < a.Len(); i++ { ae := a.Index(i) if (d.SliceOrdering && !hasAtSameIndex(b, ae, i)) || (!d.SliceOrdering && !slice.has(b, ae, d)) { missing.addA(i, &ae) } } slice = sliceTracker{} for i := 0; i < b.Len(); i++ { be := b.Index(i) if (d.SliceOrdering && !hasAtSameIndex(a, be, i)) || (!d.SliceOrdering && !slice.has(a, be, d)) { missing.addB(i, &be) } } // fallback to comparing based on order in slice if item is missing if len(missing.keys) == 0 { return nil } return d.diffComparative(path, missing, exportInterface(a)) } func (d *Differ) diffSliceComparative(path []string, a, b reflect.Value) error { c := NewComparativeList() for i := 0; i < a.Len(); i++ { ae := a.Index(i) ak := getFinalValue(ae) id := identifier(d.TagName, ak) if id != nil { c.addA(id, &ae) } } for i := 0; i < b.Len(); i++ { be := b.Index(i) bk := getFinalValue(be) id := identifier(d.TagName, bk) if id != nil { c.addB(id, &be) } } return d.diffComparative(path, c, exportInterface(a)) } // 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, d *Differ) bool { if len(*st) != s.Len() { (*st) = make([]bool, s.Len()) } for i := 0; i < s.Len(); i++ { // skip already matched elements if (*st)[i] { continue } x := s.Index(i) 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 } } return false } func getFinalValue(t reflect.Value) reflect.Value { switch t.Kind() { case reflect.Interface: return getFinalValue(t.Elem()) case reflect.Ptr: return getFinalValue(reflect.Indirect(t)) default: return t } } func hasAtSameIndex(s, v reflect.Value, atIndex int) bool { // check the element in the slice at atIndex to see if it matches Value, if it is a valid index into the slice if atIndex < s.Len() { x := s.Index(atIndex) return reflect.DeepEqual(exportInterface(x), exportInterface(v)) } return false } diff-3.0.0/diff_string.go000066400000000000000000000016021422364256300152550ustar00rootroot00000000000000/* This Source Code Form is subject to the terms of the Mozilla Public * 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 import "reflect" func (d *Differ) diffString(path []string, a, b reflect.Value, parent interface{}) error { if a.Kind() == reflect.Invalid { d.cl.Add(CREATE, path, nil, 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 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-3.0.0/diff_struct.go000066400000000000000000000045021422364256300152750ustar00rootroot00000000000000/* This Source Code Form is subject to the terms of the Mozilla Public * 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 import ( "reflect" "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, exportInterface(b)) return nil } return d.structValues(CREATE, path, b) } if b.Kind() == reflect.Invalid { if d.DisableStructValues { d.cl.Add(DELETE, path, exportInterface(a), nil) return nil } return d.structValues(DELETE, path, a) } for i := 0; i < a.NumField(); i++ { field := a.Type().Field(i) tname := tagName(d.TagName, field) if tname == "-" || hasTagOption(d.TagName, field, "immutable") { continue } if tname == "" { tname = field.Name } af := a.Field(i) bf := b.FieldByName(field.Name) fpath := path if !(d.FlattenEmbeddedStructs && field.Anonymous) { fpath = copyAppend(fpath, tname) } if d.Filter != nil && !d.Filter(fpath, a.Type(), field) { continue } // skip private fields if !a.CanInterface() { continue } err := d.diff(fpath, af, bf, exportInterface(a)) if err != nil { return err } } return nil } 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 } if a.Kind() == reflect.Ptr { a = reflect.Indirect(a) } if a.Kind() != reflect.Struct { return ErrTypeMismatch } x := reflect.New(a.Type()).Elem() for i := 0; i < a.NumField(); i++ { field := a.Type().Field(i) tname := tagName(d.TagName, field) if tname == "-" { continue } if tname == "" { tname = field.Name } af := a.Field(i) xf := x.FieldByName(field.Name) fpath := copyAppend(path, tname) if nd.Filter != nil && !nd.Filter(fpath, a.Type(), field) { continue } err := nd.diff(fpath, xf, af, exportInterface(a)) if err != nil { return err } } for i := 0; i < len(nd.cl); i++ { (d.cl) = append(d.cl, swapChange(t, nd.cl[i])) } return nil } diff-3.0.0/diff_test.go000066400000000000000000001017341422364256300147350ustar00rootroot00000000000000/* This Source Code Form is subject to the terms of the Mozilla Public * 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_test import ( "reflect" "strings" "sync" "testing" "time" "github.com/r3labs/diff/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var currentTime = time.Now() var struct1 = tmstruct{Bar: 1, Foo: "one"} var struct2 = tmstruct{Bar: 2, Foo: "two"} type tistruct struct { Name string `diff:"name,identifier"` Value int `diff:"value"` } type tuistruct struct { Value int `diff:"value"` } type tnstruct struct { Slice []tmstruct `diff:"slice"` } type tmstruct struct { Foo string `diff:"foo"` 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"` Value int `diff:"value"` Bool bool `diff:"bool"` Values []string `diff:"values"` Map map[string]string `diff:"map"` Time time.Time `diff:"time"` Pointer *string `diff:"pointer"` Ignored bool `diff:"-"` Identifiables []tistruct `diff:"identifiables"` Unidentifiables []tuistruct `diff:"unidentifiables"` Nested tnstruct `diff:"nested"` private int `diff:"private"` } func sptr(s string) *string { return &s } func TestDiff(t *testing.T) { cases := []struct { Name string A, B interface{} Changelog diff.Changelog Error error }{ { "uint-slice-insert", []uint{1, 2, 3}, []uint{1, 2, 3, 4}, diff.Changelog{ diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: uint(4)}, }, nil, }, { "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}, 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}, 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}, 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"}, 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"}, 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"}, 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}}, 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}}, 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}}, 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"}}, 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"}}, 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"}}, 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"}}, 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, diff.Changelog{ diff.Change{Type: diff.DELETE, Path: []string{"\xa3one"}, From: "test", To: nil}, }, nil, }, { "nil-map", nil, map[string]string{"one": "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"}}, 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"}}, 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"}}, 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": {}}, 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-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-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-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"}, diff.Changelog{ diff.Change{Type: diff.UPDATE, Path: []string{"name"}, From: "one", To: "two"}, }, nil, }, { "struct-int-update", tstruct{Value: 1}, tstruct{Value: 50}, diff.Changelog{ diff.Change{Type: diff.UPDATE, Path: []string{"value"}, From: 1, To: 50}, }, nil, }, { "struct-bool-update", tstruct{Bool: true}, tstruct{Bool: false}, diff.Changelog{ diff.Change{Type: diff.UPDATE, Path: []string{"bool"}, From: true, To: false}, }, nil, }, { "struct-time-update", tstruct{}, tstruct{Time: currentTime}, diff.Changelog{ diff.Change{Type: diff.UPDATE, Path: []string{"time"}, From: time.Time{}, To: currentTime}, }, nil, }, { "struct-map-update", tstruct{Map: map[string]string{"test": "123"}}, tstruct{Map: map[string]string{"test": "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")}, 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")}, 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"}}, 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}}}, 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"}}, 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}}}, 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}}}, 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}}}, diff.Changelog{}, diff.ErrTypeMismatch, }, { "omittable", tstruct{Ignored: false}, tstruct{Ignored: true}, diff.Changelog{}, nil, }, { "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}, }, nil, }, { "slice-duplicate-items", []int{1}, []int{1, 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"}}}, 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, }, { "map-string-pointer-create", map[string]*tmstruct{"one": &struct1}, map[string]*tmstruct{"one": &struct1, "two": &struct2}, 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, }, { "map-string-pointer-delete", map[string]*tmstruct{"one": &struct1, "two": &struct2}, map[string]*tmstruct{"one": &struct1}, 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) { 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)) 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 TestDiffSliceOrdering(t *testing.T) { cases := []struct { Name string A, B interface{} Changelog diff.Changelog Error error }{ { "int-slice-insert-in-middle", []int{1, 2, 4}, []int{1, 2, 3, 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}, 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}, 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}, 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"}, 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"}, 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"}, diff.Changelog{ diff.Change{Type: diff.UPDATE, Path: []string{"1"}, From: "2", To: "3"}, diff.Change{Type: diff.UPDATE, Path: []string{"2"}, From: "3", To: "2"}, }, nil, }, { "nested-slice-delete", map[string][]int{"a": {1, 2, 3}}, map[string][]int{"a": {1, 3}}, 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, }, } for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { d, err := diff.NewDiffer(diff.SliceOrdering(true)) 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 TestFilter(t *testing.T) { cases := []struct { Name string Filter []string Expected [][]string }{ {"simple", []string{"item-1", "subitem"}, [][]string{{"item-1", "subitem"}}}, {"regex", []string{"item-*"}, [][]string{{"item-1", "subitem"}, {"item-2", "subitem"}}}, } 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.Filter(tc.Filter) assert.Len(t, ncl, len(tc.Expected)) for i, e := range tc.Expected { assert.Equal(t, e, ncl[i].Path) } }) } } 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 diff.Changelog Error error }{ { "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", 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, }, } for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { cl, err := diff.StructValues(tc.ChangeType, []string{}, tc.X) assert.Equal(t, tc.Error, err) assert.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 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 := diff.NewDiffer(diff.SliceOrdering(false)) require.Nil(t, err) assert.False(t, d.SliceOrdering) cl, err := d.Diff([]int{1, 2, 3}, []int{1, 3, 2}) require.Nil(t, err) assert.Len(t, cl, 0) d, err = diff.NewDiffer(diff.SliceOrdering(true)) require.Nil(t, err) assert.True(t, d.SliceOrdering) cl, err = d.Diff([]int{1, 2, 3}, []int{1, 3, 2}) require.Nil(t, err) assert.Len(t, cl, 2) // 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-3.0.0/diff_time.go000066400000000000000000000015451422364256300147130ustar00rootroot00000000000000/* This Source Code Form is subject to the terms of the Mozilla Public * 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 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, 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 } // 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-3.0.0/diff_uint.go000066400000000000000000000014421422364256300147300ustar00rootroot00000000000000/* This Source Code Form is subject to the terms of the Mozilla Public * 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 import ( "reflect" ) func (d *Differ) diffUint(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 a.Uint() != b.Uint() { 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-3.0.0/error.go000066400000000000000000000031771422364256300141210ustar00rootroot00000000000000package 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-3.0.0/filter.go000066400000000000000000000007101422364256300142430ustar00rootroot00000000000000/* This Source Code Form is subject to the terms of the Mozilla Public * 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 import "regexp" func pathmatch(filter, path []string) bool { for i, f := range filter { if len(path) < i+1 { return false } matched, _ := regexp.MatchString(f, path[i]) if !matched { return false } } return true } diff-3.0.0/go.mod000066400000000000000000000002771422364256300135450ustar00rootroot00000000000000module github.com/r3labs/diff/v3 go 1.13 require ( github.com/stretchr/testify v1.5.1 github.com/vmihailenco/msgpack v4.0.4+incompatible google.golang.org/appengine v1.6.6 // indirect ) diff-3.0.0/go.sum000066400000000000000000000042421422364256300135660ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff-3.0.0/options.go000066400000000000000000000062701422364256300144600ustar00rootroot00000000000000package 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 { d.SliceOrdering = enabled return nil } } // 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 { d.DisableStructValues = true 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-3.0.0/patch.go000066400000000000000000000163271422364256300140700ustar00rootroot00000000000000package 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-3.0.0/patch_map.go000066400000000000000000000052671422364256300147260ustar00rootroot00000000000000package diff import ( "errors" "reflect" "github.com/vmihailenco/msgpack" ) //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-3.0.0/patch_slice.go000066400000000000000000000046631422364256300152470ustar00rootroot00000000000000package 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-3.0.0/patch_struct.go000066400000000000000000000030171422364256300154640ustar00rootroot00000000000000package 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-3.0.0/patch_test.go000066400000000000000000000265251422364256300151300ustar00rootroot00000000000000package 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()) }) }