Home > Back-end >  How to diff entire struct with r3labs/diff
How to diff entire struct with r3labs/diff

Time:11-21

I've come across the github.com/r3labs/diff library for Go language to compare two structs of the same type.

Library is working quite well, except for one use-case which is the following: I am using the Date struct to represent a date:

type Date struct {
    Year  int
    Month int
    Day   int
}

Now, there are some other more complex structs that make use of the Date struct let's say for example:

type Student struct {
  DateOfBirth Date
}

If I am about the compare two students, like

diff.Diff(
  Student{DateOfBirth: Date{2021, 11, 13}},
  Student{DateOfBirth: Date{2021, 10, 9}},
)

what I would get as a result is a changelog with 2 items, one for the DateOfBirth > Month and the other for the DateOfBirth > Day.

My desired result would be a changelog with a single item (DateOfBirth) and the value of 2021-10-09.

Is that possible somehow with the library?

CodePudding user response:

NOTE this solution is using github.com/r3labs/diff/v2.

There isn't such an option. The diff just processes struct fields recursively and produces the changelog for each different field.

To achieve the output you want, you can implement your own ValueDiffer. That way you can diff structs "atomically" and append to the changelog in the format you want.

A contrived example, partially copied from the package internals:

type DateDiffer struct {
}

// Whether this differ should be used to match a specific type
func (d *DateDiffer) Match(a, b reflect.Value) bool {
    return diff.AreType(a, b, reflect.TypeOf(Date{}))
}

// The actual diff function, where you also append to the changelog
// using your custom format
func (d *DateDiffer) Diff(cl *diff.Changelog, path []string, a, b reflect.Value) 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 d1, d2 Date
    d1, _ = a.Interface().(Date)
    d2, _ = b.Interface().(Date)
    if d1.Day != d2.Day || d1.Month != d2.Month || d1.Year != d2.Year {
        cl.Add(diff.UPDATE, path, fmt.Sprintf("%d-%d-%d", d1.Year, d1.Month, d1.Day), fmt.Sprintf("%d-%d-%d", d2.Year, d2.Month, d2.Day))
    }
    return nil
}

// unsure what this is actually for, but you must implement it either way
func (d *DateDiffer) InsertParentDiffer(dfunc func(path []string, a, b reflect.Value, p interface{}) error) {
    return
}

And then you use it as such:

    d2, _ := diff.NewDiffer(diff.CustomValueDiffers(&DateDiffer{}))

    s1 := Student{DateOfBirth: Date{2021, 11, 13}}
    s2 := Student{DateOfBirth: Date{2021, 10, 9}}

    ch2, _ := d2.Diff(s1, s2)

Output (json marshalled and indented):

[
  {
   "type": "update",
   "path": [
    "DateOfBirth"
   ],
   "from": "2021-11-13",
   "to": "2021-10-9"
  }
 ]

CodePudding user response:

After some research I've found the solution.

I needed to create a custom differ for the Date and also use the DisableStructValues option from the package.

This option is useful as it disables populating a separate change for each item in a struct, and returns the whole object when comparing it to a nil value.

diff.Diff(
  Student{DateOfBirth: Date{2021, 11, 13}},
  Student{DateOfBirth: Date{2021, 10, 9}},
  diff.CustomValueDiffers(differ.DateDiffer{}),
  diff.DisableStructValues()
)

To implement a custom differ, one needs a new struct that implements the following interface:

type ValueDiffer interface {
    Match(a, b reflect.Value) bool
    Diff(cl *Changelog, path []string, a, b reflect.Value) error
    InsertParentDiffer(dfunc func(path []string, a, b reflect.Value, p interface{}) error)
}

And here is the implementation for my custom differ.

type DateDiffer struct {
    DiffFunc (func(path []string, a, b reflect.Value, p interface{}) error)
}

func (differ DateDiffer) Match(a, b reflect.Value) bool {
    return diff.AreType(a, b, reflect.TypeOf(Date{}))
}

func (differ DateDiffer) Diff(cl *diff.Changelog, path []string, a, b reflect.Value) 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 source, target Date
    source, _ = a.Interface().(Date)
    target, _ = b.Interface().(Date)
    if !source.Equal(target) {
        cl.Add(diff.UPDATE, path, a.Interface(), b.Interface())
    }

    return nil
}

func (differ DateDiffer) InsertParentDiffer(dfunc func(path []string, a, b reflect.Value, p interface{}) error) {
    differ.DiffFunc = dfunc
}

Hope this will help someone with a similar use-case.

  • Related