Skip to content

Commit 2f558af

Browse files
authored
internal: add structdiff.IsEqual() (#4203)
## Changes New function structdiff.IsEqual() which follows the same logic as structdiff.GetStructDiff() but does not build a diff. ## Why Need this in #4201 reflect.DeepEqual() does not work for types with ForceSendFields because ForceSendFields can have more or less fields in it without changing the actual value. ## Tests Unit tests.
1 parent c5505fe commit 2f558af

File tree

2 files changed

+167
-0
lines changed

2 files changed

+167
-0
lines changed

libs/structs/structdiff/diff_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,12 @@ func TestGetStructDiff(t *testing.T) {
434434
assert.NoError(t, err)
435435
assert.Nil(t, got)
436436
})
437+
438+
t.Run(tt.name+" IsEqual", func(t *testing.T) {
439+
equal := IsEqual(tt.a, tt.b)
440+
expected := len(tt.want) == 0 && !tt.wantErr
441+
assert.Equal(t, expected, equal)
442+
})
437443
}
438444
}
439445

libs/structs/structdiff/equal.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package structdiff
2+
3+
import (
4+
"reflect"
5+
"slices"
6+
7+
"github.com/databricks/cli/libs/structs/structtag"
8+
)
9+
10+
// IsEqual compares two Go structs and returns true if they are equal.
11+
// It uses the same comparison logic as GetStructDiff but is more efficient
12+
// as it short-circuits on the first difference found.
13+
// Respects ForceSendFields if present.
14+
// Types of a and b must match exactly, otherwise returns false.
15+
// Note, reflect.DeepEqual() does not work for SDK structs, because ForceSendFields can contain different sets for the same value.
16+
func IsEqual(a, b any) bool {
17+
v1 := reflect.ValueOf(a)
18+
v2 := reflect.ValueOf(b)
19+
20+
if !v1.IsValid() && !v2.IsValid() {
21+
return true
22+
}
23+
24+
if !v1.IsValid() || !v2.IsValid() {
25+
return false
26+
}
27+
28+
if v1.Type() != v2.Type() {
29+
return false
30+
}
31+
32+
return equalValues(v1, v2)
33+
}
34+
35+
// equalValues returns true if v1 and v2 are equal.
36+
func equalValues(v1, v2 reflect.Value) bool {
37+
if !v1.IsValid() {
38+
return !v2.IsValid()
39+
} else if !v2.IsValid() {
40+
return false
41+
}
42+
43+
v1Type := v1.Type()
44+
45+
if v1Type != v2.Type() {
46+
return false
47+
}
48+
49+
kind := v1.Kind()
50+
51+
// Perform nil checks for nilable types.
52+
switch kind {
53+
case reflect.Pointer, reflect.Map, reflect.Slice, reflect.Interface, reflect.Chan, reflect.Func:
54+
v1Nil := v1.IsNil()
55+
v2Nil := v2.IsNil()
56+
if v1Nil && v2Nil {
57+
return true
58+
}
59+
if v1Nil || v2Nil {
60+
return false
61+
}
62+
default:
63+
// Not a nilable type.
64+
// Proceed with direct comparison below.
65+
}
66+
67+
switch kind {
68+
case reflect.Pointer:
69+
return equalValues(v1.Elem(), v2.Elem())
70+
case reflect.Struct:
71+
return equalStruct(v1, v2)
72+
case reflect.Slice, reflect.Array:
73+
if v1.Len() != v2.Len() {
74+
return false
75+
}
76+
for i := range v1.Len() {
77+
if !equalValues(v1.Index(i), v2.Index(i)) {
78+
return false
79+
}
80+
}
81+
case reflect.Map:
82+
if v1Type.Key().Kind() == reflect.String {
83+
return equalMapStringKey(v1, v2)
84+
}
85+
return reflect.DeepEqual(v1.Interface(), v2.Interface())
86+
default:
87+
return reflect.DeepEqual(v1.Interface(), v2.Interface())
88+
}
89+
return true
90+
}
91+
92+
func equalStruct(s1, s2 reflect.Value) bool {
93+
t := s1.Type()
94+
forced1 := getForceSendFields(s1)
95+
forced2 := getForceSendFields(s2)
96+
97+
for i := range t.NumField() {
98+
sf := t.Field(i)
99+
if !sf.IsExported() || sf.Name == "ForceSendFields" {
100+
continue
101+
}
102+
103+
// Continue traversing embedded structs.
104+
if sf.Anonymous {
105+
if !equalValues(s1.Field(i), s2.Field(i)) {
106+
return false
107+
}
108+
continue
109+
}
110+
111+
jsonTag := structtag.JSONTag(sf.Tag.Get("json"))
112+
113+
v1Field := s1.Field(i)
114+
v2Field := s2.Field(i)
115+
116+
zero1 := v1Field.IsZero()
117+
zero2 := v2Field.IsZero()
118+
119+
if zero1 || zero2 {
120+
if jsonTag.OmitEmpty() {
121+
if zero1 {
122+
if !slices.Contains(forced1, sf.Name) {
123+
v1Field = reflect.ValueOf(nil)
124+
}
125+
}
126+
if zero2 {
127+
if !slices.Contains(forced2, sf.Name) {
128+
v2Field = reflect.ValueOf(nil)
129+
}
130+
}
131+
}
132+
}
133+
134+
if !equalValues(v1Field, v2Field) {
135+
return false
136+
}
137+
}
138+
return true
139+
}
140+
141+
func equalMapStringKey(m1, m2 reflect.Value) bool {
142+
keySet := map[string]reflect.Value{}
143+
for _, k := range m1.MapKeys() {
144+
// Key is always string at this point
145+
ks := k.Interface().(string)
146+
keySet[ks] = k
147+
}
148+
for _, k := range m2.MapKeys() {
149+
ks := k.Interface().(string)
150+
keySet[ks] = k
151+
}
152+
153+
for _, k := range keySet {
154+
v1 := m1.MapIndex(k)
155+
v2 := m2.MapIndex(k)
156+
if !equalValues(v1, v2) {
157+
return false
158+
}
159+
}
160+
return true
161+
}

0 commit comments

Comments
 (0)