Skip to content

feat: add ExactDiffReporter #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 69 additions & 28 deletions differ.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package mendoza

import (
"github.com/sanity-io/mendoza/internal/mendoza"
"unicode/utf8"

"github.com/sanity-io/mendoza/internal/mendoza"
)

type differ struct {
Expand Down Expand Up @@ -50,6 +51,7 @@ func (options *Options) CreatePatch(left, right interface{}) (Patch, error) {
left: leftList,
right: rightList,
hashIndex: hashIndex,
options: options,
}
return differ.build(), nil
}
Expand Down Expand Up @@ -143,7 +145,7 @@ func (d *differ) build() Patch {
},
}

d.reconstruct(0, reqs)
d.reconstruct(d.options.exactDiffReporter, 0, reqs)

req := reqs[0]

Expand All @@ -170,26 +172,30 @@ func (req *request) update(patch Patch, size int, outputKey string) {
}
}

func (d *differ) reconstruct(idx int, reqs []request) {
if len(reqs) == 0 {
return
}

// reconstruct is the main entry point for calculating the diff of a value in the right-side document.
func (d *differ) reconstruct(reporter ExactDiffReporter, idx int, reqs []request) {
entry := d.right.Entries[idx]

if entry.IsNonEmptyMap() {
d.reconstructMap(idx, reqs)
return
}
if len(reqs) > 0 {
if entry.IsNonEmptyMap() {
d.reconstructMap(reporter, idx, reqs)
// We return here because `reconstructMap` reports exact diffs recursively.
return
}

if entry.IsNonEmptySlice() {
d.reconstructSlice(idx, reqs)
return
if entry.IsNonEmptySlice() {
d.reconstructSlice(reporter, idx, reqs)
// We return here because `reconstructMap` reports exact diffs recursively.
return
}

if rightString, ok := entry.Value.(string); ok {
d.reconstructString(idx, rightString, reqs)
}
}

if rightString, ok := entry.Value.(string); ok {
d.reconstructString(idx, rightString, reqs)
return
if reporter != nil {
reporter.Report(entry.Value)
}
}

Expand Down Expand Up @@ -275,6 +281,8 @@ func (mc *mapCandidate) init(contextIdx int, requestIdx int) {
mc.contextIdx = contextIdx
}

// insertAlias is invoked when we find that one value in the left map matches another value in the right map.
// This will be invoked even if the key is different.
func (mc *mapCandidate) insertAlias(target mendoza.Reference, source mendoza.Reference, size int) {
current, currentOk := mc.alias[target.Key]

Expand All @@ -285,7 +293,6 @@ func (mc *mapCandidate) insertAlias(target mendoza.Reference, source mendoza.Ref
fieldIdx: source.Index,
sameKey: true,
}
return
}

if currentOk {
Expand All @@ -304,6 +311,7 @@ func (mc *mapCandidate) insertAlias(target mendoza.Reference, source mendoza.Ref
fieldIdx: source.Index,
sameKey: false,
}
return
}

func (mc *mapCandidate) IsMissing(reference mendoza.Reference) bool {
Expand All @@ -315,9 +323,10 @@ func (mc *mapCandidate) RegisterRequest(childIdx int, childRef mendoza.Reference
mc.seenKeys[childRef.Key] = struct{}{}
}

func (d *differ) reconstructMap(idx int, reqs []request) {
func (d *differ) reconstructMap(reporter ExactDiffReporter, idx int, reqs []request) {
// right-index -> list of requests
fieldRequests := [][]request{}
fieldIsSame := []bool{}

// The input here is a list of requests. Each requests has _context_ and _primary_ which looks like this:
//
Expand Down Expand Up @@ -372,7 +381,8 @@ func (d *differ) reconstructMap(idx int, reqs []request) {

for it := d.right.Iter(idx); !it.IsDone(); it.Next() {
fieldEntry := it.GetEntry()
fieldRequests = append(fieldRequests, nil)

isSame := false

for _, otherIdx := range d.hashIndex.Data[fieldEntry.Hash] {
otherEntry := d.left.Entries[otherIdx]
Expand All @@ -381,9 +391,15 @@ func (d *differ) reconstructMap(idx int, reqs []request) {
cand := &candidates[candIdx]
if cand.contextIdx == otherEntry.Parent {
cand.insertAlias(fieldEntry.Reference, otherEntry.Reference, fieldEntry.Size)
if fieldEntry.Reference.Key == otherEntry.Reference.Key {
isSame = true
}
}
}
}

fieldRequests = append(fieldRequests, nil)
fieldIsSame = append(fieldIsSame, isSame)
}

// Now build the requests
Expand Down Expand Up @@ -424,7 +440,18 @@ func (d *differ) reconstructMap(idx int, reqs []request) {
}

for it := d.right.Iter(idx); !it.IsDone(); it.Next() {
d.reconstruct(it.GetIndex(), fieldRequests[it.GetEntry().Reference.Index])
ref := it.GetEntry().Reference
fieldIdx := ref.Index
shouldReport := reporter != nil && !fieldIsSame[fieldIdx]
var childReporter ExactDiffReporter
if shouldReport {
childReporter = reporter
reporter.EnterField(ref.Key)
}
d.reconstruct(childReporter, it.GetIndex(), fieldRequests[fieldIdx])
if shouldReport {
reporter.LeaveField(ref.Key)
}
}

for _, cand := range candidates {
Expand Down Expand Up @@ -530,10 +557,6 @@ func (d *differ) reconstructMap(idx int, reqs []request) {
}
}

type sliceRequestData struct {
candidates map[int]*sliceCandidate
}

type sliceAlias struct {
elementIdx int
prevIsAdjacent bool
Expand Down Expand Up @@ -597,9 +620,10 @@ func (sc *sliceCandidate) insertAlias(target mendoza.Reference, source mendoza.R
}
}

func (d *differ) reconstructSlice(idx int, reqs []request) {
func (d *differ) reconstructSlice(reporter ExactDiffReporter, idx int, reqs []request) {
// right-index -> requests
elementRequests := [][]request{}
elementIsSame := []bool{}

candidates := make([]sliceCandidate, 0, len(reqs))

Expand All @@ -615,7 +639,8 @@ func (d *differ) reconstructSlice(idx int, reqs []request) {

for it := d.right.Iter(idx); !it.IsDone(); it.Next() {
elementEntry := it.GetEntry()
elementRequests = append(elementRequests, nil)

isSame := false

for _, otherIdx := range d.hashIndex.Data[elementEntry.Hash] {
otherEntry := d.left.Entries[otherIdx]
Expand All @@ -624,9 +649,15 @@ func (d *differ) reconstructSlice(idx int, reqs []request) {
cand := &candidates[candIdx]
if cand.contextIdx == otherEntry.Parent {
cand.insertAlias(elementEntry.Reference, otherEntry.Reference, elementEntry.Size)
if elementEntry.Reference.Index == otherEntry.Reference.Index {
isSame = true
}
}
}
}

elementRequests = append(elementRequests, nil)
elementIsSame = append(elementIsSame, isSame)
}

// Now build the requests
Expand Down Expand Up @@ -658,7 +689,17 @@ func (d *differ) reconstructSlice(idx int, reqs []request) {
}

for it := d.right.Iter(idx); !it.IsDone(); it.Next() {
d.reconstruct(it.GetIndex(), elementRequests[it.GetEntry().Reference.Index])
ref := it.GetEntry().Reference
shouldReport := reporter != nil && !elementIsSame[ref.Index]
var childReporter ExactDiffReporter
if shouldReport {
childReporter = reporter
reporter.EnterElement(ref.Index)
}
d.reconstruct(childReporter, it.GetIndex(), elementRequests[ref.Index])
if shouldReport {
reporter.LeaveElement(ref.Index)
}
}

for _, cand := range candidates {
Expand Down
144 changes: 144 additions & 0 deletions exact_diff_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package mendoza_test

import (
"strconv"
"testing"

"github.com/stretchr/testify/require"

"github.com/sanity-io/mendoza"
)

type exactDiffEntry struct {
path []string
val interface{}
}

type exactDiffReporter struct {
entries []exactDiffEntry

currentPath []string
}

func (r *exactDiffReporter) EnterField(key string) {
r.currentPath = append(r.currentPath, key)
}

func (r *exactDiffReporter) LeaveField(key string) {
r.currentPath = r.currentPath[:len(r.currentPath)-1]
}

func (r *exactDiffReporter) EnterElement(idx int) {
r.currentPath = append(r.currentPath, strconv.Itoa(idx))
}

func (r *exactDiffReporter) LeaveElement(_ int) {
r.currentPath = r.currentPath[:len(r.currentPath)-1]
}

func (r *exactDiffReporter) Report(val interface{}) {
entry := exactDiffEntry{
path: make([]string, len(r.currentPath)),
val: val,
}
copy(entry.path, r.currentPath)
r.entries = append(r.entries, entry)
}

func TestExactDiff(t *testing.T) {
type testCase struct {
name string
left interface{}
right interface{}
result []exactDiffEntry
}

for _, tc := range []testCase{
{
name: "float no diff",
left: map[string]interface{}{"a": 1.0, "b": 2.0, "c": 3.0, "d": 4.0},
right: map[string]interface{}{"a": 1.0, "b": 2.0, "c": 3.0, "d": 4.0},
result: []exactDiffEntry{},
},
{
name: "float single field diff",
left: map[string]interface{}{"a": 1.0, "b": 3.0, "c": 3.0, "d": 4.0},
right: map[string]interface{}{"a": 1.0, "b": 2.0, "c": 3.0, "d": 4.0},
result: []exactDiffEntry{
{
path: []string{"b"},
val: 2.0,
},
},
},
{
name: "map changes values",
left: map[string]interface{}{"a": 1.0, "b": 2.0},
right: map[string]interface{}{"a": 1.0, "b": map[string]interface{}{"c": 3.0, "d": 4.0}},
result: []exactDiffEntry{
{
path: []string{"b", "c"},
val: 3.0,
},
{
path: []string{"b", "d"},
val: 4.0,
},
},
},
{
name: "map update",
left: map[string]interface{}{"a": 1.0},
right: map[string]interface{}{"a": 1.0, "b": map[string]interface{}{"c": []interface{}{0.0}}},
result: []exactDiffEntry{
{
path: []string{"b"},
val: map[string]interface{}{"c": []interface{}{0.0}},
},
},
},
{
name: "slice no diff",
left: map[string]interface{}{"a": 1.0, "b": []interface{}{1.0, 2.0}},
right: map[string]interface{}{"a": 1.0, "b": []interface{}{1.0, 2.0}},
result: []exactDiffEntry{},
},
{
name: "slice one element diff",
left: map[string]interface{}{"a": 1.0, "b": []interface{}{1.0, 2.0}},
right: map[string]interface{}{"a": 1.0, "b": []interface{}{2.0, 2.0}},
result: []exactDiffEntry{
{
path: []string{"b", "0"},
val: 2.0,
},
},
},
{
name: "string no diff",
left: map[string]interface{}{"a": 1.0, "b": "hello", "c": 3.0, "d": 4.0},
right: map[string]interface{}{"a": 1.0, "b": "hello", "c": 3.0, "d": 4.0},
result: []exactDiffEntry{},
},
{
name: "string single field diff",
left: map[string]interface{}{"a": 1.0, "b": "hello", "c": 3.0, "d": 4.0},
right: map[string]interface{}{"a": 1.0, "b": "world", "c": 3.0, "d": 4.0},
result: []exactDiffEntry{
{
path: []string{"b"},
val: "world",
},
},
},
} {
tc := tc
t.Run(tc.name, func(t *testing.T) {
reporter := exactDiffReporter{entries: make([]exactDiffEntry, 0)}
opts := mendoza.DefaultOptions.WithExactDiffReporter(&reporter)
_, err := opts.CreatePatch(tc.left, tc.right)
require.NoError(t, err)
require.EqualValues(t, tc.result, reporter.entries)
})
}
}
Loading