diff --git a/pkg/generate/code/resource_reference.go b/pkg/generate/code/resource_reference.go index 6bd84ee4..f846246c 100644 --- a/pkg/generate/code/resource_reference.go +++ b/pkg/generate/code/resource_reference.go @@ -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 @@ -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 } @@ -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 } @@ -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)) @@ -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) @@ -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 @@ -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++ } } @@ -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 diff --git a/pkg/generate/code/resource_reference_test.go b/pkg/generate/code/resource_reference_test.go index c801a536..6225e5ba 100644 --- a/pkg/generate/code/resource_reference_test.go +++ b/pkg/generate/code/resource_reference_test.go @@ -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") + } + 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) +} diff --git a/pkg/testdata/models/apis/quicksight/0000-00-00/generator-with-nested-references.yaml b/pkg/testdata/models/apis/quicksight/0000-00-00/generator-with-nested-references.yaml new file mode 100644 index 00000000..51af1e77 --- /dev/null +++ b/pkg/testdata/models/apis/quicksight/0000-00-00/generator-with-nested-references.yaml @@ -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