diff --git a/pkg/generate/code/set_resource.go b/pkg/generate/code/set_resource.go index e492af9d..a0eb352f 100644 --- a/pkg/generate/code/set_resource.go +++ b/pkg/generate/code/set_resource.go @@ -118,14 +118,29 @@ func SetResource( return "" } + var err error // We might be in a "wrapper" shape. Unwrap it to find the real object - // representation for the CRD's createOp. If there is a single member - // shape and that member shape is a structure, unwrap it. - if outputShape.UsedAsOutput && len(outputShape.MemberRefs) == 1 { - for memberName, memberRef := range outputShape.MemberRefs { - if memberRef.Shape.Type == "structure" { - sourceVarName += "." + memberName - outputShape = memberRef.Shape + // representation for the CRD's createOp/DescribeOP. + + // Use the wrapper field path if it's given in the ack-generate config file. + wrapperFieldPath := r.GetOutputWrapperFieldPath(op) + if wrapperFieldPath != nil { + outputShape, err = GetWrapperOutputShape(outputShape, *wrapperFieldPath) + if err != nil { + msg := fmt.Sprintf("Unable to unwrap the output shape: %v", err) + panic(msg) + } + sourceVarName += "." + *wrapperFieldPath + } else { + // If the wrapper field path is not specified in the config file and if + // there is a single member shape and that member shape is a structure, + // unwrap it. + if outputShape.UsedAsOutput && len(outputShape.MemberRefs) == 1 { + for memberName, memberRef := range outputShape.MemberRefs { + if memberRef.Shape.Type == "structure" { + sourceVarName += "." + memberName + outputShape = memberRef.Shape + } } } } @@ -302,6 +317,41 @@ func SetResource( return out } +// GetWrapperOutputShape returns the shape of the last element of a given field +// Path. It carefully unwraps the output shape and verifies that every element +// of the field path exists in their correspanding parent shape and that they are +// structures. +func GetWrapperOutputShape( + shape *awssdkmodel.Shape, + fieldPath string, +) (*awssdkmodel.Shape, error) { + if fieldPath == "" { + return shape, nil + } + fieldPathParts := strings.Split(fieldPath, ".") + for x, wrapperField := range fieldPathParts { + for memberName, memberRef := range shape.MemberRefs { + if memberName == wrapperField { + if memberRef.Shape.Type != "structure" { + // All the mentionned shapes must be structure + return nil, fmt.Errorf( + "Expected SetOutput.WrapperFieldPath to only contain fields of type 'structure'."+ + " Found %s of type '%s'", + memberName, memberRef.Shape.Type, + ) + } + remainPath := strings.Join(fieldPathParts[x+1:], ".") + return GetWrapperOutputShape(memberRef.Shape, remainPath) + } + } + return nil, fmt.Errorf( + "Incorrect SetOutput.WrapperFieldPath. Could not find %s in Shape %s", + wrapperField, shape.ShapeName, + ) + } + return shape, nil +} + func ListMemberNameInReadManyOutput( r *model.CRD, ) string { diff --git a/pkg/generate/code/set_resource_test.go b/pkg/generate/code/set_resource_test.go index 4517a52d..43b95103 100644 --- a/pkg/generate/code/set_resource_test.go +++ b/pkg/generate/code/set_resource_test.go @@ -14,6 +14,7 @@ package code_test import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -22,6 +23,7 @@ import ( "github.com/aws-controllers-k8s/code-generator/pkg/generate/code" "github.com/aws-controllers-k8s/code-generator/pkg/model" "github.com/aws-controllers-k8s/code-generator/pkg/testutil" + awssdkmodel "github.com/aws/aws-sdk-go/private/model/api" ) func TestSetResource_APIGWv2_Route_Create(t *testing.T) { @@ -153,6 +155,60 @@ func TestSetResource_APIGWv2_Route_ReadOne(t *testing.T) { ) } +func TestSetResource_DynamoDB_Backup_ReadOne(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + g := testutil.NewGeneratorForService(t, "dynamodb") + + crd := testutil.GetCRDByName(t, g, "Backup") + require.NotNil(crd) + + expected := ` + if ko.Status.ACKResourceMetadata == nil { + ko.Status.ACKResourceMetadata = &ackv1alpha1.ResourceMetadata{} + } + if resp.BackupDescription.BackupDetails.BackupArn != nil { + arn := ackv1alpha1.AWSResourceName(*resp.BackupDescription.BackupDetails.BackupArn) + ko.Status.ACKResourceMetadata.ARN = &arn + } + if resp.BackupDescription.BackupDetails.BackupCreationDateTime != nil { + ko.Status.BackupCreationDateTime = &metav1.Time{*resp.BackupDescription.BackupDetails.BackupCreationDateTime} + } else { + ko.Status.BackupCreationDateTime = nil + } + if resp.BackupDescription.BackupDetails.BackupExpiryDateTime != nil { + ko.Status.BackupExpiryDateTime = &metav1.Time{*resp.BackupDescription.BackupDetails.BackupExpiryDateTime} + } else { + ko.Status.BackupExpiryDateTime = nil + } + if resp.BackupDescription.BackupDetails.BackupName != nil { + ko.Spec.BackupName = resp.BackupDescription.BackupDetails.BackupName + } else { + ko.Spec.BackupName = nil + } + if resp.BackupDescription.BackupDetails.BackupSizeBytes != nil { + ko.Status.BackupSizeBytes = resp.BackupDescription.BackupDetails.BackupSizeBytes + } else { + ko.Status.BackupSizeBytes = nil + } + if resp.BackupDescription.BackupDetails.BackupStatus != nil { + ko.Status.BackupStatus = resp.BackupDescription.BackupDetails.BackupStatus + } else { + ko.Status.BackupStatus = nil + } + if resp.BackupDescription.BackupDetails.BackupType != nil { + ko.Status.BackupType = resp.BackupDescription.BackupDetails.BackupType + } else { + ko.Status.BackupType = nil + } +` + assert.Equal( + expected, + code.SetResource(crd.Config(), crd, model.OpTypeGet, "resp", "ko", 1, true), + ) +} + func TestSetResource_CodeDeploy_Deployment_Create(t *testing.T) { assert := assert.New(t) require := require.New(t) @@ -2530,3 +2586,62 @@ func TestSetResource_RDS_DBSubnetGroup_ReadMany(t *testing.T) { code.SetResource(crd.Config(), crd, model.OpTypeList, "resp", "ko", 1, false), ) } + +func TestGetWrapperOutputShape(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + g := testutil.NewGeneratorForService(t, "dynamodb") + + crd := testutil.GetCRDByName(t, g, "Backup") + require.NotNil(crd) + + op := crd.Ops.ReadOne.OutputRef.Shape + + type args struct { + outputShape *awssdkmodel.Shape + fieldPath string + } + tests := []struct { + name string + args args + wantErr bool + wantShapeName string + }{ + { + name: "incorrect field path: element not found", + args: args{ + outputShape: op, + fieldPath: "BackupDescription.Something", + }, + wantErr: true, + }, + { + name: "incorrect field path: element not of type structure", + args: args{ + outputShape: op, + fieldPath: "BackupDescription.BackupArn", + }, + wantErr: true, + }, + { + name: "correct field path", + args: args{ + outputShape: op, + fieldPath: "BackupDescription.BackupDetails", + }, + wantErr: false, + wantShapeName: "BackupDetails", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + outputShape, err := code.GetWrapperOutputShape(tt.args.outputShape, tt.args.fieldPath) + if (err != nil) != tt.wantErr { + assert.Fail(fmt.Sprintf("GetWrapperOutputShape() error = %v, wantErr %v", err, tt.wantErr)) + } else if !tt.wantErr { + assert.Equal(tt.wantShapeName, outputShape.ShapeName) + } + }) + } +} diff --git a/pkg/generate/config/operation.go b/pkg/generate/config/operation.go index 8157a283..59b880d0 100644 --- a/pkg/generate/config/operation.go +++ b/pkg/generate/config/operation.go @@ -28,6 +28,9 @@ type OperationConfig struct { // `resourceManager` struct that will set fields on a `resource` struct // depending on the output of the operation. SetOutputCustomMethodName string `json:"set_output_custom_method_name,omitempty"` + // OutputWrapperFieldPath provides the JSON-Path like to the struct field containing + // information that will be merged into a `resource` object. + OutputWrapperFieldPath string `json:"output_wrapper_field_path,omitempty"` // Override for resource name in case of heuristic failure // An example of this is correcting stutter when the resource logic doesn't properly determine the resource name ResourceName string `json:"resource_name"` diff --git a/pkg/generate/testdata/models/apis/dynamodb/0000-00-00/generator.yaml b/pkg/generate/testdata/models/apis/dynamodb/0000-00-00/generator.yaml index 4c73140f..34a39b40 100644 --- a/pkg/generate/testdata/models/apis/dynamodb/0000-00-00/generator.yaml +++ b/pkg/generate/testdata/models/apis/dynamodb/0000-00-00/generator.yaml @@ -4,3 +4,12 @@ resources: errors: 404: code: ResourceNotFoundException +operations: + DescribeBackup: + # DescribeBackupOutput is an unsual shape because it contains information for + # the backup it self (BackupDetails), the table details when the backup was + # created (SourceTableDetails) and the table features (SourceTableFeatureDetails). + # If not specified the code generator will try to determine the wrapper field by + # selecting for the output shape that only have a single member, which is incorrect + # in this case. + output_wrapper_field_path: BackupDescription.BackupDetails \ No newline at end of file diff --git a/pkg/model/crd.go b/pkg/model/crd.go index 1110ac32..536907bd 100644 --- a/pkg/model/crd.go +++ b/pkg/model/crd.go @@ -352,6 +352,28 @@ func (r *CRD) SetOutputCustomMethodName( return &resGenConfig.SetOutputCustomMethodName } +// GetOutputWrapperFieldPath returns the JSON-Path of the output wrapper field +// as *string for a given operation, if specified in generator config. +func (r *CRD) GetOutputWrapperFieldPath( + op *awssdkmodel.Operation, +) *string { + if op == nil { + return nil + } + if r.cfg == nil { + return nil + } + opConfig, found := r.cfg.Operations[op.Name] + if !found { + return nil + } + + if opConfig.OutputWrapperFieldPath == "" { + return nil + } + return &opConfig.OutputWrapperFieldPath +} + // GetCustomImplementation returns custom implementation method name for the // supplied operation as specified in generator config func (r *CRD) GetCustomImplementation(