Skip to content

Commit 3ba8c2e

Browse files
authored
Fix structaccess.Get to handle embedded structs and empty slices/maps (#3657)
## Why Fixing bugs, this is used in direct deployment for resolving resource references. Tests are adapted from #3650 it's the same issue in different libraries. ## Tests New unit tests.
1 parent c0a0f04 commit 3ba8c2e

File tree

2 files changed

+400
-45
lines changed

2 files changed

+400
-45
lines changed

libs/structs/structaccess/get.go

Lines changed: 105 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -90,25 +90,32 @@ func Get(v any, path *structpath.PathNode) (any, error) {
9090
func accessKey(v reflect.Value, key string, path *structpath.PathNode) (reflect.Value, error) {
9191
switch v.Kind() {
9292
case reflect.Struct:
93-
fv, sf, owner, ok := findStructFieldByKey(v, key)
93+
// Precalculate ForceSendFields mappings for this struct hierarchy
94+
forceSendFieldsMap := getForceSendFieldsForFromTyped(v)
95+
96+
fv, sf, embeddedIndex, ok := findStructFieldByKey(v, key)
9497
if !ok {
9598
return reflect.Value{}, fmt.Errorf("%s: field %q not found in %s", path.String(), key, v.Type())
9699
}
97-
// Evaluate ForceSendFields on both the current struct and the declaring owner
98-
force := containsForceSendField(v, sf.Name) || containsForceSendField(owner, sf.Name)
99100

100-
// Honor omitempty: if present and value is zero and not forced, treat as omitted (nil).
101+
// Check ForceSendFields using precalculated map
102+
var force bool
103+
if fields, exists := forceSendFieldsMap[embeddedIndex]; exists {
104+
force = containsString(fields, sf.Name)
105+
}
106+
107+
// Honor omitempty: if present and value is empty and not forced, treat as omitted (nil).
101108
jsonTag := structtag.JSONTag(sf.Tag.Get("json"))
102109
if jsonTag.OmitEmpty() && !force {
103110
if fv.Kind() == reflect.Pointer {
104111
if fv.IsNil() {
105112
return reflect.Value{}, nil
106113
}
107-
// Non-nil pointer: check the element zero-ness for pointers to scalars/structs.
108-
if fv.Elem().IsZero() {
114+
// Non-nil pointer: check if the pointed-to value is empty for omitempty
115+
if isEmptyForOmitEmpty(fv.Elem()) {
109116
return reflect.Value{}, nil
110117
}
111-
} else if fv.IsZero() {
118+
} else if isEmptyForOmitEmpty(fv) {
112119
return reflect.Value{}, nil
113120
}
114121
}
@@ -132,18 +139,18 @@ func accessKey(v reflect.Value, key string, path *structpath.PathNode) (reflect.
132139
}
133140
}
134141

135-
// findStructFieldByKey searches exported fields of struct v for a field matching key.
136-
// It matches json tag name (when present and not "-") only.
137-
// It also searches embedded anonymous structs (pointer or value) recursively.
138-
func findStructFieldByKey(v reflect.Value, key string) (reflect.Value, reflect.StructField, reflect.Value, bool) {
142+
// findFieldInStruct searches for a field by JSON key in a single struct (no embedding).
143+
// Returns: fieldValue, structField, found
144+
func findFieldInStruct(v reflect.Value, key string) (reflect.Value, reflect.StructField, bool) {
139145
t := v.Type()
140-
141-
// First pass: direct fields
142146
for i := range t.NumField() {
143147
sf := t.Field(i)
144148
if sf.PkgPath != "" { // unexported
145149
continue
146150
}
151+
if sf.Anonymous { // skip embedded fields
152+
continue
153+
}
147154

148155
// Read JSON tag using structtag helper
149156
name := structtag.JSONTag(sf.Tag.Get("json")).Name()
@@ -157,11 +164,26 @@ func findStructFieldByKey(v reflect.Value, key string) (reflect.Value, reflect.S
157164
if btag.Internal() || btag.ReadOnly() {
158165
continue
159166
}
160-
return v.Field(i), sf, v, true
167+
return v.Field(i), sf, true
161168
}
162169
}
170+
return reflect.Value{}, reflect.StructField{}, false
171+
}
172+
173+
// findStructFieldByKey searches exported fields of struct v for a field matching key.
174+
// It matches json tag name (when present and not "-") only.
175+
// It also searches embedded anonymous structs (flattening semantics).
176+
// Returns: fieldValue, structField, embeddedIndex, found
177+
// embeddedIndex is -1 for direct fields, or the index of the embedded struct containing the field.
178+
func findStructFieldByKey(v reflect.Value, key string) (reflect.Value, reflect.StructField, int, bool) {
179+
t := v.Type()
180+
181+
// First pass: direct fields
182+
if fv, sf, found := findFieldInStruct(v, key); found {
183+
return fv, sf, -1, true
184+
}
163185

164-
// Second pass: search embedded anonymous structs recursively (flattening semantics)
186+
// Second pass: search embedded anonymous structs (flattening semantics)
165187
for i := range t.NumField() {
166188
sf := t.Field(i)
167189
if !sf.Anonymous {
@@ -179,38 +201,88 @@ func findStructFieldByKey(v reflect.Value, key string) (reflect.Value, reflect.S
179201
if fv.Kind() != reflect.Struct {
180202
continue
181203
}
182-
if out, osf, owner, ok := findStructFieldByKey(fv, key); ok {
183-
// Skip fields marked as internal or readonly via bundle tag
184-
btag := structtag.BundleTag(osf.Tag.Get("bundle"))
185-
if btag.Internal() || btag.ReadOnly() {
186-
// Treat as not found and continue searching other anonymous fields
187-
continue
204+
if out, osf, found := findFieldInStruct(fv, key); found {
205+
return out, osf, i, true
206+
}
207+
}
208+
209+
return reflect.Value{}, reflect.StructField{}, -1, false
210+
}
211+
212+
// getForceSendFieldsForFromTyped collects ForceSendFields values for FromTyped operations
213+
// Returns map[structKey][]fieldName where structKey is -1 for direct fields, embedded index for embedded fields
214+
func getForceSendFieldsForFromTyped(v reflect.Value) map[int][]string {
215+
if !v.IsValid() || v.Type().Kind() != reflect.Struct {
216+
return make(map[int][]string)
217+
}
218+
219+
result := make(map[int][]string)
220+
221+
for i := range v.Type().NumField() {
222+
field := v.Type().Field(i)
223+
fieldValue := v.Field(i)
224+
225+
if field.Name == "ForceSendFields" && !field.Anonymous {
226+
// Direct ForceSendFields (structKey = -1)
227+
if fields, ok := fieldValue.Interface().([]string); ok {
228+
result[-1] = fields
229+
}
230+
} else if field.Anonymous {
231+
// Embedded struct - check for ForceSendFields inside it
232+
if embeddedStruct := getEmbeddedStructForReading(fieldValue); embeddedStruct.IsValid() {
233+
if forceSendField := embeddedStruct.FieldByName("ForceSendFields"); forceSendField.IsValid() {
234+
if fields, ok := forceSendField.Interface().([]string); ok {
235+
result[i] = fields
236+
}
237+
}
188238
}
189-
return out, osf, owner, true
190239
}
191240
}
192241

193-
return reflect.Value{}, reflect.StructField{}, reflect.Value{}, false
242+
return result
194243
}
195244

196-
// containsForceSendField reports whether struct v has a ForceSendFields slice containing goFieldName.
197-
func containsForceSendField(v reflect.Value, goFieldName string) bool {
198-
if !v.IsValid() || v.Kind() != reflect.Struct {
199-
return false
245+
// Helper function for reading - doesn't create nil pointers
246+
func getEmbeddedStructForReading(fieldValue reflect.Value) reflect.Value {
247+
if fieldValue.Kind() == reflect.Pointer {
248+
if fieldValue.IsNil() {
249+
return reflect.Value{} // Don't create, just return invalid
250+
}
251+
fieldValue = fieldValue.Elem()
200252
}
201-
fsField := v.FieldByName("ForceSendFields")
202-
if !fsField.IsValid() || fsField.Kind() != reflect.Slice {
203-
return false
253+
if fieldValue.Kind() == reflect.Struct {
254+
return fieldValue
204255
}
205-
for i := range fsField.Len() {
206-
el := fsField.Index(i)
207-
if el.Kind() == reflect.String && el.String() == goFieldName {
256+
return reflect.Value{}
257+
}
258+
259+
// containsString checks if a slice contains a specific string
260+
func containsString(slice []string, str string) bool {
261+
for _, s := range slice {
262+
if s == str {
208263
return true
209264
}
210265
}
211266
return false
212267
}
213268

269+
// isEmptyForOmitEmpty returns true if the value should be omitted by JSON omitempty.
270+
// This matches JSON encoder behavior, which is different from reflect.IsZero() for slices/maps.
271+
func isEmptyForOmitEmpty(v reflect.Value) bool {
272+
switch v.Kind() {
273+
case reflect.Slice, reflect.Map, reflect.Array:
274+
return v.Len() == 0
275+
case reflect.Interface, reflect.Pointer:
276+
return v.IsNil()
277+
case reflect.Struct:
278+
// Pointers to structs are not considered empty if pointer != nil
279+
// Structs as values are never empty and omitempty on them has no effect.
280+
return false
281+
default:
282+
return v.IsZero()
283+
}
284+
}
285+
214286
// deref dereferences pointers and interfaces until it reaches a non-pointer, non-interface value.
215287
// Returns ok=false if it encounters a nil pointer/interface.
216288
func deref(v reflect.Value) (reflect.Value, bool) {

0 commit comments

Comments
 (0)