Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
50 changes: 34 additions & 16 deletions pkg/generate/code/resource_reference.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ var (
// indexVarFmt stores the format string which takes an integer and creates
// the name of a variable used for the index part of a for-each loop
indexVarFmt = "f%didx"

// mapKeyVarFmt stores the format string which takes an integer and creates
// the name of a variable used for the key part of a map iteration
mapKeyVarFmt = "f%dkey"

// mapValueVarFmt stores the format string which takes an integer and creates
// the name of a variable used for the value part of a map iteration
mapValueVarFmt = "f%dvalue"
)

// ReferenceFieldsValidation returns the go code to validate reference field and
Expand Down Expand Up @@ -235,7 +243,7 @@ func ResolveReferencesForField(field *model.Field, sourceVarName string, indentL

outPrefix += getReferencedStateForField(field, innerIndentLevel)

concreteValueAccessor, err := buildIndexBasedFieldAccessor(field, sourceVarName, indexVarFmt)
concreteValueAccessor, err := buildIndexBasedFieldAccessor(field, sourceVarName)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -285,7 +293,7 @@ func ClearResolvedReferencesForField(field *model.Field, targetVarName string, i
} else {
innerOut += fmt.Sprintf("%sif %s != nil {\n", innerIndent, fieldAccessPrefix)
}
concreteValueAccessor, err := buildIndexBasedFieldAccessor(field, targetVarName, indexVarFmt)
concreteValueAccessor, err := buildIndexBasedFieldAccessor(field, targetVarName)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -355,10 +363,20 @@ func iterReferenceValues(

switch ref.Shape.Type {
case ("map"):
return "", fmt.Errorf(
"resource %q, field %q: references cannot be within a map",
r.Kind, field.Path,
keyVarName := fmt.Sprintf(mapKeyVarFmt, currentListDepth)
valueVarName := fmt.Sprintf(mapValueVarFmt, currentListDepth)

fieldAccessPrefix = fmt.Sprintf("%s.%s", fieldAccessPrefix, fp.At(fpDepth))

outPrefix += fmt.Sprintf("%sfor %s, %s := range %s {\n", indent,
lo.Ternary(shouldRenderIndexes, keyVarName, "_"),
valueVarName,
fieldAccessPrefix,
)
outSuffix = fmt.Sprintf("%s}\n%s", indent, outSuffix)

fieldAccessPrefix = valueVarName
currentListDepth++
case ("structure"):
fieldAccessPrefix = fmt.Sprintf("%s.%s", fieldAccessPrefix, fp.At(fpDepth))

Expand Down Expand Up @@ -396,17 +414,12 @@ func iterReferenceValues(
}

// buildNestedFieldAccessor generates Go code that accesses an inner struct,
// using slice indexes where necessary.
//
// `indexVarFmt` should be a format string that takes a single integer and
// returns the name of a variable which holds the index for the n-th parent
// slice. For example, f%didx will be used to create f0idx, f1idx, etc. for the
// parent slices in the accessors.
// using slice indexes or map keys where necessary.
//
// By default, this method will iterate through every field in the field path.
// Supplying a `parentOffset` will only iterate through the first `fp.Size() -
// parentOffset` number of paths.
func buildIndexBasedFieldAccessorWithOffset(field *model.Field, sourceVarName, indexVarFmt string, parentOffset int) (string, error) {
func buildIndexBasedFieldAccessorWithOffset(field *model.Field, sourceVarName string, parentOffset int) (string, error) {
r := field.CRD
fp := fieldpath.FromString(field.Path)

Expand All @@ -425,7 +438,7 @@ func buildIndexBasedFieldAccessorWithOffset(field *model.Field, sourceVarName, i
fieldName := curFP.Pop()
indexList := ""

if cur.ShapeRef.Shape.Type == "list" {
if cur.ShapeRef.Shape.Type == "list" || cur.ShapeRef.Shape.Type == "map" {

// We want to access indexes when iterating through lists of
// structs. If we find a list at the end of the field path, then we
Expand All @@ -434,7 +447,12 @@ func buildIndexBasedFieldAccessorWithOffset(field *model.Field, sourceVarName, i
// This only applies for when there is no offset, since any offset >
// 0 will cut off the initial field from the path
if idx != (fp.Size()-1) && !isList {
indexList = fmt.Sprintf("[%s]", fmt.Sprintf(indexVarFmt, nestedFieldDepth))
// Use map-specific key format for maps, otherwise use list index format
varFmt := indexVarFmt
if cur.ShapeRef.Shape.Type == "map" {
varFmt = mapKeyVarFmt
}
indexList = fmt.Sprintf("[%s]", fmt.Sprintf(varFmt, nestedFieldDepth))
nestedFieldDepth++
}
}
Expand All @@ -447,8 +465,8 @@ func buildIndexBasedFieldAccessorWithOffset(field *model.Field, sourceVarName, i

// buildIndexBasedFieldAccessor calls buildNestedFieldAccessorWithOffset with an
// offset of 0.
func buildIndexBasedFieldAccessor(field *model.Field, sourceVarName, indexVarFmt string) (string, error) {
return buildIndexBasedFieldAccessorWithOffset(field, sourceVarName, indexVarFmt, 0)
func buildIndexBasedFieldAccessor(field *model.Field, sourceVarName string) (string, error) {
return buildIndexBasedFieldAccessorWithOffset(field, sourceVarName, 0)
}

// getReferencedStateForField returns Go code that makes a call to
Expand Down
100 changes: 100 additions & 0 deletions pkg/generate/code/resource_reference_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -568,3 +568,103 @@ func Test_ClearResolvedReferencesForField_SingleReference_WithinMultipleSlices(t
require.NoError(err)
assert.Equal(expected, got)
}

func Test_ResolveReferencesForField_SingleReference_WithinMap(t *testing.T) {
assert := assert.New(t)
require := require.New(t)

g := testutil.NewModelForServiceWithOptions(t, "quicksight",
&testutil.TestingModelOptions{
GeneratorConfigFile: "generator-with-nested-references.yaml",
})

crd := testutil.GetCRDByName(t, g, "DataSet")
require.NotNil(crd)

expected :=
` for f0key, f0value := range ko.Spec.PhysicalTableMap {
if f0value.CustomSQL != nil {
if f0value.CustomSQL.DataSourceRef != nil && f0value.CustomSQL.DataSourceRef.From != nil {
hasReferences = true
arr := f0value.CustomSQL.DataSourceRef.From
if arr.Name == nil || *arr.Name == "" {
return hasReferences, fmt.Errorf("provided resource reference is nil or empty: PhysicalTableMap.CustomSQL.DataSourceRef")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: should we include the map key in this error message? If there are multiple entries of the same object type it would help users identify the problematic part of their spec.

Copy link
Copy Markdown
Member Author

@gustavodiaz7722 gustavodiaz7722 Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for now we can leave as is, since it follows similar behavior for list type references. I agree though it's poor UX if we don't include key/element identifier.

For now I think it's okay to leave as is. I can follow up on this in another PR if that's okay

}
namespace := ko.ObjectMeta.GetNamespace()
if arr.Namespace != nil && *arr.Namespace != "" {
namespace = *arr.Namespace
}
obj := &svcapitypes.DataSet{}
if err := getReferencedResourceState_DataSet(ctx, apiReader, obj, *arr.Name, namespace); err != nil {
return hasReferences, err
}
ko.Spec.PhysicalTableMap[f0key].CustomSQL.DataSourceARN = (*string)(obj.Status.ACKResourceMetadata.ARN)
}
}
}
`

field := crd.Fields["PhysicalTableMap.CustomSQL.DataSourceARN"]
require.NotNil(field, "Field PhysicalTableMap.CustomSQL.DataSourceARN not found")
got, err := code.ResolveReferencesForField(field, "ko", 1)
require.NoError(err)
assert.Equal(expected, got)
}

func Test_ClearResolvedReferencesForField_SingleReference_WithinMap(t *testing.T) {
assert := assert.New(t)
require := require.New(t)

g := testutil.NewModelForServiceWithOptions(t, "quicksight",
&testutil.TestingModelOptions{
GeneratorConfigFile: "generator-with-nested-references.yaml",
})

crd := testutil.GetCRDByName(t, g, "DataSet")
require.NotNil(crd)

expected :=
` for f0key, f0value := range ko.Spec.PhysicalTableMap {
if f0value.CustomSQL != nil {
if f0value.CustomSQL.DataSourceRef != nil {
ko.Spec.PhysicalTableMap[f0key].CustomSQL.DataSourceARN = nil
}
}
}
`

field := crd.Fields["PhysicalTableMap.CustomSQL.DataSourceARN"]
require.NotNil(field, "Field PhysicalTableMap.CustomSQL.DataSourceARN not found")
got, err := code.ClearResolvedReferencesForField(field, "ko", 1)
require.NoError(err)
assert.Equal(expected, got)
}

func Test_ReferenceFieldsValidation_MapNestedReference(t *testing.T) {
assert := assert.New(t)
require := require.New(t)

g := testutil.NewModelForServiceWithOptions(t, "quicksight",
&testutil.TestingModelOptions{
GeneratorConfigFile: "generator-with-nested-references.yaml",
})

crd := testutil.GetCRDByName(t, g, "DataSet")
require.NotNil(crd)

expected :=
` for _, f0value := range ko.Spec.PhysicalTableMap {
if f0value.CustomSQL != nil {
if f0value.CustomSQL.DataSourceRef != nil && f0value.CustomSQL.DataSourceARN != nil {
return ackerr.ResourceReferenceAndIDNotSupportedFor("PhysicalTableMap.CustomSQL.DataSourceARN", "PhysicalTableMap.CustomSQL.DataSourceRef")
}
}
}
`

field := crd.Fields["PhysicalTableMap.CustomSQL.DataSourceARN"]
require.NotNil(field, "Field PhysicalTableMap.CustomSQL.DataSourceARN not found")
got, err := code.ReferenceFieldsValidation(field, "ko", 1)
require.NoError(err)
assert.Equal(expected, got)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
ignore:
resource_names: []
field_paths:
- CreateDataSetOutput.RequestId
- CreateDataSetOutput.Status
resources:
DataSet:
exceptions:
errors:
404:
code: ResourceNotFoundException
fields:
DataSetId:
is_primary_key: true
ColumnGroups:
compare:
is_ignored: true
ColumnLevelPermissionRules:
compare:
is_ignored: true
DataPrepConfiguration:
compare:
is_ignored: true
DataSetUsageConfiguration:
compare:
is_ignored: true
DatasetParameters:
compare:
is_ignored: true
FieldFolders:
compare:
is_ignored: true
FolderArns:
compare:
is_ignored: true
LogicalTableMap:
compare:
is_ignored: true
Permissions:
compare:
is_ignored: true
PhysicalTableMap:
compare:
is_ignored: true
RowLevelPermissionDataSet:
compare:
is_ignored: true
SemanticModelConfiguration:
compare:
is_ignored: true
PerformanceConfiguration:
compare:
is_ignored: true
UseAs:
compare:
is_ignored: true
PhysicalTableMap.CustomSql.DataSourceArn:
references:
resource: DataSet
path: Status.ACKResourceMetadata.ARN
tags:
ignore: true