Skip to content

Commit 9314627

Browse files
authored
Enhance resource comparison for collections and blobs (#475)
This update enhances the logic for comparing resources in code generation, especially for collections (maps and arrays) and blob types. The previous method used nil checks, but they weren't helpful for these types. The revised code introduces a new function, `fastCompareTypes`, to generate Go code that quickly compares two objects of the same type. For collections, it uses the built-in `len` function to swiftly spot length differences before diving into detailed checks. For blobs, the code now employs the `bytes.Equal` function for straightforward byte array comparisons. This enhancement aims to make the generated comparison logic more direct, readable, and efficient, especially when dealing with collections and blobs in AWS SDK shapes. Signed-off-by: Amine Hilaly <hilalyamine@gmail.com> By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent c1e9779 commit 9314627

File tree

2 files changed

+238
-67
lines changed

2 files changed

+238
-67
lines changed

pkg/generate/code/compare.go

Lines changed: 177 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -163,29 +163,22 @@ func CompareResource(
163163
memberShapeRef := specField.ShapeRef
164164
memberShape := memberShapeRef.Shape
165165

166-
// if ackcompare.HasNilDifference(a.ko.Spec.Name, b.ko.Spec.Name) {
167-
// delta.Add("Spec.Name", a.ko.Spec.Name, b.ko.Spec.Name)
168-
// }
169-
nilCode := compareNil(
166+
// Use len, bytes.Equal and HasNilDifference to fast compare types, and
167+
// try to avoid deep comparison as much as possible.
168+
fastComparisonOutput, needToCloseBlock := fastCompareTypes(
170169
compareConfig,
171170
memberShape,
172171
deltaVarName,
172+
fieldPath,
173173
firstResAdaptedVarName,
174174
secondResAdaptedVarName,
175-
fieldPath,
176175
indentLevel,
177176
)
178-
179-
if nilCode != "" {
180-
// else if a.ko.Spec.Name != nil && b.ko.Spec.Name != nil {
181-
out += fmt.Sprintf(
182-
"%s else if %s != nil && %s != nil {\n",
183-
nilCode, firstResAdaptedVarName, secondResAdaptedVarName,
184-
)
185-
indentLevel++
186-
}
177+
out += fastComparisonOutput
187178

188179
switch memberShape.Type {
180+
case "blob":
181+
// We already handled the case of blobs above, so we can skip it here.
189182
case "structure":
190183
// Recurse through all the struct's fields and subfields, building
191184
// nested conditionals and calls to `delta.Add()`...
@@ -197,7 +190,7 @@ func CompareResource(
197190
firstResAdaptedVarName,
198191
secondResAdaptedVarName,
199192
fieldPath,
200-
indentLevel,
193+
indentLevel+1,
201194
)
202195
case "list":
203196
// Returns Go code that compares all the elements of the slice fields...
@@ -209,7 +202,7 @@ func CompareResource(
209202
firstResAdaptedVarName,
210203
secondResAdaptedVarName,
211204
fieldPath,
212-
indentLevel,
205+
indentLevel+1,
213206
)
214207
case "map":
215208
// Returns Go code that compares all the elements of the map fields...
@@ -221,7 +214,7 @@ func CompareResource(
221214
firstResAdaptedVarName,
222215
secondResAdaptedVarName,
223216
fieldPath,
224-
indentLevel,
217+
indentLevel+1,
225218
)
226219
default:
227220
// if *a.ko.Spec.Name != *b.ko.Spec.Name) {
@@ -234,15 +227,14 @@ func CompareResource(
234227
firstResAdaptedVarName,
235228
secondResAdaptedVarName,
236229
fieldPath,
237-
indentLevel,
230+
indentLevel+1,
238231
)
239232
}
240-
if nilCode != "" {
233+
if needToCloseBlock {
241234
// }
242235
out += fmt.Sprintf(
243236
"%s}\n", indent,
244237
)
245-
indentLevel--
246238
}
247239
}
248240
return out
@@ -285,12 +277,8 @@ func compareNil(
285277
indent := strings.Repeat("\t", indentLevel)
286278

287279
switch shape.Type {
288-
case "list", "blob":
289-
// for slice types, there is no nilability test. Instead, the normal
290-
// value test checks length of slices.
291-
return ""
292280
case "boolean", "string", "character", "byte", "short", "integer", "long",
293-
"float", "double", "timestamp", "structure", "map", "jsonvalue":
281+
"float", "double", "timestamp", "structure", "jsonvalue":
294282
// if ackcompare.HasNilDifference(a.ko.Spec.Name, b.ko.Spec.Name) {
295283
out += fmt.Sprintf(
296284
"%sif ackcompare.HasNilDifference(%s, %s) {\n",
@@ -355,11 +343,6 @@ func compareScalar(
355343
"%sif *%s != *%s {\n",
356344
indent, firstResVarName, secondResVarName,
357345
)
358-
case "blob":
359-
out += fmt.Sprintf(
360-
"%sif !bytes.Equal(%s, %s) {\n",
361-
indent, firstResVarName, secondResVarName,
362-
)
363346
case "timestamp":
364347
// if !a.ko.Spec.CreatedAt.Equal(b.ko.Spec.CreatedAt) {
365348
out += fmt.Sprintf(
@@ -650,29 +633,20 @@ func CompareStruct(
650633
continue
651634
}
652635

653-
// if ackcompare.HasNilDifference(a.ko.Spec.Name, b.ko.Spec.Name == nil) {
654-
// delta.Add("Spec.Name", a.ko.Spec.Name, b.ko.Spec.Name)
655-
// }
656-
nilCode := compareNil(
636+
fastComparisonOutput, needToCloseBlock := fastCompareTypes(
657637
compareConfig,
658638
memberShape,
659639
deltaVarName,
640+
memberFieldPath,
660641
firstResAdaptedVarName,
661642
secondResAdaptedVarName,
662-
memberFieldPath,
663643
indentLevel,
664644
)
665-
666-
if nilCode != "" {
667-
// else if a.ko.Spec.Name != nil && b.ko.Spec.Name != nil {
668-
out += fmt.Sprintf(
669-
"%s else if %s != nil && %s != nil {\n",
670-
nilCode, firstResAdaptedVarName, secondResAdaptedVarName,
671-
)
672-
indentLevel++
673-
}
645+
out += fastComparisonOutput
674646

675647
switch memberShape.Type {
648+
case "blob":
649+
// We already handled the case of blobs above, so we can skip it here.
676650
case "structure":
677651
// Recurse through all the struct's fields and subfields, building
678652
// nested conditionals and calls to `delta.Add()`...
@@ -684,7 +658,7 @@ func CompareStruct(
684658
firstResAdaptedVarName,
685659
secondResAdaptedVarName,
686660
memberFieldPath,
687-
indentLevel,
661+
indentLevel+1,
688662
)
689663
case "list":
690664
// Returns Go code that compares all the elements of the slice fields...
@@ -696,7 +670,7 @@ func CompareStruct(
696670
firstResAdaptedVarName,
697671
secondResAdaptedVarName,
698672
memberFieldPath,
699-
indentLevel,
673+
indentLevel+1,
700674
)
701675
case "map":
702676
// Returns Go code that compares all the elements of the map fields...
@@ -708,7 +682,7 @@ func CompareStruct(
708682
firstResAdaptedVarName,
709683
secondResAdaptedVarName,
710684
memberFieldPath,
711-
indentLevel,
685+
indentLevel+1,
712686
)
713687
default:
714688
// if *a.ko.Spec.Name != *b.ko.Spec.Name {
@@ -721,16 +695,169 @@ func CompareStruct(
721695
firstResAdaptedVarName,
722696
secondResAdaptedVarName,
723697
memberFieldPath,
724-
indentLevel,
698+
indentLevel+1,
725699
)
726700
}
727-
if nilCode != "" {
701+
if needToCloseBlock {
728702
// }
729703
out += fmt.Sprintf(
730704
"%s}\n", indent,
731705
)
732-
indentLevel--
733706
}
734707
}
735708
return out
736709
}
710+
711+
// fastCompareTypes outputs Go code that fast-compares two objects of the same
712+
// type, by leveraging nil check comparison and length checks. This is used
713+
// when we want to quickly determine if two objects are different, but don't
714+
// need to know the specific differences. For example, when determining if a
715+
// that an array of structs has changed, we don't need to know which structs
716+
// have changed, if the size of the array has changed.
717+
//
718+
// Generally, we can distinguish between the following cases:
719+
// 1. Both objects are structures or scalars type pointers.
720+
// 2. Both objects are collections (slices or maps).
721+
// 3. Both objects are blobs.
722+
//
723+
// In the case of 1, we can use the HasNilDifference function to
724+
// eliminate early on the case where one of the objects is nil and other
725+
// is not.
726+
//
727+
// In the case of 2, we can use the built-in len function to eliminate
728+
// early on the case where the collections have different lengths.
729+
// The trick here is that len(nil) is 0, so we don't need to check for
730+
// nils explicitly.
731+
//
732+
// In the case of 3, we can use the bytes.Equal function to compare the
733+
// byte arrays. bytes.Equal works well with nil arrays too.
734+
// Output code will look something like this:
735+
//
736+
// if ackcompare.HasNilDifference(a.ko.Spec.Name, b.ko.Spec.Name) {
737+
// delta.Add("Spec.Name", a.ko.Spec.Name, b.ko.Spec.Name)
738+
// } else if a.ko.Spec.Name != nil && b.ko.Spec.Name != nil {
739+
// if *a.ko.Spec.Name != *b.ko.Spec.Name) {
740+
// delta.Add("Spec.Name", a.ko.Spec.Name, b.ko.Spec.Name)
741+
// }
742+
// }
743+
//
744+
// if len(a.ko.Spec.Tags) != len(b.ko.Spec.Tags) {
745+
// delta.Add("Spec.Tags", a.ko.Spec.Tags, b.ko.Spec.Tags)
746+
// } else if len(a.ko.Spec.Tags) > 0 {
747+
// if !ackcompare.SliceStringPEqual(a.ko.Spec.Tags, b.ko.Spec.Tags) {
748+
// delta.Add("Spec.Tags", a.ko.Spec.Tags, b.ko.Spec.Tags)
749+
// }
750+
// }
751+
func fastCompareTypes(
752+
// struct informing code generator how to compare the field values.
753+
compareConfig *ackgenconfig.CompareFieldConfig,
754+
// AWSSDK Shape describing the type of the field being compared.
755+
memberShape *awssdkmodel.Shape,
756+
// String representing the name of the variable that is of type
757+
// `*ackcompare.Delta`. We will generate Go code that calls the `Add()`
758+
// method of this variable when differences between fields are detected.
759+
deltaVarName string,
760+
// String representing the json path of the field being compared, e.g.
761+
// "Spec.Name".
762+
fieldPath string,
763+
// String representing the name of the variable that represents the first
764+
// CR under comparison. This will typically be something like "a.ko". See
765+
// `templates/pkg/resource/delta.go.tpl`.
766+
firstResVarName string,
767+
// String representing the name of the variable that represents the second
768+
// CR under comparison. This will typically be something like "b.ko". See
769+
// `templates/pkg/resource/delta.go.tpl`.
770+
secondResVarName string,
771+
// Number of levels of indentation to use
772+
indentLevel int,
773+
) (string, bool) {
774+
out := ""
775+
needToCloseBlock := false
776+
indent := strings.Repeat("\t", indentLevel)
777+
778+
switch memberShape.Type {
779+
case "list", "map":
780+
// Note once we eliminate the case of non equal sizes, we can
781+
// safely assume that the objects have the same length and we can
782+
// we can only iterate over them if they have more than 0 elements.
783+
//
784+
// if len(a.ko.Spec.Tags) != len(b.ko.Spec.Tags) {
785+
// delta.Add("Spec.Tags", a.ko.Spec.Tags, b.ko.Spec.Tags)
786+
// } else len(a.ko.Spec.Tags) > 0 {
787+
// ...
788+
// }
789+
out += fmt.Sprintf(
790+
"%sif len(%s) != len(%s) {\n",
791+
indent,
792+
firstResVarName,
793+
secondResVarName,
794+
)
795+
out += fmt.Sprintf(
796+
"%s\t%s.Add(\"%s\", %s, %s)\n",
797+
indent,
798+
deltaVarName,
799+
fieldPath,
800+
firstResVarName,
801+
secondResVarName,
802+
)
803+
// For sure we are inside the else block of the nil/size check, so we need
804+
// to increase the indentation level.
805+
out += fmt.Sprintf(
806+
"%s} else if len(%s) > 0 {\n",
807+
indent,
808+
firstResVarName,
809+
)
810+
// For sure we are inside the else block of the nil/size check, so we need
811+
// to increase the indentation level and ask the caller to close the block.
812+
needToCloseBlock = true
813+
case "blob":
814+
// Blob is a special case because we need to compare the byte arrays
815+
// using the bytes.Equal function.
816+
//
817+
// if !bytes.Equal(a.ko.Spec.Certificate, b.ko.Spec.Certificate) {
818+
// delta.Add("Spec.Certificate", a.ko.Spec.Certificate, b.ko.Spec.Certificate)
819+
// }
820+
out += fmt.Sprintf(
821+
"%sif !bytes.Equal(%s, %s) {\n",
822+
indent,
823+
firstResVarName,
824+
secondResVarName,
825+
)
826+
out += fmt.Sprintf(
827+
"%s\t%s.Add(\"%s\", %s, %s)\n",
828+
indent,
829+
deltaVarName,
830+
fieldPath,
831+
firstResVarName,
832+
secondResVarName,
833+
)
834+
out += fmt.Sprintf(
835+
"%s}\n", indent,
836+
)
837+
default:
838+
// For any other type, we can use the HasNilDifference function to
839+
// eliminate early on the case where one of the objects is nil and
840+
// other is not.
841+
out += compareNil(
842+
compareConfig,
843+
memberShape,
844+
deltaVarName,
845+
firstResVarName,
846+
secondResVarName,
847+
fieldPath,
848+
indentLevel,
849+
)
850+
851+
// if ackcompare.HasNilDifference(a.ko.Spec.Name, b.ko.Spec.Name) {
852+
// delta.Add("Spec.Name", a.ko.Spec.Name, b.ko.Spec.Name)
853+
// } else if a.ko.Spec.Name != nil && b.ko.Spec.Name != nil {
854+
out += fmt.Sprintf(
855+
" else if %s != nil && %s != nil {\n",
856+
firstResVarName, secondResVarName,
857+
)
858+
// For sure we are inside the else block of the nil/size check, so we need
859+
// to increase the indentation level and ask the caller to close the block.
860+
needToCloseBlock = true
861+
}
862+
return out, needToCloseBlock
863+
}

0 commit comments

Comments
 (0)