Skip to content

Commit 7a2f8b1

Browse files
Add support for embedded structs in structdiff (#3759)
## Changes This change adds proper handling of anonymous (embedded) struct fields in the structdiff package. When comparing structs with embedded fields, the embedded struct's fields now appear at the root level of the path rather than being prefixed with the embedded struct's name. For example, with: ```go type Embedded struct { Field string `json:"field"` } type Container struct { Embedded Name string `json:"name"` } ``` A change to `Field` will be reported as "field" instead of "Embedded.field". ## Why Dashboards have embedded strings in the resource definition. This is needed so that specifying `parent_path` as a key works in field triggers. Otherwise you would need to specify `Dashboard.parent_path` to account for the embedded struct. ## Tests Added comprehensive test coverage for: - Single embedded field changes - Multiple embedded field changes - Mixed embedded and non-embedded field changes - Zero to non-zero transitions with omitempty - Non-zero to zero transitions All test cases include mirror tests and self-comparison tests.
1 parent a0411d8 commit 7a2f8b1

File tree

2 files changed

+133
-0
lines changed

2 files changed

+133
-0
lines changed

libs/structs/structdiff/diff.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,12 @@ func diffStruct(path *structpath.PathNode, s1, s2 reflect.Value, changes *[]Chan
127127
continue
128128
}
129129

130+
// Continue traversing embedded structs. Do not add the key to the path though.
131+
if sf.Anonymous {
132+
diffValues(path, s1.Field(i), s2.Field(i), changes)
133+
continue
134+
}
135+
130136
jsonTag := structtag.JSONTag(sf.Tag.Get("json"))
131137

132138
// Resolve field name from JSON tag or fall back to Go field name

libs/structs/structdiff/diff_test.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,21 @@ type C struct {
2626
ForceSendFields []string `json:"-"`
2727
}
2828

29+
type Embedded struct {
30+
EmbeddedString string `json:"embedded_field,omitempty"`
31+
EmbeddedInt int `json:"embedded_int,omitempty"`
32+
}
33+
34+
type D struct {
35+
Embedded // Anonymous embedded struct
36+
Name string `json:"name,omitempty"`
37+
}
38+
39+
type E struct {
40+
*Embedded // Pointer to anonymous embedded struct
41+
Name string `json:"name,omitempty"`
42+
}
43+
2944
// ResolvedChange represents a change with the field path as a string (like the old Change struct)
3045
type ResolvedChange struct {
3146
Field string
@@ -260,6 +275,118 @@ func TestGetStructDiff(t *testing.T) {
260275
{Field: "key1.is_enabled", Old: false, New: nil},
261276
},
262277
},
278+
279+
// Embedded struct tests
280+
{
281+
name: "embedded struct field change",
282+
a: D{Embedded: Embedded{EmbeddedString: "old"}, Name: "test"},
283+
b: D{Embedded: Embedded{EmbeddedString: "new"}, Name: "test"},
284+
want: []ResolvedChange{{Field: "embedded_field", Old: "old", New: "new"}},
285+
},
286+
{
287+
name: "embedded struct multiple field changes",
288+
a: D{Embedded: Embedded{EmbeddedString: "old", EmbeddedInt: 5}, Name: "test"},
289+
b: D{Embedded: Embedded{EmbeddedString: "new", EmbeddedInt: 10}, Name: "test"},
290+
want: []ResolvedChange{
291+
{Field: "embedded_field", Old: "old", New: "new"},
292+
{Field: "embedded_int", Old: 5, New: 10},
293+
},
294+
},
295+
{
296+
name: "embedded and non-embedded field changes",
297+
a: D{Embedded: Embedded{EmbeddedString: "old"}, Name: "alice"},
298+
b: D{Embedded: Embedded{EmbeddedString: "new"}, Name: "bob"},
299+
want: []ResolvedChange{
300+
{Field: "embedded_field", Old: "old", New: "new"},
301+
{Field: "name", Old: "alice", New: "bob"},
302+
},
303+
},
304+
{
305+
name: "embedded struct zero to non-zero",
306+
a: D{Name: "test"},
307+
b: D{Embedded: Embedded{EmbeddedString: "value"}, Name: "test"},
308+
want: []ResolvedChange{{Field: "embedded_field", Old: nil, New: "value"}},
309+
},
310+
{
311+
name: "embedded struct non-zero to zero",
312+
a: D{Embedded: Embedded{EmbeddedString: "value"}, Name: "test"},
313+
b: D{Name: "test"},
314+
want: []ResolvedChange{{Field: "embedded_field", Old: "value", New: nil}},
315+
},
316+
{
317+
name: "embedded struct both zero",
318+
a: D{Name: "test"},
319+
b: D{Name: "test"},
320+
want: nil,
321+
},
322+
{
323+
name: "embedded struct only non-embedded field changes",
324+
a: D{Embedded: Embedded{EmbeddedString: "same"}, Name: "alice"},
325+
b: D{Embedded: Embedded{EmbeddedString: "same"}, Name: "bob"},
326+
want: []ResolvedChange{{Field: "name", Old: "alice", New: "bob"}},
327+
},
328+
329+
// Pointer embedded struct tests
330+
{
331+
name: "pointer embedded struct field change",
332+
a: E{Embedded: &Embedded{EmbeddedString: "old"}, Name: "test"},
333+
b: E{Embedded: &Embedded{EmbeddedString: "new"}, Name: "test"},
334+
want: []ResolvedChange{{Field: "embedded_field", Old: "old", New: "new"}},
335+
},
336+
{
337+
name: "pointer embedded struct multiple field changes",
338+
a: E{Embedded: &Embedded{EmbeddedString: "old", EmbeddedInt: 5}, Name: "test"},
339+
b: E{Embedded: &Embedded{EmbeddedString: "new", EmbeddedInt: 10}, Name: "test"},
340+
want: []ResolvedChange{
341+
{Field: "embedded_field", Old: "old", New: "new"},
342+
{Field: "embedded_int", Old: 5, New: 10},
343+
},
344+
},
345+
{
346+
name: "pointer embedded and non-embedded field changes",
347+
a: E{Embedded: &Embedded{EmbeddedString: "old"}, Name: "alice"},
348+
b: E{Embedded: &Embedded{EmbeddedString: "new"}, Name: "bob"},
349+
want: []ResolvedChange{
350+
{Field: "embedded_field", Old: "old", New: "new"},
351+
{Field: "name", Old: "alice", New: "bob"},
352+
},
353+
},
354+
{
355+
name: "pointer embedded struct nil to non-nil",
356+
a: E{Name: "test"},
357+
b: E{Embedded: &Embedded{EmbeddedString: "value"}, Name: "test"},
358+
want: []ResolvedChange{{Field: "", Old: (*Embedded)(nil), New: &Embedded{EmbeddedString: "value"}}},
359+
},
360+
{
361+
name: "pointer embedded struct non-nil to nil",
362+
a: E{Embedded: &Embedded{EmbeddedString: "value"}, Name: "test"},
363+
b: E{Name: "test"},
364+
want: []ResolvedChange{{Field: "", Old: &Embedded{EmbeddedString: "value"}, New: (*Embedded)(nil)}},
365+
},
366+
{
367+
name: "pointer embedded struct both nil",
368+
a: E{Name: "test"},
369+
b: E{Name: "test"},
370+
want: nil,
371+
},
372+
{
373+
name: "pointer embedded struct only non-embedded field changes",
374+
a: E{Embedded: &Embedded{EmbeddedString: "same"}, Name: "alice"},
375+
b: E{Embedded: &Embedded{EmbeddedString: "same"}, Name: "bob"},
376+
want: []ResolvedChange{{Field: "name", Old: "alice", New: "bob"}},
377+
},
378+
{
379+
name: "pointer embedded struct zero to non-zero int",
380+
a: E{Name: "test"},
381+
b: E{Embedded: &Embedded{EmbeddedInt: 42}, Name: "test"},
382+
want: []ResolvedChange{{Field: "", Old: (*Embedded)(nil), New: &Embedded{EmbeddedInt: 42}}},
383+
},
384+
{
385+
name: "pointer embedded struct non-zero to zero int",
386+
a: E{Embedded: &Embedded{EmbeddedInt: 42}, Name: "test"},
387+
b: E{Name: "test"},
388+
want: []ResolvedChange{{Field: "", Old: &Embedded{EmbeddedInt: 42}, New: (*Embedded)(nil)}},
389+
},
263390
}
264391

265392
for _, tt := range tests {

0 commit comments

Comments
 (0)