Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions libs/structs/structaccess/bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,38 @@ func TestGet_ConfigRoot_JobTagsAccess(t *testing.T) {
require.Error(t, ValidateByString(reflect.TypeFor[config.Root](), "resources.apps.my_app.url.inner"))
require.Error(t, ValidateByString(reflect.TypeFor[config.Root](), "resources.apps.my_app.url1"))
}

// A bundle resource embeds a config struct that embeds the SDK request struct, so its
// fields sit two levels down. Get, Set and ValidatePath all have to reach them, and
// ForceSendFields belongs to the struct that declares the field -- not to the outer one
// that shadows the name.
func TestGetSet_DoublyEmbeddedField(t *testing.T) {
project := &resources.PostgresProject{} //exhaustruct:ignore
project.ProjectId = "p"

require.NoError(t, ValidateByString(reflect.TypeOf(project), "budget_policy_id"))

require.NoError(t, SetByString(project, "budget_policy_id", "abc"))
require.Equal(t, "abc", project.BudgetPolicyId)

value, err := GetByString(project, "budget_policy_id")
require.NoError(t, err)
require.Equal(t, "abc", value)

// An explicit empty value is recorded on ProjectSpec, which declares the field.
require.NoError(t, SetByString(project, "budget_policy_id", ""))
require.Contains(t, project.ProjectSpec.ForceSendFields, "BudgetPolicyId")
require.NotContains(t, project.ForceSendFields, "BudgetPolicyId")

value, err = GetByString(project, "budget_policy_id")
require.NoError(t, err)
// The empty string, not nil: that is what separates an explicit "" from an absent field.
require.Equal(t, any(""), value)

// And dropping it again leaves the field absent.
require.NoError(t, SetByString(project, "budget_policy_id", nil))
require.NotContains(t, project.ProjectSpec.ForceSendFields, "BudgetPolicyId")
value, err = GetByString(project, "budget_policy_id")
require.NoError(t, err)
require.Nil(t, value)
}
158 changes: 46 additions & 112 deletions libs/structs/structaccess/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,19 +138,13 @@ func Get(v any, path *structpath.PathNode) (any, error) {
func accessKey(v reflect.Value, key string, path *structpath.PathNode) (reflect.Value, error) {
switch v.Kind() {
case reflect.Struct:
// Precalculate ForceSendFields mappings for this struct hierarchy
forceSendFieldsMap := getForceSendFieldsForFromTyped(v)

fv, sf, embeddedIndex, ok := findStructFieldByKey(v, key)
fv, sf, owner, ok := findStructFieldByKey(v, key)
if !ok {
return reflect.Value{}, fmt.Errorf("%s: field %q not found in %s", path.String(), key, v.Type())
}

// Check ForceSendFields using precalculated map
var force bool
if fields, exists := forceSendFieldsMap[embeddedIndex]; exists {
force = containsString(fields, sf.Name)
}
// ForceSendFields is only managed by the struct that declares the field.
force := forceSendFieldsContains(owner, sf.Name)

// Honor omitempty: if present and value is empty and not forced, treat as omitted (nil).
jsonTag := structtag.JSONTag(sf.Tag.Get("json"))
Expand Down Expand Up @@ -234,124 +228,64 @@ func accessKeyValue(v reflect.Value, key, value string, path *structpath.PathNod
return reflect.Value{}, &NotFoundError{fmt.Sprintf("%s: no element found with %s=%q", path.String(), key, value)}
}

// findFieldInStruct searches for a field by JSON key in a single struct (no embedding).
// Returns: fieldValue, structField, found
func findFieldInStruct(v reflect.Value, key string) (reflect.Value, reflect.StructField, bool) {
t := v.Type()
for i := range t.NumField() {
sf := t.Field(i)
if sf.PkgPath != "" { // unexported
continue
}
if sf.Anonymous { // skip embedded fields
continue
}

// Read JSON tag using structtag helper
name := structtag.JSONTag(sf.Tag.Get("json")).Name()
if name == "-" {
name = ""
}

if sf.Name == EmbeddedSliceFieldName {
continue // EmbeddedSlice fields are not accessible by name
}
if name != "" && name == key {
// Skip fields marked as internal or readonly via bundle tag
btag := structtag.BundleTag(sf.Tag.Get("bundle"))
if btag.Internal() || btag.ReadOnly() {
continue
}
return v.Field(i), sf, true
}
}
return reflect.Value{}, reflect.StructField{}, false
}

// findStructFieldByKey searches exported fields of struct v for a field matching key.
// It matches json tag name (when present and not "-") only.
// It also searches embedded anonymous structs (flattening semantics).
// Returns: fieldValue, structField, embeddedIndex, found
// embeddedIndex is -1 for direct fields, or the index of the embedded struct containing the field.
func findStructFieldByKey(v reflect.Value, key string) (reflect.Value, reflect.StructField, int, bool) {
t := v.Type()

// First pass: direct fields
if fv, sf, found := findFieldInStruct(v, key); found {
return fv, sf, -1, true
// findStructFieldByKey resolves key against the type of v and then navigates v along the
// index chain the resolution produced.
//
// Resolving on the type is what keeps Get, Set and ValidatePattern agreeing with each other
// and with encoding/json: the type decides which of two same-named fields wins, whether the
// name is ambiguous, and by which path the winner is reached. Navigating the value afterwards
// means a nil pointer on that path reads as an absent field, rather than the search falling
// through to a deeper field of the same name that the wire format never carries.
//
// Returns: fieldValue, structField, owner (the struct value declaring the field), found
func findStructFieldByKey(v reflect.Value, key string) (reflect.Value, reflect.StructField, reflect.Value, bool) {
index, sf, ok := findFieldIndexByKeyType(v.Type(), key)
if !ok {
return reflect.Value{}, reflect.StructField{}, reflect.Value{}, false
}

// Second pass: search embedded anonymous structs (flattening semantics)
for i := range t.NumField() {
sf := t.Field(i)
if !sf.Anonymous {
continue
}
fv := v.Field(i)
// Dereference pointer anonymous structs
for fv.Kind() == reflect.Pointer {
if fv.IsNil() {
// Not initialized; can't descend
break
cur := v
var owner reflect.Value
for _, i := range index {
for cur.Kind() == reflect.Pointer {
if cur.IsNil() {
return reflect.Value{}, reflect.StructField{}, reflect.Value{}, false
}
fv = fv.Elem()
}
if fv.Kind() != reflect.Struct {
continue
cur = cur.Elem()
}
if out, osf, found := findFieldInStruct(fv, key); found {
return out, osf, i, true
if cur.Kind() != reflect.Struct {
return reflect.Value{}, reflect.StructField{}, reflect.Value{}, false
}
owner = cur
cur = cur.Field(i)
}

return reflect.Value{}, reflect.StructField{}, -1, false
return cur, sf, owner, true
}

// getForceSendFieldsForFromTyped collects ForceSendFields values for FromTyped operations
// Returns map[structKey][]fieldName where structKey is -1 for direct fields, embedded index for embedded fields
func getForceSendFieldsForFromTyped(v reflect.Value) map[int][]string {
if !v.IsValid() || v.Type().Kind() != reflect.Struct {
return make(map[int][]string)
// forceSendFields returns the ForceSendFields slice a struct declares itself. A struct that
// embeds another shadows it deliberately -- see resources.PostgresProjectConfig -- so only
// the declaring struct tracks a field of its own.
func forceSendFields(owner reflect.Value) reflect.Value {
if !owner.IsValid() || owner.Kind() != reflect.Struct {
return reflect.Value{}
}

result := make(map[int][]string)

for i := range v.Type().NumField() {
field := v.Type().Field(i)
fieldValue := v.Field(i)

for i := range owner.Type().NumField() {
field := owner.Type().Field(i)
if field.Name == "ForceSendFields" && !field.Anonymous {
// Direct ForceSendFields (structKey = -1)
if fields, ok := reflect.TypeAssert[[]string](fieldValue); ok {
result[-1] = fields
}
} else if field.Anonymous {
// Embedded struct - check for ForceSendFields inside it
if embeddedStruct := getEmbeddedStructForReading(fieldValue); embeddedStruct.IsValid() {
if forceSendField := embeddedStruct.FieldByName("ForceSendFields"); forceSendField.IsValid() {
if fields, ok := reflect.TypeAssert[[]string](forceSendField); ok {
result[i] = fields
}
}
}
return owner.Field(i)
}
}

return result
return reflect.Value{}
}

// Helper function for reading - doesn't create nil pointers
func getEmbeddedStructForReading(fieldValue reflect.Value) reflect.Value {
if fieldValue.Kind() == reflect.Pointer {
if fieldValue.IsNil() {
return reflect.Value{} // Don't create, just return invalid
}
fieldValue = fieldValue.Elem()
}
if fieldValue.Kind() == reflect.Struct {
return fieldValue
// forceSendFieldsContains reports whether a struct forces the named field to be sent.
func forceSendFieldsContains(owner reflect.Value, name string) bool {
fsf := forceSendFields(owner)
if !fsf.IsValid() {
return false
}
return reflect.Value{}
fields, ok := reflect.TypeAssert[[]string](fsf)
return ok && containsString(fields, name)
}

// containsString checks if a slice contains a specific string
Expand Down
66 changes: 6 additions & 60 deletions libs/structs/structaccess/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func setFieldOrMapValue(parentVal reflect.Value, key string, valueVal reflect.Va

// setStructField sets a field in a struct and handles ForceSendFields
func setStructField(parentVal reflect.Value, fieldName string, valueVal reflect.Value) error {
fv, sf, embeddedIndex, ok := findStructFieldByKey(parentVal, fieldName)
fv, sf, owner, ok := findStructFieldByKey(parentVal, fieldName)
if !ok {
return fmt.Errorf("field %q not found in %s", fieldName, parentVal.Type())
}
Expand All @@ -155,9 +155,9 @@ func setStructField(parentVal reflect.Value, fieldName string, valueVal reflect.
if !valueVal.IsValid() {
// Setting nil: the field is being made absent, which convertValue renders as the zero
// value. Pass the invalid value through so it is removed from ForceSendFields.
return updateForceSendFields(parentVal, sf.Name, embeddedIndex, valueVal, sf)
return updateForceSendFields(owner, sf.Name, valueVal, sf)
}
return updateForceSendFields(parentVal, sf.Name, embeddedIndex, converted, sf)
return updateForceSendFields(owner, sf.Name, converted, sf)
}

// setMapValue sets a value in a map
Expand Down Expand Up @@ -314,7 +314,7 @@ func convertValue(valueVal reflect.Value, targetType reflect.Type) (reflect.Valu
// - If setting nil: remove field from ForceSendFields
// - If setting empty value: add field to ForceSendFields (if not already present)
// Only applies to fields with omitempty tag
func updateForceSendFields(parentVal reflect.Value, fieldName string, embeddedIndex int, valueVal reflect.Value, structField reflect.StructField) error {
func updateForceSendFields(owner reflect.Value, fieldName string, valueVal reflect.Value, structField reflect.StructField) error {
isSettingNil := !valueVal.IsValid()
isSettingEmptyValue := valueVal.IsValid() && isEmptyForOmitEmpty(valueVal)

Expand All @@ -330,8 +330,8 @@ func updateForceSendFields(parentVal reflect.Value, fieldName string, embeddedIn
return nil
}

// Find the appropriate ForceSendFields slice to modify
forceSendFieldsSlice := findForceSendFieldsForSetting(parentVal, embeddedIndex)
// Only the struct that declares the field tracks it.
forceSendFieldsSlice := forceSendFields(owner)
if !forceSendFieldsSlice.IsValid() {
// No ForceSendFields to update
return nil
Expand All @@ -348,60 +348,6 @@ func updateForceSendFields(parentVal reflect.Value, fieldName string, embeddedIn
return nil
}

// findForceSendFieldsForSetting finds the correct ForceSendFields slice to modify
// This should match the logic in get.go's getForceSendFieldsForFromTyped
// Only the struct that contains the ForceSendFields can manage its own fields
// embeddedIndex: -1 for direct fields, or the index of the embedded struct
func findForceSendFieldsForSetting(parentVal reflect.Value, embeddedIndex int) reflect.Value {
if embeddedIndex == -1 {
// Direct field - check if parent struct has its own ForceSendFields
// We need to check the struct type directly, not through field promotion
parentType := parentVal.Type()
for i := range parentType.NumField() {
field := parentType.Field(i)
if field.Name == "ForceSendFields" && !field.Anonymous {
// Parent has direct ForceSendFields
return parentVal.Field(i)
}
}
// Parent struct has no direct ForceSendFields, so no management possible
return reflect.Value{}
} else {
// Embedded field - look for ForceSendFields in the embedded struct
embeddedField := parentVal.Field(embeddedIndex)
embeddedStruct := getEmbeddedStructForSetting(embeddedField)
if !embeddedStruct.IsValid() {
return reflect.Value{}
}
fsf := embeddedStruct.FieldByName("ForceSendFields")
if fsf.IsValid() {
return fsf
}
// Embedded struct has no ForceSendFields, so no management possible
return reflect.Value{}
}
}

// getEmbeddedStructForSetting gets the embedded struct for setting operations
// Creates nil pointers if needed
func getEmbeddedStructForSetting(fieldValue reflect.Value) reflect.Value {
if fieldValue.Kind() == reflect.Pointer {
if fieldValue.IsNil() {
// Create new instance if needed
if fieldValue.CanSet() {
fieldValue.Set(reflect.New(fieldValue.Type().Elem()))
} else {
return reflect.Value{}
}
}
fieldValue = fieldValue.Elem()
}
if fieldValue.Kind() == reflect.Struct {
return fieldValue
}
return reflect.Value{}
}

// removeFromForceSendFields removes fieldName from the ForceSendFields slice
func removeFromForceSendFields(forceSendFieldsSlice reflect.Value, fieldName string) {
// Get the original []string slice
Expand Down
Loading