-
Notifications
You must be signed in to change notification settings - Fork 68
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Semantic equals implementation (#1546)
* normalize atlasproject spec * reflect normalize * add error handling and skip byte slices * restructure, add byte slice skipping * add sortable * add sortable and unit tests Co-authored-by: Sergiusz Urbaniak <sergiusz.urbaniak@gmail.com>
- Loading branch information
1 parent
d4f4033
commit 61bf70f
Showing
10 changed files
with
610 additions
and
141 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
100644 d537760138665c24225e4f5725c5bd80775033a6 go.mod | ||
100644 60d2b0501dda29d0d258c6a0f401c95854606d5c go.mod |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
package cmp | ||
|
||
import ( | ||
"cmp" | ||
"fmt" | ||
"math/rand" | ||
"reflect" | ||
"slices" | ||
"sort" | ||
) | ||
|
||
type Normalizer[T any] interface { | ||
Normalize() T | ||
} | ||
|
||
func SemanticEqual[T Normalizer[T]](this, that T) bool { | ||
return reflect.DeepEqual(this.Normalize(), that.Normalize()) | ||
} | ||
|
||
func NormalizeSlice[S ~[]E, E any](slice S, cmp func(a, b E) int) S { | ||
if len(slice) == 0 { | ||
return nil | ||
} | ||
slices.SortFunc(slice, cmp) | ||
return slice | ||
} | ||
|
||
func Normalize(data any) error { | ||
var err error | ||
traverse(data, func(slice reflect.Value) { | ||
sort.Slice(slice.Interface(), func(i, j int) bool { | ||
iIface, jIface := slice.Index(i).Interface(), slice.Index(j).Interface() | ||
|
||
if ok, result := compareSortable(iIface, jIface); ok { | ||
return result < 0 | ||
} | ||
|
||
result, e := ByJSON(iIface, jIface) | ||
if e != nil { | ||
err = fmt.Errorf("error converting slice %v to JSON: %w", slice, e) | ||
return false | ||
} | ||
return result < 0 | ||
}) | ||
}) | ||
return err | ||
} | ||
|
||
func compareSortable(i, j any) (bool, int) { | ||
iSortable, iSortableOK := i.(Sortable) | ||
jSortable, jSortableOK := j.(Sortable) | ||
|
||
if iSortableOK && jSortableOK { | ||
return true, cmp.Compare(iSortable.Key(), jSortable.Key()) | ||
} | ||
|
||
return false, -1 | ||
} | ||
|
||
func PermuteOrder(data any, r *rand.Rand) { | ||
traverse(data, func(slice reflect.Value) { | ||
r.Shuffle(slice.Len(), func(i, j int) { | ||
reflect.Swapper(slice.Interface())(i, j) | ||
}) | ||
}) | ||
} | ||
|
||
func traverse(data any, f func(slice reflect.Value)) { | ||
traverseValue(reflect.ValueOf(data), f) | ||
} | ||
|
||
func traverseValue(value reflect.Value, f func(slice reflect.Value)) { | ||
switch value.Kind() { | ||
case reflect.Pointer: | ||
// if it is a pointer, traverse over its dereferenced value | ||
traverseValue(value.Elem(), f) | ||
|
||
case reflect.Struct: | ||
for i := 0; i < value.NumField(); i++ { | ||
// skip unexported fields | ||
if value.Type().Field(i).PkgPath != "" { | ||
continue | ||
} | ||
// traverse over each field in the struct | ||
traverseValue(value.Field(i), f) | ||
} | ||
|
||
case reflect.Slice: | ||
// omit zero length slices | ||
if value.Len() == 0 { | ||
return | ||
} | ||
// skip []byte slices | ||
if value.Type().Elem().Kind() == reflect.Uint8 { | ||
return | ||
} | ||
// traverse over each element in the slice | ||
for j := 0; j < value.Len(); j++ { | ||
traverseValue(value.Index(j), f) | ||
} | ||
// base case: we can apply the given function | ||
f(value) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
package cmp | ||
|
||
import ( | ||
"reflect" | ||
"testing" | ||
|
||
v1apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" | ||
) | ||
|
||
func TestEndlessRecursion(t *testing.T) { | ||
for _, tc := range []struct { | ||
name string | ||
data any | ||
want any | ||
}{ | ||
{ | ||
name: "pointer", | ||
data: &struct { | ||
Slice []string | ||
}{ | ||
Slice: []string{"C", "B", "A", "sort"}, | ||
}, | ||
want: &struct { | ||
Slice []string | ||
}{ | ||
Slice: []string{"A", "B", "C", "sort"}, | ||
}, | ||
}, | ||
{ | ||
name: "nested JSON", | ||
data: struct { | ||
NestedJSON v1apiextensions.JSON | ||
}{ | ||
NestedJSON: v1apiextensions.JSON{Raw: []byte("CBA")}, | ||
}, | ||
want: struct { | ||
NestedJSON v1apiextensions.JSON | ||
}{ | ||
NestedJSON: v1apiextensions.JSON{Raw: []byte("CBA")}, | ||
}, | ||
}, | ||
{ | ||
name: "ignore byte slices", | ||
data: struct { | ||
ByteSlice []byte | ||
}{ | ||
ByteSlice: []byte("CBA"), | ||
}, | ||
want: struct { | ||
ByteSlice []byte | ||
}{ | ||
ByteSlice: []byte("CBA"), | ||
}, | ||
}, | ||
{ | ||
name: "ignore zero length slices", | ||
data: struct { | ||
EmptySlice []string | ||
}{}, | ||
want: struct { | ||
EmptySlice []string | ||
}{}, | ||
}, | ||
{ | ||
name: "ignore unexported fields", | ||
data: struct { | ||
ExportedFields []string | ||
unExportedField []string | ||
}{ | ||
ExportedFields: []string{"C", "B", "A", "sort"}, | ||
unExportedField: []string{"C", "B", "A", `don't sort'`}, | ||
}, | ||
want: struct { | ||
ExportedFields []string | ||
unExportedField []string | ||
}{ | ||
ExportedFields: []string{"A", "B", "C", "sort"}, | ||
unExportedField: []string{"C", "B", "A", `don't sort'`}, | ||
}, | ||
}, | ||
{ | ||
name: "nested slices", | ||
data: struct { | ||
Nested []struct { | ||
Slice []string | ||
} | ||
}{ | ||
Nested: []struct { | ||
Slice []string | ||
}{ | ||
{Slice: []string{"Z", "Y", "X"}}, | ||
{Slice: []string{"C", "B", "A"}}, | ||
}, | ||
}, | ||
want: struct { | ||
Nested []struct { | ||
Slice []string | ||
} | ||
}{ | ||
Nested: []struct { | ||
Slice []string | ||
}{ | ||
{Slice: []string{"A", "B", "C"}}, | ||
{Slice: []string{"X", "Y", "Z"}}, | ||
}, | ||
}, | ||
}, | ||
} { | ||
t.Run(tc.name, func(t *testing.T) { | ||
err := Normalize(tc.data) | ||
if err != nil { | ||
t.Error(err) | ||
return | ||
} | ||
if !reflect.DeepEqual(tc.data, tc.want) { | ||
t.Errorf("want normalized value %+v, got %v", tc.want, tc.data) | ||
return | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package cmp | ||
|
||
import ( | ||
"cmp" | ||
"encoding/json" | ||
"fmt" | ||
"strings" | ||
) | ||
|
||
type Sortable interface { | ||
Key() string | ||
} | ||
|
||
func PointerKey[T Sortable](in *T) string { | ||
if in == nil { | ||
return "nil" | ||
} | ||
return (*in).Key() | ||
} | ||
|
||
func SliceKey[K Sortable](in []K) string { | ||
result := make([]string, 0, len(in)) | ||
for i := range in { | ||
result = append(result, in[i].Key()) | ||
} | ||
return "[" + strings.Join(result, ",") + "]" | ||
} | ||
|
||
func ByKey[S Sortable](x, y S) int { | ||
return cmp.Compare(x.Key(), y.Key()) | ||
} | ||
|
||
func ByJSON[T any](x, y T) (int, error) { | ||
xJSON, err := marshalJSON(x) | ||
if err != nil { | ||
return -1, fmt.Errorf("error converting %v to JSON: %w", x, err) | ||
} | ||
yJSON, err := marshalJSON(y) | ||
if err != nil { | ||
return -1, fmt.Errorf("error converting %v to JSON: %w", x, err) | ||
} | ||
return cmp.Compare(xJSON, yJSON), nil | ||
} | ||
|
||
func marshalJSON[T any](obj T) (string, error) { | ||
jObj, err := json.Marshal(obj) | ||
if err != nil { | ||
return "", err | ||
} | ||
return string(jObj), nil | ||
} |
Oops, something went wrong.