11package unit
22
33import (
4+ "encoding/json"
45 "reflect"
56 "slices"
67 "strconv"
78 "strings"
89 "testing"
910
10- "github.com/stretchr/testify/assert"
1111 "github.com/stretchr/testify/require"
1212)
1313
@@ -17,127 +17,106 @@ import (
1717// This is primarily used to ensure that the GetCreateOptions() and GetUpdateOptions()
1818// functions are implemented correctly.
1919func assertJSONObjectsSimilar [TA , TB any ](t testing.TB , a TA , b TB ) {
20- assertJSONObjectsSimilarInner (t , []string {}, a , b )
20+ // Encoding and decoding JSON here is hacky, but it
21+ // lets us avoid some ugly type reflection pointer logic
22+ aJSON , err := json .Marshal (a )
23+ require .NoError (t , err )
24+
25+ bJSON , err := json .Marshal (b )
26+ require .NoError (t , err )
27+
28+ var aParsed , bParsed map [string ]any
29+
30+ require .NoError (t , json .Unmarshal (aJSON , & aParsed ))
31+ require .NoError (t , json .Unmarshal (bJSON , & bParsed ))
32+
33+ assertJSONObjectsSimilarInner (t , []string {}, aParsed , bParsed )
2134}
2235
23- func assertJSONObjectsSimilarInner [TA , TB any ](t testing.TB , path []string , a TA , b TB ) {
24- aValue := derefValueRecursive (reflect .ValueOf (a ))
25- bValue := derefValueRecursive (reflect .ValueOf (b ))
36+ func assertJSONObjectsSimilarInner (t testing.TB , path []string , a , b any ) {
37+ a = normalizeEmptyValues (a )
38+ b = normalizeEmptyValues (b )
39+
40+ aValue := reflect .ValueOf (a )
41+ bValue := reflect .ValueOf (b )
2642
27- aFields := aggregateJSONFields ( reflect . ValueOf ( a ) )
28- bFields := aggregateJSONFields ( reflect . ValueOf ( b ) )
43+ aKind := aValue . Kind ( )
44+ bKind := bValue . Kind ( )
2945
3046 require .Equalf (
3147 t ,
32- aValue . Kind () ,
33- bValue . Kind () ,
34- "%s kind mismatch: %s != %s" ,
35- path ,
36- aValue . Kind () ,
37- bValue . Kind () ,
48+ aKind ,
49+ bKind ,
50+ "%s type mismatch: %s != %s" ,
51+ strings . Join ( path , "." ) ,
52+ aKind ,
53+ bKind ,
3854 )
3955
4056 switch aValue .Kind () {
57+ case reflect .Map :
58+ for _ , key := range aValue .MapKeys () {
59+ aFieldValue := aValue .MapIndex (key )
60+ bFieldValue := bValue .MapIndex (key )
61+
62+ if ! bFieldValue .IsValid () {
63+ // This key is not shared so we can ignore it
64+ continue
65+ }
66+
67+ assertJSONObjectsSimilarInner (
68+ t ,
69+ slices .Concat (path , []string {key .String ()}),
70+ aFieldValue .Interface (),
71+ bFieldValue .Interface (),
72+ )
73+ }
4174 case reflect .Slice :
42- assert .Equalf (
75+ require .Equalf (
4376 t ,
4477 aValue .Len (),
4578 bValue .Len (),
4679 "%s slice length mismatch: %d != %d" ,
47- path ,
80+ strings . Join ( path , "." ) ,
4881 aValue .Len (),
4982 bValue .Len (),
5083 )
5184
5285 for index := range aValue .Len () {
53- assertJSONObjectsSimilarInner (
54- t ,
55- slices .Concat (path , []string {strconv .Itoa (index )}),
56- aValue .Index (index ),
57- bValue .Index (index ),
58- )
59- }
60- case reflect .Map :
61- aKeys := aValue .MapKeys ()
62- bKeys := bValue .MapKeys ()
63-
64- assert .Equalf (
65- t ,
66- aKeys ,
67- bKeys ,
68- "%s map keys mismatch: %v != %v" ,
69- path ,
70- aKeys ,
71- bKeys ,
72- )
86+ aFieldValue := aValue .Index (index )
87+ bFieldValue := bValue .Index (index )
7388
74- for _ , key := range aKeys {
7589 assertJSONObjectsSimilarInner (
7690 t ,
77- slices .Concat (path , []string {key . String ( )}),
78- aValue . MapIndex ( key ),
79- bValue . MapIndex ( key ),
91+ slices .Concat (path , []string {strconv . Itoa ( index )}),
92+ aFieldValue . Interface ( ),
93+ bFieldValue . Interface ( ),
8094 )
8195 }
82- case reflect .Struct :
83- for key , aFieldValue := range aFields {
84- bFieldValue , ok := bFields [key ]
85- if ! ok {
86- // This key isn't shared, nothing to do here
87- continue
88- }
89-
90- assertJSONObjectsSimilarInner (t , slices .Concat (path , []string {key }), aFieldValue , bFieldValue )
91- }
9296 default :
93- assert .Equal (
97+ require .Equal (
9498 t ,
95- aValue . Interface () ,
96- bValue . Interface () ,
97- "%s value mismatch: %s != %s " ,
98- path ,
99- aValue . Interface () ,
100- bValue . Interface () ,
99+ a ,
100+ b ,
101+ "%s value mismatch: %v != %v " ,
102+ strings . Join ( path , "." ) ,
103+ a ,
104+ b ,
101105 )
102106 }
103107}
104108
105- func aggregateJSONFields (v reflect.Value ) map [string ]reflect.Value {
106- vType := derefTypeRecursive (v .Type ())
107-
108- result := make (map [string ]reflect.Value , vType .NumField ())
109-
110- for fieldNum := range vType .NumField () {
111- field := vType .Field (fieldNum )
112-
113- jsonTag , jsonTagOk := field .Tag .Lookup ("json" )
114- if ! jsonTagOk {
115- // No JSON tag is defined, nothing to do here
116- continue
117- }
118-
119- if jsonTag == "-" {
120- continue
121- }
122-
123- jsonTagKey := strings .Split (jsonTag , "," )[0 ]
124- result [jsonTagKey ] = derefValueRecursive (derefValueRecursive (v ).FieldByName (field .Name ))
125- }
126-
127- return result
128- }
129-
130- func derefTypeRecursive (v reflect.Type ) reflect.Type {
131- if v .Kind () == reflect .Ptr {
132- return derefTypeRecursive (v .Elem ())
133- }
134-
135- return v
136- }
137-
138- func derefValueRecursive (v reflect.Value ) reflect.Value {
139- if v .Kind () == reflect .Ptr {
140- return derefValueRecursive (v .Elem ())
109+ // normalizeEmptyValues normalizes the given value for use in JSON object diffing,
110+ // primarily replacing any map, slice, or array values with nil.
111+ //
112+ // This is necessary because an empty length-having type is functionally equivalent
113+ // to a nil value when using GetCreateOptions(...) and GetUpdateOptions(...).
114+ func normalizeEmptyValues (v any ) any {
115+ vValue := reflect .ValueOf (v )
116+ vKind := vValue .Kind ()
117+
118+ if (vKind == reflect .Map || vKind == reflect .Slice || vKind == reflect .Array ) && vValue .Len () < 1 {
119+ return nil
141120 }
142121
143122 return v
0 commit comments