Skip to content

Commit 6703e6d

Browse files
committed
typed: Simplify merge and compare to be more focused
Update merge and compare to specifically do what they are supposed to do. This results in some performance improvements and simplifies the code quite significantly.
1 parent 479fba6 commit 6703e6d

File tree

2 files changed

+158
-242
lines changed

2 files changed

+158
-242
lines changed

typed/compare.go

+122-120
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,61 @@ limitations under the License.
1717
package typed
1818

1919
import (
20+
"fmt"
21+
"strings"
22+
2023
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
2124
"sigs.k8s.io/structured-merge-diff/v4/schema"
2225
"sigs.k8s.io/structured-merge-diff/v4/value"
2326
)
2427

28+
// Comparison is the return value of a TypedValue.Compare() operation.
29+
//
30+
// No field will appear in more than one of the three fieldsets. If all of the
31+
// fieldsets are empty, then the objects must have been equal.
32+
type Comparison struct {
33+
// Removed contains any fields removed by rhs (the right-hand-side
34+
// object in the comparison).
35+
Removed *fieldpath.Set
36+
// Modified contains fields present in both objects but different.
37+
Modified *fieldpath.Set
38+
// Added contains any fields added by rhs.
39+
Added *fieldpath.Set
40+
}
41+
42+
// IsSame returns true if the comparison returned no changes (the two
43+
// compared objects are similar).
44+
func (c *Comparison) IsSame() bool {
45+
return c.Removed.Empty() && c.Modified.Empty() && c.Added.Empty()
46+
}
47+
48+
// String returns a human readable version of the comparison.
49+
func (c *Comparison) String() string {
50+
bld := strings.Builder{}
51+
if !c.Modified.Empty() {
52+
bld.WriteString(fmt.Sprintf("- Modified Fields:\n%v\n", c.Modified))
53+
}
54+
if !c.Added.Empty() {
55+
bld.WriteString(fmt.Sprintf("- Added Fields:\n%v\n", c.Added))
56+
}
57+
if !c.Removed.Empty() {
58+
bld.WriteString(fmt.Sprintf("- Removed Fields:\n%v\n", c.Removed))
59+
}
60+
return bld.String()
61+
}
62+
63+
// ExcludeFields fields from the compare recursively removes the fields
64+
// from the entire comparison
65+
func (c *Comparison) ExcludeFields(fields *fieldpath.Set) *Comparison {
66+
if fields == nil || fields.Empty() {
67+
return c
68+
}
69+
c.Removed = c.Removed.RecursiveDifference(fields)
70+
c.Modified = c.Modified.RecursiveDifference(fields)
71+
c.Added = c.Added.RecursiveDifference(fields)
72+
return c
73+
}
74+
2575
type compareWalker struct {
2676
lhs value.Value
2777
rhs value.Value
@@ -31,15 +81,8 @@ type compareWalker struct {
3181
// Current path that we are comparing
3282
path fieldpath.Path
3383

34-
// How to compare. Called after schema validation for all leaf fields.
35-
rule compareRule
36-
37-
// If set, called after non-leaf items have been compared. (`out` is
38-
// probably already set.)
39-
postItemHook compareRule
40-
41-
// output of the merge operation (nil if none)
42-
out *interface{}
84+
// Resulting comparison.
85+
comparison *Comparison
4386

4487
// internal housekeeping--don't set when constructing.
4588
inLeaf bool // Set to true if we're in a "big leaf"--atomic map/list
@@ -50,11 +93,6 @@ type compareWalker struct {
5093
allocator value.Allocator
5194
}
5295

53-
// compareRule examine w.lhs and w.rhs (up to one of which may be nil) and
54-
// optionally set w.out. If lhs and rhs are both set, they will be of
55-
// comparable type.
56-
type compareRule func(w *compareWalker)
57-
5896
// compare compares stuff.
5997
func (w *compareWalker) compare(prefixFn func() string) (errs ValidationErrors) {
6098
if w.lhs == nil && w.rhs == nil {
@@ -81,8 +119,12 @@ func (w *compareWalker) compare(prefixFn func() string) (errs ValidationErrors)
81119
errs = append(errs, handleAtom(arhs, w.typeRef, w)...)
82120
}
83121

84-
if !w.inLeaf && w.postItemHook != nil {
85-
w.postItemHook(w)
122+
if !w.inLeaf {
123+
if w.lhs == nil {
124+
w.comparison.Added.Insert(w.path)
125+
} else if w.rhs == nil {
126+
w.comparison.Removed.Insert(w.path)
127+
}
86128
}
87129
return errs.WithLazyPrefix(prefixFn)
88130
}
@@ -98,7 +140,15 @@ func (w *compareWalker) doLeaf() {
98140
w.inLeaf = true
99141

100142
// We don't recurse into leaf fields for merging.
101-
w.rule(w)
143+
if w.lhs == nil {
144+
w.comparison.Added.Insert(w.path)
145+
} else if w.rhs == nil {
146+
w.comparison.Removed.Insert(w.path)
147+
} else if !value.EqualsUsing(w.allocator, w.rhs, w.lhs) {
148+
// TODO: Equality is not sufficient for this.
149+
// Need to implement equality check on the value type.
150+
w.comparison.Modified.Insert(w.path)
151+
}
102152
}
103153

104154
func (w *compareWalker) doScalar(t *schema.Scalar) ValidationErrors {
@@ -115,7 +165,7 @@ func (w *compareWalker) doScalar(t *schema.Scalar) ValidationErrors {
115165
return nil
116166
}
117167

118-
func (w *compareWalker) prepareDescent(pe fieldpath.PathElement, tr schema.TypeRef) *compareWalker {
168+
func (w *compareWalker) prepareDescent(pe fieldpath.PathElement, tr schema.TypeRef, cmp *Comparison) *compareWalker {
119169
if w.spareWalkers == nil {
120170
// first descent.
121171
w.spareWalkers = &[]*compareWalker{}
@@ -131,7 +181,7 @@ func (w *compareWalker) prepareDescent(pe fieldpath.PathElement, tr schema.TypeR
131181
w2.path = append(w2.path, pe)
132182
w2.lhs = nil
133183
w2.rhs = nil
134-
w2.out = nil
184+
w2.comparison = cmp
135185
return w2
136186
}
137187

@@ -162,102 +212,64 @@ func (w *compareWalker) visitListItems(t *schema.List, lhs, rhs value.List) (err
162212
if lhs != nil {
163213
lLen = lhs.Length()
164214
}
165-
outLen := lLen
166-
if outLen < rLen {
167-
outLen = rLen
168-
}
169-
out := make([]interface{}, 0, outLen)
170-
171-
rhsOrder, observedRHS, rhsErrs := w.indexListPathElements(t, rhs)
172-
errs = append(errs, rhsErrs...)
173-
lhsOrder, observedLHS, lhsErrs := w.indexListPathElements(t, lhs)
174-
errs = append(errs, lhsErrs...)
175215

176-
sharedOrder := make([]*fieldpath.PathElement, 0, rLen)
177-
for i := range rhsOrder {
178-
pe := &rhsOrder[i]
179-
if _, ok := observedLHS.Get(*pe); ok {
180-
sharedOrder = append(sharedOrder, pe)
216+
lValues := fieldpath.MakePathElementValueMap(lLen)
217+
for i := 0; i < lLen; i++ {
218+
child := lhs.At(i)
219+
pe, err := listItemToPathElement(w.allocator, w.schema, t, i, child)
220+
if err != nil {
221+
errs = append(errs, errorf("element %v: %v", i, err.Error())...)
222+
// If we can't construct the path element, we can't
223+
// even report errors deeper in the schema, so bail on
224+
// this element.
225+
continue
226+
}
227+
// Ignore repeated occurences of `pe`.
228+
if _, found := lValues.Get(pe); found {
229+
continue
181230
}
231+
lValues.Insert(pe, child)
182232
}
183233

184-
var nextShared *fieldpath.PathElement
185-
if len(sharedOrder) > 0 {
186-
nextShared = sharedOrder[0]
187-
sharedOrder = sharedOrder[1:]
188-
}
189-
190-
lLen, rLen = len(lhsOrder), len(rhsOrder)
191-
for lI, rI := 0, 0; lI < lLen || rI < rLen; {
192-
if lI < lLen && rI < rLen {
193-
pe := lhsOrder[lI]
194-
if pe.Equals(rhsOrder[rI]) {
195-
// merge LHS & RHS items
196-
lChild, _ := observedLHS.Get(pe)
197-
rChild, _ := observedRHS.Get(pe)
198-
mergeOut, errs := w.mergeListItem(t, pe, lChild, rChild)
199-
errs = append(errs, errs...)
200-
if mergeOut != nil {
201-
out = append(out, *mergeOut)
202-
}
203-
lI++
204-
rI++
205-
206-
nextShared = nil
207-
if len(sharedOrder) > 0 {
208-
nextShared = sharedOrder[0]
209-
sharedOrder = sharedOrder[1:]
210-
}
211-
continue
212-
}
213-
if _, ok := observedRHS.Get(pe); ok && nextShared != nil && !nextShared.Equals(lhsOrder[lI]) {
214-
// shared item, but not the one we want in this round
215-
lI++
216-
continue
217-
}
218-
}
219-
if lI < lLen {
220-
pe := lhsOrder[lI]
221-
if _, ok := observedRHS.Get(pe); !ok {
222-
// take LHS item
223-
lChild, _ := observedLHS.Get(pe)
224-
mergeOut, errs := w.mergeListItem(t, pe, lChild, nil)
225-
errs = append(errs, errs...)
226-
if mergeOut != nil {
227-
out = append(out, *mergeOut)
228-
}
229-
lI++
230-
continue
231-
}
234+
rValues := fieldpath.MakePathElementValueMap(rLen)
235+
for i := 0; i < rLen; i++ {
236+
rValue := rhs.At(i)
237+
pe, err := listItemToPathElement(w.allocator, w.schema, t, i, rValue)
238+
if err != nil {
239+
errs = append(errs, errorf("element %v: %v", i, err.Error())...)
240+
// If we can't construct the path element, we can't
241+
// even report errors deeper in the schema, so bail on
242+
// this element.
243+
continue
232244
}
233-
if rI < rLen {
234-
// Take the RHS item, merge with matching LHS item if possible
235-
pe := rhsOrder[rI]
236-
lChild, _ := observedLHS.Get(pe) // may be nil
237-
rChild, _ := observedRHS.Get(pe)
238-
mergeOut, errs := w.mergeListItem(t, pe, lChild, rChild)
239-
errs = append(errs, errs...)
240-
if mergeOut != nil {
241-
out = append(out, *mergeOut)
242-
}
243-
rI++
244-
// Advance nextShared, if we are merging nextShared.
245-
if nextShared != nil && nextShared.Equals(pe) {
246-
nextShared = nil
247-
if len(sharedOrder) > 0 {
248-
nextShared = sharedOrder[0]
249-
sharedOrder = sharedOrder[1:]
250-
}
251-
}
245+
// Ignore repeated occurences of `pe`.
246+
if _, found := rValues.Get(pe); found {
247+
continue
252248
}
249+
rValues.Insert(pe, rValue)
250+
// We can merge with nil if lValue is not present.
251+
lValue, _ := lValues.Get(pe)
252+
errs = append(errs, w.mergeListItem(t, pe, lValue, rValue)...)
253253
}
254254

255-
if len(out) > 0 {
256-
i := interface{}(out)
257-
w.out = &i
255+
// Add items from left that are not in right.
256+
for i := 0; i < lLen; i++ {
257+
lValue := lhs.At(i)
258+
pe, err := listItemToPathElement(w.allocator, w.schema, t, i, lValue)
259+
if err != nil {
260+
errs = append(errs, errorf("element %v: %v", i, err.Error())...)
261+
// If we can't construct the path element, we can't
262+
// even report errors deeper in the schema, so bail on
263+
// this element.
264+
continue
265+
}
266+
if _, found := rValues.Get(pe); found {
267+
continue
268+
}
269+
errs = append(errs, w.mergeListItem(t, pe, lValue, nil)...)
258270
}
259271

260-
return errs
272+
return
261273
}
262274

263275
func (w *compareWalker) indexListPathElements(t *schema.List, list value.List) ([]fieldpath.PathElement, fieldpath.PathElementValueMap, ValidationErrors) {
@@ -288,16 +300,13 @@ func (w *compareWalker) indexListPathElements(t *schema.List, list value.List) (
288300
return pes, observed, errs
289301
}
290302

291-
func (w *compareWalker) mergeListItem(t *schema.List, pe fieldpath.PathElement, lChild, rChild value.Value) (out *interface{}, errs ValidationErrors) {
292-
w2 := w.prepareDescent(pe, t.ElementType)
303+
func (w *compareWalker) mergeListItem(t *schema.List, pe fieldpath.PathElement, lChild, rChild value.Value) ValidationErrors {
304+
w2 := w.prepareDescent(pe, t.ElementType, w.comparison)
293305
w2.lhs = lChild
294306
w2.rhs = rChild
295-
errs = append(errs, w2.compare(pe.String)...)
296-
if w2.out != nil {
297-
out = w2.out
298-
}
307+
errs := w2.compare(pe.String)
299308
w.finishDescent(w2)
300-
return
309+
return errs
301310
}
302311

303312
func (w *compareWalker) derefList(prefix string, v value.Value) (value.List, ValidationErrors) {
@@ -346,13 +355,10 @@ func (w *compareWalker) visitMapItem(t *schema.Map, out map[string]interface{},
346355
fieldType = sf.Type
347356
}
348357
pe := fieldpath.PathElement{FieldName: &key}
349-
w2 := w.prepareDescent(pe, fieldType)
358+
w2 := w.prepareDescent(pe, fieldType, w.comparison)
350359
w2.lhs = lhs
351360
w2.rhs = rhs
352361
errs = append(errs, w2.compare(pe.String)...)
353-
if w2.out != nil {
354-
out[key] = *w2.out
355-
}
356362
w.finishDescent(w2)
357363
return errs
358364
}
@@ -364,10 +370,6 @@ func (w *compareWalker) visitMapItems(t *schema.Map, lhs, rhs value.Map) (errs V
364370
errs = append(errs, w.visitMapItem(t, out, key, lhsValue, rhsValue)...)
365371
return true
366372
})
367-
if len(out) > 0 {
368-
i := interface{}(out)
369-
w.out = &i
370-
}
371373

372374
return errs
373375
}

0 commit comments

Comments
 (0)