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.