From eb71ef25c05a2baef5a9ac9af466a34871432f81 Mon Sep 17 00:00:00 2001 From: Bohdan Siryk Date: Fri, 23 Feb 2024 13:06:00 +0200 Subject: [PATCH] dcomprasion structs were implemented --- .../clusters.instaclustr.com_cadences.yaml | 3 +- .../clusters.instaclustr.com_cassandras.yaml | 2 - pkg/utils/dcomparison/map_diff.go | 7 +- pkg/utils/dcomparison/struct_diff.go | 194 ++++++++++++++++++ pkg/utils/dcomparison/struct_diff_test.go | 127 ++++++++++++ 5 files changed, 325 insertions(+), 8 deletions(-) create mode 100644 pkg/utils/dcomparison/struct_diff.go create mode 100644 pkg/utils/dcomparison/struct_diff_test.go diff --git a/config/crd/bases/clusters.instaclustr.com_cadences.yaml b/config/crd/bases/clusters.instaclustr.com_cadences.yaml index a329f66ad..24f8c18ae 100644 --- a/config/crd/bases/clusters.instaclustr.com_cadences.yaml +++ b/config/crd/bases/clusters.instaclustr.com_cadences.yaml @@ -282,7 +282,7 @@ spec: type: object maxItems: 1 type: array - pciComplianceMode: + pciCompliance: type: boolean privateNetwork: type: boolean @@ -393,7 +393,6 @@ spec: type: string required: - dataCentres - - pciComplianceMode - useCadenceWebAuth type: object status: diff --git a/config/crd/bases/clusters.instaclustr.com_cassandras.yaml b/config/crd/bases/clusters.instaclustr.com_cassandras.yaml index e18d85ced..5ef899082 100644 --- a/config/crd/bases/clusters.instaclustr.com_cassandras.yaml +++ b/config/crd/bases/clusters.instaclustr.com_cassandras.yaml @@ -342,8 +342,6 @@ spec: type: array version: type: string - required: - - pciCompliance type: object status: description: CassandraStatus defines the observed state of Cassandra diff --git a/pkg/utils/dcomparison/map_diff.go b/pkg/utils/dcomparison/map_diff.go index 589e6cd91..61cd9c6ad 100644 --- a/pkg/utils/dcomparison/map_diff.go +++ b/pkg/utils/dcomparison/map_diff.go @@ -14,10 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package dcomparison provides a solution for deeply comparing two maps, -// including their nested maps and slices. It is designed to identify differences -// between two maps that can contain a variety of data types, such as strings, -// integers, other maps, and slices. +// Package dcomparison provides a solution for deeply comparing two objects (struct, maps). +// It is designed to identify differences between two objects that may contain a variety of +// data types, such as strings, integers, other maps, and slices. package dcomparison import ( diff --git a/pkg/utils/dcomparison/struct_diff.go b/pkg/utils/dcomparison/struct_diff.go new file mode 100644 index 000000000..cb2f70214 --- /dev/null +++ b/pkg/utils/dcomparison/struct_diff.go @@ -0,0 +1,194 @@ +package dcomparison + +import ( + "fmt" + "reflect" + "strings" +) + +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +func StructDiff[T any](obj1, obj2 T, path string) (ObjectDiffs, error) { + val1 := reflect.ValueOf(obj1) + val2 := reflect.ValueOf(obj2) + + cmp := structComparer{} + + if !cmp.isStruct(val1.Type()) { + return nil, fmt.Errorf("expected struct, got: %s", val1.Kind()) + } + + if val1.Kind() == reflect.Ptr { + val1 = val1.Elem() + val2 = val2.Elem() + } + + cmp.compare(val1, val2, path) + return cmp.diffs, nil +} + +const ( + SkipTag = "dcomprasionSkip" + SkipValue = "true" + jsonTag = "json" +) + +type structComparer struct { + diffs ObjectDiffs +} + +func (s *structComparer) compare(obj1, obj2 reflect.Value, path string) { + // skip looking for differences if subtrees deeply equal to each other + if reflect.DeepEqual(obj1, obj2) { + return + } + + switch obj1.Kind() { + case reflect.Ptr: + s.comparePtrs(obj1, obj2, path) + case reflect.Struct: + s.compareStructs(obj1, obj2, path) + case reflect.Slice, reflect.Array: + s.compareSlicesOrArrays(obj1, obj2, path) + case reflect.Map: + s.compareMaps(obj1, obj2, path) + default: + if !obj1.Equal(obj2) { + s.diffs.Append(ObjectDiff{ + Field: path, + Value1: obj1.Interface(), + Value2: obj2.Interface(), + }) + } + } +} + +func (s *structComparer) isStruct(t reflect.Type) bool { + return t.Kind() == reflect.Struct || t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct +} + +func (s *structComparer) comparePtrs(obj1, obj2 reflect.Value, subpath string) { + switch { + case obj1.IsValid() && obj2.IsValid(): + s.compare(obj1.Elem(), obj2.Elem(), subpath) + + case obj1.IsZero() && obj2.IsZero(): + return + + default: + s.diffs.Append(ObjectDiff{ + Field: subpath, + Value1: obj1.Interface(), + Value2: obj2.Interface(), + }) + } +} + +func (s *structComparer) compareStructs(obj1, obj2 reflect.Value, path string) { + n := obj1.NumField() + for i := 0; i < n; i++ { + field1 := obj1.Type().Field(i) + + if s.shouldSkip(field1) { + continue + } + + subPath := path + "." + s.getFieldName(field1) + + s.compare(obj1.Field(i), obj2.Field(i), subPath) + } +} + +// shouldSkip indicates should the field be skipped during comparing. +// It is skipped only when: +// 1. field is not exported +// 2. field doesn't have a SkipTag +// 3. value of the SkipTag doesn't equal to SkipValue +func (s *structComparer) shouldSkip(field reflect.StructField) bool { + if !field.IsExported() { + return true + } + + val, has := field.Tag.Lookup(SkipTag) + return has && val == SkipValue +} + +func (s *structComparer) getJsonFieldName(tag reflect.StructTag) string { + val, has := tag.Lookup(jsonTag) + if !has { + return "" + } + + return strings.Split(val, ",")[0] +} + +func (s *structComparer) getFieldName(field reflect.StructField) string { + fieldName := s.getJsonFieldName(field.Tag) + if fieldName == "" { + // If there is no json tag use the name of field directly + fieldName = field.Name + } + + return fieldName +} + +func (s *structComparer) compareSlicesOrArrays(slice1, slice2 reflect.Value, path string) { + maxLen := max(slice1.Len(), slice2.Len()) + for i := 0; i < maxLen; i++ { + val1, val2 := s.getSliceElement(slice1, i), s.getSliceElement(slice2, i) + subPath := fmt.Sprintf("%s[%d]", path, i) + s.compare(val1, val2, subPath) + } +} + +func (s *structComparer) getSliceElement(slice reflect.Value, i int) reflect.Value { + if slice.Len() < i { + return slice.Index(i) + } + + return reflect.Zero(slice.Elem().Type()) +} + +func (s *structComparer) compareMaps(map1, map2 reflect.Value, path string) { + for _, key := range map1.MapKeys() { + val1 := map1.MapIndex(key) + val2 := map2.MapIndex(key) + + subPath := fmt.Sprintf("%s[%v]", path, key.Interface()) + + if val2.IsValid() { + s.compare(val1, val2, subPath) + } else { + s.diffs.Append(ObjectDiff{ + Field: subPath, + Value1: val1.Interface(), + Value2: nil, + }) + } + } + + for _, key := range map2.MapKeys() { + subPath := fmt.Sprintf("%s[%v]", path, key.Interface()) + if !map1.MapIndex(key).IsValid() { + s.diffs.Append(ObjectDiff{ + Field: subPath, + Value1: nil, + Value2: map2.MapIndex(key).Interface(), + }) + } + } +} diff --git a/pkg/utils/dcomparison/struct_diff_test.go b/pkg/utils/dcomparison/struct_diff_test.go new file mode 100644 index 000000000..74c4994f7 --- /dev/null +++ b/pkg/utils/dcomparison/struct_diff_test.go @@ -0,0 +1,127 @@ +package dcomparison + +import ( + "reflect" + "testing" +) + +type exportedIntFieldWithSkipTrue struct { + Exported int `dcomprasionSkip:"true"` +} + +type exportedIntFieldWithSkipFalse struct { + Exported int `dcomprasionSkip:"false"` +} + +type exportedStringFieldWithJsonTag struct { + ExportedString string `json:"exportedString"` +} + +func TestStructDiff(t *testing.T) { + type args struct { + obj1 any + obj2 any + } + type testCase struct { + name string + args args + want ObjectDiffs + wantErr bool + } + + tests := []testCase{ + { + name: "not struct type", + args: args{1, 1}, + wantErr: true, + }, + { + name: "struct with exported int field, same values", + args: args{ + obj1: struct { + Exported int + }{1}, + obj2: struct { + Exported int + }{1}, + }, + want: nil, + }, + { + name: "struct with exported int field, different values", + args: args{ + obj1: struct { + Exported int + }{1}, + obj2: struct { + Exported int + }{2}, + }, + want: ObjectDiffs{ + {"spec.Exported", 1, 2}, + }, + }, + { + name: "struct with exported int field, same values, skip", + args: args{ + obj1: exportedIntFieldWithSkipTrue{1}, + obj2: exportedIntFieldWithSkipTrue{1}, + }, + }, + { + name: "struct with exported int field, different values, skip", + args: args{ + obj1: exportedIntFieldWithSkipTrue{1}, + obj2: exportedIntFieldWithSkipTrue{2}, + }, + }, + { + name: "struct with exported int field, same values, has skip tag but do not skip", + args: args{ + obj1: exportedIntFieldWithSkipFalse{1}, + obj2: exportedIntFieldWithSkipFalse{1}, + }, + }, + { + name: "struct with exported int field, different values, has skip tag but do not skip", + args: args{ + obj1: exportedIntFieldWithSkipFalse{2}, + obj2: exportedIntFieldWithSkipFalse{1}, + }, + want: ObjectDiffs{ + {"spec.Exported", 2, 1}, + }, + }, + { + name: "exported string field with json tag, same values, do not skip", + args: args{ + obj1: exportedStringFieldWithJsonTag{"test"}, + obj2: exportedStringFieldWithJsonTag{"test"}, + }, + }, + { + name: "exported string field with json tag, diff values, do not skip", + args: args{ + obj1: exportedStringFieldWithJsonTag{"test1"}, + obj2: exportedStringFieldWithJsonTag{"test2"}, + }, + want: ObjectDiffs{ + {"spec.exportedString", "test1", "test2"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + const rootPath = "spec" + got, err := StructDiff(tt.args.obj1, tt.args.obj2, rootPath) + if (err != nil) != tt.wantErr { + t.Errorf("StructDiff() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("StructDiff() got = %v, want %v", got, tt.want) + } + }) + } +}