6
6
"reflect"
7
7
8
8
"github.com/google/uuid"
9
+ "golang.org/x/xerrors"
9
10
10
11
"github.com/coder/coder/coderd/audit"
11
12
"github.com/coder/coder/coderd/database"
@@ -18,11 +19,9 @@ func structName(t reflect.Type) string {
18
19
func diffValues (left ,right any ,table Table ) audit.Map {
19
20
var (
20
21
baseDiff = audit.Map {}
21
-
22
- leftV = reflect .ValueOf (left )
23
-
24
- rightV = reflect .ValueOf (right )
25
- rightT = reflect .TypeOf (right )
22
+ rightT = reflect .TypeOf (right )
23
+ leftV = reflect .ValueOf (left )
24
+ rightV = reflect .ValueOf (right )
26
25
27
26
diffKey = table [structName (rightT )]
28
27
)
@@ -31,19 +30,25 @@ func diffValues(left, right any, table Table) audit.Map {
31
30
panic (fmt .Sprintf ("dev error: type %q (type %T) attempted audit but not auditable" ,rightT .Name (),right ))
32
31
}
33
32
34
- for i := 0 ;i < rightT .NumField ();i ++ {
35
- if ! rightT .Field (i ).IsExported () {
36
- continue
37
- }
33
+ // allFields contains all top level fields of the struct.
34
+ allFields ,err := flattenStructFields (leftV ,rightV )
35
+ if err != nil {
36
+ // This should never happen. Only structs should be flattened. If an
37
+ // error occurs, an unsupported or non-struct type was passed in.
38
+ panic (fmt .Sprintf ("dev error: failed to flatten struct fields: %v" ,err ))
39
+ }
38
40
41
+ for _ ,field := range allFields {
39
42
var (
40
- leftF = leftV . Field ( i )
41
- rightF = rightV . Field ( i )
43
+ leftF = field . LeftF
44
+ rightF = field . RightF
42
45
43
46
leftI = leftF .Interface ()
44
47
rightI = rightF .Interface ()
48
+ )
45
49
46
- diffName = rightT .Field (i ).Tag .Get ("json" )
50
+ var (
51
+ diffName = field .FieldType .Tag .Get ("json" )
47
52
)
48
53
49
54
atype ,ok := diffKey [diffName ]
@@ -145,6 +150,64 @@ func convertDiffType(left, right any) (newLeft, newRight any, changed bool) {
145
150
}
146
151
}
147
152
153
+ // fieldDiff has all the required information to return an audit diff for a
154
+ // given field.
155
+ type fieldDiff struct {
156
+ FieldType reflect.StructField
157
+ LeftF reflect.Value
158
+ RightF reflect.Value
159
+ }
160
+
161
+ // flattenStructFields will return all top level fields for a given structure.
162
+ // Only anonymously embedded structs will be recursively flattened such that their
163
+ // fields are returned as top level fields. Named nested structs will be returned
164
+ // as a single field.
165
+ // Conflicting field names need to be handled by the caller.
166
+ func flattenStructFields (leftV ,rightV reflect.Value ) ([]fieldDiff ,error ) {
167
+ // Dereference pointers if the field is a pointer field.
168
+ if leftV .Kind ()== reflect .Ptr {
169
+ leftV = derefPointer (leftV )
170
+ rightV = derefPointer (rightV )
171
+ }
172
+
173
+ if leftV .Kind ()!= reflect .Struct {
174
+ return nil ,xerrors .Errorf ("%q is not a struct, kind=%s" ,leftV .String (),leftV .Kind ())
175
+ }
176
+
177
+ var allFields []fieldDiff
178
+ rightT := rightV .Type ()
179
+
180
+ // Loop through all top level fields of the struct.
181
+ for i := 0 ;i < rightT .NumField ();i ++ {
182
+ if ! rightT .Field (i ).IsExported () {
183
+ continue
184
+ }
185
+
186
+ var (
187
+ leftF = leftV .Field (i )
188
+ rightF = rightV .Field (i )
189
+ )
190
+
191
+ if rightT .Field (i ).Anonymous {
192
+ // Anonymous fields are recursively flattened.
193
+ anonFields ,err := flattenStructFields (leftF ,rightF )
194
+ if err != nil {
195
+ return nil ,xerrors .Errorf ("flatten anonymous field %q: %w" ,rightT .Field (i ).Name ,err )
196
+ }
197
+ allFields = append (allFields ,anonFields ... )
198
+ continue
199
+ }
200
+
201
+ // Single fields append as is.
202
+ allFields = append (allFields ,fieldDiff {
203
+ LeftF :leftF ,
204
+ RightF :rightF ,
205
+ FieldType :rightT .Field (i ),
206
+ })
207
+ }
208
+ return allFields ,nil
209
+ }
210
+
148
211
// derefPointer deferences a reflect.Value that is a pointer to its underlying
149
212
// value. It dereferences recursively until it finds a non-pointer value. If the
150
213
// pointer is nil, it will be coerced to the zero value of the underlying type.