r3labs/diff

repository·master·Indexed 21 days ago

https://github.com/r3labs/diff

A Go library for diffing structures and values using reflection and struct tags. It generates a detailed changelog of modifications (create, update, delete) between two objects, which can be used to patch a target object via diff.Patch or diff.Merge. The library supports custom configuration through functional options, custom ValueDiffer implementations, and specific struct tags for controlling diffing behavior.

Tokens
7.3K
Snippets
35
Records
42
Agent score
76%

What's inside r3labs/diff

  1. Understand the Change struct format

    master

    When you call diff.Diff(from, to), the library returns a changelog containing Change objects. Each object describes a specific modification:

    • Type: The nature of the change (create, update, or delete).
    • Path: A slice of strings representing the traversal path (field names or array indices) to the changed value.
    • From: The original value in the from structure.
    • To: The new value detected in the to structure.
    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
    }
  2. Use ComparativeList to store indexed comparisons

    master

    The ComparativeList type is used to store indexed comparisons between two sets of values. It maintains a mapping of keys to Comparative objects, where each Comparative object holds a pointer to a reflect.Value for both side A and side B. This is useful when you need to track differences across keyed collections.

    To use it, initialize a list with NewComparativeList() and populate it using the addA and addB methods. Note that addA and addB are internal methods used to build the comparison state for a specific key k.

    // Initialize a new list
    cl := diff.NewComparativeList()
    
    // Note: addA and addB are used internally to associate 
    // reflect.Values with specific keys.
    // cl.addA(key, &valA)
    // cl.addB(key, &valB)
  3. Understand the DiffError structure

    master

    A DiffError is a specialized error type used throughout the library. It provides a structured way to report errors with context:

    • Message: The primary error description.
    • Count: An integer representing the number of causes appended to the error chain.
    • Next: The underlying error being wrapped.

    When calling .Error(), the output follows the format: <message> (cause count <count>) <cause_error_string>.

  4. Understand the Change structure

    master

    A Change object represents a single mutation. It contains the following fields:

    • Type: A string indicating the nature of the change (CREATE, UPDATE, or DELETE).
    • Path: A []string representing the breadcrumb path to the changed element.
    • From: The value before the change.
    • To: The value after the change.
    • parent: The parent object of the change (internal/optional).
  5. Configure struct fields using `diff` tags

    master

    For struct fields to be included in a diff, they must be tagged with diff:"name". All tag values are prefixed with diff.

    Supported tag options:

    • -: Excludes the value from being diffed.
    • identifier: Used for comparing arrays by a matching identifier instead of order. Example: diff:"name, identifier".
    • immutable: Omits the field from diffing, but includes it in the changelog when using diff.StructValues() (useful for showing full state when comparing against nil).
    • nocreate: Tells the patch function to skip elements that would otherwise require allocation.
    • omitunequal: Instructs the patcher to selectively ignore values that are not a 100% match to the 'from' value in the changelog.
  6. Perform a basic diff

    master

    Use diff.Diff(a, b) to compare two structures. Only fields with a diff tag will be compared.

    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)
        // changelog will indicate the third element (index 2) was deleted
    }
  7. Configure diffing behavior with Options

    master

    You can pass functional options to diff.Diff or use diff.NewDiffer to create a reusable differ instance with specific behaviors.

    Options:

    • diff.SliceOrdering(bool): Ensures slice item ordering is taken into account.
    • diff.DiscardComplexOrigin(): Omits additional origin information about structs to reduce memory footprint (may affect patch behavior).
    • diff.AllowTypeMismatch(bool): A global directive to allow/disallow patch applying if the 'from' value does not match the target.
    • diff.Filter(callback): A callback to determine which fields the differ descends into.
    • diff.DisableStructValues(): Disables populating a separate change for each item in a struct when compared to a nil value.
    • diff.TagName(string): Sets the tag name to use (e.g., diff.TagName("json")).
    // Using options at call time
    changelog, err := diff.Diff(a, b, diff.DisableStructValues(), diff.AllowTypeMismatch(true))
    
    // Using a Differ instance
    d, err := diff.NewDiffer(diff.SliceOrdering(true))
    if err != nil {
        panic(err)
    }
    changelog, err := d.Diff(a, b)
  8. Apply changes using Patch and Merge

    master

    The library provides mechanisms to apply a changelog to a target instance.

    • diff.Patch(changelog, target): A "best effort" operation that applies changes from the changelog to the target pointer. It returns a patchlog which can be used to check for errors encountered during the process.
    • diff.Merge(from, to, target): A convenience function that performs both the diff (between from and to) and the patch (applying the result to target) in one step.

    Note: Patch does not fail immediately; use patchlog.ErrorCount() to inspect how many issues occurred.

    // Patching an existing instance
    changelog, _ := diff.Diff(a, b)
    patchlog := diff.Patch(changelog, &c)
    fmt.Printf("Encountered %d errors while patching", patchlog.ErrorCount())
    
    // Merging in one step
    // a: from, b: to, c: target
    patchlog, err := diff.Merge(a, b, &c)
  9. Inspect PatchLog results

    master

    After applying a patch, you receive a PatchLog, which is a slice of PatchLogEntry. You can use these methods to inspect the outcome:

    • HasErrors(): Returns true if any entry in the log contains an error.
    • ErrorCount(): Returns the total number of errors encountered during the patching process.
    • Applied(): Returns true if all change log entries were successfully applied (even if errors were encountered elsewhere, this checks if every entry has the FlagApplied flag).
    • HasFlag(flag): (On an individual PatchLogEntry) Checks if a specific PatchFlags is set for that specific change.
    log, err := diff.Patch(cl, target)
    
    if log.HasErrors() {
        fmt.Printf("Encountered %d errors\n", log.ErrorCount())
    }
    
    if log.Applied() {
        fmt.Println("All changes were successfully applied")
    }
    
    // Inspecting a specific entry
    for _, entry := range log {
        if entry.HasFlag(diff.FlagUpdated) {
            fmt.Printf("Field %v was updated from %v to %v\n", entry.Path, entry.From, entry.To)
        }
    }
  10. Initialize a new ComparativeList with NewComparativeList

    master

    Call NewComparativeList() to create a new instance of ComparativeList. This initializes the underlying map and the keys slice required to track indexed comparisons.

    cl := diff.NewComparativeList()
  11. Implement a custom ValueDiffer

    master

    To handle specific types or logic differently (e.g., custom equality for a specific struct), implement the ValueDiffer interface. This allows you to intercept the diffing process before the built-in functions are called.

    Methods to implement:

    • Match(a, b reflect.Value) bool: Returns true if this differ should handle the given values.
    • Diff(dt DiffType, df DiffFunc, cl *Changelog, path []string, a, b reflect.Value, parent interface{}) error: The logic for performing the diff and adding to the changelog.
    • InsertParentDiffer(dfunc func(path []string, a, b reflect.Value, p interface{}) error): Used to provide a function for inserting parent context.