Skip to content

Commit 99e7023

Browse files
authored
Add a new operation config to override the SetOutput wrapper field (#78)
Issue N/A In some AWS APIs `Describe` operations output contains the wanted information in a wrapper field like `TableDescription` and `GlobalTableDescription` ... However in some other APIs for the same operation type, the wanted information is wrapped in different nested object, like `BackupDescription.BackupDetails` for example. Description of changes: This patch adds a new operation configuration that instructs the code-generator to use a specific field path, in order to find the struct that contains the information we want to set the output from. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 4857c76 commit 99e7023

File tree

5 files changed

+206
-7
lines changed

5 files changed

+206
-7
lines changed

pkg/generate/code/set_resource.go

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,29 @@ func SetResource(
118118
return ""
119119
}
120120

121+
var err error
121122
// We might be in a "wrapper" shape. Unwrap it to find the real object
122-
// representation for the CRD's createOp. If there is a single member
123-
// shape and that member shape is a structure, unwrap it.
124-
if outputShape.UsedAsOutput && len(outputShape.MemberRefs) == 1 {
125-
for memberName, memberRef := range outputShape.MemberRefs {
126-
if memberRef.Shape.Type == "structure" {
127-
sourceVarName += "." + memberName
128-
outputShape = memberRef.Shape
123+
// representation for the CRD's createOp/DescribeOP.
124+
125+
// Use the wrapper field path if it's given in the ack-generate config file.
126+
wrapperFieldPath := r.GetOutputWrapperFieldPath(op)
127+
if wrapperFieldPath != nil {
128+
outputShape, err = GetWrapperOutputShape(outputShape, *wrapperFieldPath)
129+
if err != nil {
130+
msg := fmt.Sprintf("Unable to unwrap the output shape: %v", err)
131+
panic(msg)
132+
}
133+
sourceVarName += "." + *wrapperFieldPath
134+
} else {
135+
// If the wrapper field path is not specified in the config file and if
136+
// there is a single member shape and that member shape is a structure,
137+
// unwrap it.
138+
if outputShape.UsedAsOutput && len(outputShape.MemberRefs) == 1 {
139+
for memberName, memberRef := range outputShape.MemberRefs {
140+
if memberRef.Shape.Type == "structure" {
141+
sourceVarName += "." + memberName
142+
outputShape = memberRef.Shape
143+
}
129144
}
130145
}
131146
}
@@ -302,6 +317,41 @@ func SetResource(
302317
return out
303318
}
304319

320+
// GetWrapperOutputShape returns the shape of the last element of a given field
321+
// Path. It carefully unwraps the output shape and verifies that every element
322+
// of the field path exists in their correspanding parent shape and that they are
323+
// structures.
324+
func GetWrapperOutputShape(
325+
shape *awssdkmodel.Shape,
326+
fieldPath string,
327+
) (*awssdkmodel.Shape, error) {
328+
if fieldPath == "" {
329+
return shape, nil
330+
}
331+
fieldPathParts := strings.Split(fieldPath, ".")
332+
for x, wrapperField := range fieldPathParts {
333+
for memberName, memberRef := range shape.MemberRefs {
334+
if memberName == wrapperField {
335+
if memberRef.Shape.Type != "structure" {
336+
// All the mentionned shapes must be structure
337+
return nil, fmt.Errorf(
338+
"Expected SetOutput.WrapperFieldPath to only contain fields of type 'structure'."+
339+
" Found %s of type '%s'",
340+
memberName, memberRef.Shape.Type,
341+
)
342+
}
343+
remainPath := strings.Join(fieldPathParts[x+1:], ".")
344+
return GetWrapperOutputShape(memberRef.Shape, remainPath)
345+
}
346+
}
347+
return nil, fmt.Errorf(
348+
"Incorrect SetOutput.WrapperFieldPath. Could not find %s in Shape %s",
349+
wrapperField, shape.ShapeName,
350+
)
351+
}
352+
return shape, nil
353+
}
354+
305355
func ListMemberNameInReadManyOutput(
306356
r *model.CRD,
307357
) string {

pkg/generate/code/set_resource_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
package code_test
1515

1616
import (
17+
"fmt"
1718
"testing"
1819

1920
"github.com/stretchr/testify/assert"
@@ -22,6 +23,7 @@ import (
2223
"github.com/aws-controllers-k8s/code-generator/pkg/generate/code"
2324
"github.com/aws-controllers-k8s/code-generator/pkg/model"
2425
"github.com/aws-controllers-k8s/code-generator/pkg/testutil"
26+
awssdkmodel "github.com/aws/aws-sdk-go/private/model/api"
2527
)
2628

2729
func TestSetResource_APIGWv2_Route_Create(t *testing.T) {
@@ -153,6 +155,60 @@ func TestSetResource_APIGWv2_Route_ReadOne(t *testing.T) {
153155
)
154156
}
155157

158+
func TestSetResource_DynamoDB_Backup_ReadOne(t *testing.T) {
159+
assert := assert.New(t)
160+
require := require.New(t)
161+
162+
g := testutil.NewGeneratorForService(t, "dynamodb")
163+
164+
crd := testutil.GetCRDByName(t, g, "Backup")
165+
require.NotNil(crd)
166+
167+
expected := `
168+
if ko.Status.ACKResourceMetadata == nil {
169+
ko.Status.ACKResourceMetadata = &ackv1alpha1.ResourceMetadata{}
170+
}
171+
if resp.BackupDescription.BackupDetails.BackupArn != nil {
172+
arn := ackv1alpha1.AWSResourceName(*resp.BackupDescription.BackupDetails.BackupArn)
173+
ko.Status.ACKResourceMetadata.ARN = &arn
174+
}
175+
if resp.BackupDescription.BackupDetails.BackupCreationDateTime != nil {
176+
ko.Status.BackupCreationDateTime = &metav1.Time{*resp.BackupDescription.BackupDetails.BackupCreationDateTime}
177+
} else {
178+
ko.Status.BackupCreationDateTime = nil
179+
}
180+
if resp.BackupDescription.BackupDetails.BackupExpiryDateTime != nil {
181+
ko.Status.BackupExpiryDateTime = &metav1.Time{*resp.BackupDescription.BackupDetails.BackupExpiryDateTime}
182+
} else {
183+
ko.Status.BackupExpiryDateTime = nil
184+
}
185+
if resp.BackupDescription.BackupDetails.BackupName != nil {
186+
ko.Spec.BackupName = resp.BackupDescription.BackupDetails.BackupName
187+
} else {
188+
ko.Spec.BackupName = nil
189+
}
190+
if resp.BackupDescription.BackupDetails.BackupSizeBytes != nil {
191+
ko.Status.BackupSizeBytes = resp.BackupDescription.BackupDetails.BackupSizeBytes
192+
} else {
193+
ko.Status.BackupSizeBytes = nil
194+
}
195+
if resp.BackupDescription.BackupDetails.BackupStatus != nil {
196+
ko.Status.BackupStatus = resp.BackupDescription.BackupDetails.BackupStatus
197+
} else {
198+
ko.Status.BackupStatus = nil
199+
}
200+
if resp.BackupDescription.BackupDetails.BackupType != nil {
201+
ko.Status.BackupType = resp.BackupDescription.BackupDetails.BackupType
202+
} else {
203+
ko.Status.BackupType = nil
204+
}
205+
`
206+
assert.Equal(
207+
expected,
208+
code.SetResource(crd.Config(), crd, model.OpTypeGet, "resp", "ko", 1, true),
209+
)
210+
}
211+
156212
func TestSetResource_CodeDeploy_Deployment_Create(t *testing.T) {
157213
assert := assert.New(t)
158214
require := require.New(t)
@@ -2530,3 +2586,62 @@ func TestSetResource_RDS_DBSubnetGroup_ReadMany(t *testing.T) {
25302586
code.SetResource(crd.Config(), crd, model.OpTypeList, "resp", "ko", 1, false),
25312587
)
25322588
}
2589+
2590+
func TestGetWrapperOutputShape(t *testing.T) {
2591+
assert := assert.New(t)
2592+
require := require.New(t)
2593+
2594+
g := testutil.NewGeneratorForService(t, "dynamodb")
2595+
2596+
crd := testutil.GetCRDByName(t, g, "Backup")
2597+
require.NotNil(crd)
2598+
2599+
op := crd.Ops.ReadOne.OutputRef.Shape
2600+
2601+
type args struct {
2602+
outputShape *awssdkmodel.Shape
2603+
fieldPath string
2604+
}
2605+
tests := []struct {
2606+
name string
2607+
args args
2608+
wantErr bool
2609+
wantShapeName string
2610+
}{
2611+
{
2612+
name: "incorrect field path: element not found",
2613+
args: args{
2614+
outputShape: op,
2615+
fieldPath: "BackupDescription.Something",
2616+
},
2617+
wantErr: true,
2618+
},
2619+
{
2620+
name: "incorrect field path: element not of type structure",
2621+
args: args{
2622+
outputShape: op,
2623+
fieldPath: "BackupDescription.BackupArn",
2624+
},
2625+
wantErr: true,
2626+
},
2627+
{
2628+
name: "correct field path",
2629+
args: args{
2630+
outputShape: op,
2631+
fieldPath: "BackupDescription.BackupDetails",
2632+
},
2633+
wantErr: false,
2634+
wantShapeName: "BackupDetails",
2635+
},
2636+
}
2637+
for _, tt := range tests {
2638+
t.Run(tt.name, func(t *testing.T) {
2639+
outputShape, err := code.GetWrapperOutputShape(tt.args.outputShape, tt.args.fieldPath)
2640+
if (err != nil) != tt.wantErr {
2641+
assert.Fail(fmt.Sprintf("GetWrapperOutputShape() error = %v, wantErr %v", err, tt.wantErr))
2642+
} else if !tt.wantErr {
2643+
assert.Equal(tt.wantShapeName, outputShape.ShapeName)
2644+
}
2645+
})
2646+
}
2647+
}

pkg/generate/config/operation.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ type OperationConfig struct {
2828
// `resourceManager` struct that will set fields on a `resource` struct
2929
// depending on the output of the operation.
3030
SetOutputCustomMethodName string `json:"set_output_custom_method_name,omitempty"`
31+
// OutputWrapperFieldPath provides the JSON-Path like to the struct field containing
32+
// information that will be merged into a `resource` object.
33+
OutputWrapperFieldPath string `json:"output_wrapper_field_path,omitempty"`
3134
// Override for resource name in case of heuristic failure
3235
// An example of this is correcting stutter when the resource logic doesn't properly determine the resource name
3336
ResourceName string `json:"resource_name"`

pkg/generate/testdata/models/apis/dynamodb/0000-00-00/generator.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,12 @@ resources:
44
errors:
55
404:
66
code: ResourceNotFoundException
7+
operations:
8+
DescribeBackup:
9+
# DescribeBackupOutput is an unsual shape because it contains information for
10+
# the backup it self (BackupDetails), the table details when the backup was
11+
# created (SourceTableDetails) and the table features (SourceTableFeatureDetails).
12+
# If not specified the code generator will try to determine the wrapper field by
13+
# selecting for the output shape that only have a single member, which is incorrect
14+
# in this case.
15+
output_wrapper_field_path: BackupDescription.BackupDetails

pkg/model/crd.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,28 @@ func (r *CRD) SetOutputCustomMethodName(
352352
return &resGenConfig.SetOutputCustomMethodName
353353
}
354354

355+
// GetOutputWrapperFieldPath returns the JSON-Path of the output wrapper field
356+
// as *string for a given operation, if specified in generator config.
357+
func (r *CRD) GetOutputWrapperFieldPath(
358+
op *awssdkmodel.Operation,
359+
) *string {
360+
if op == nil {
361+
return nil
362+
}
363+
if r.cfg == nil {
364+
return nil
365+
}
366+
opConfig, found := r.cfg.Operations[op.Name]
367+
if !found {
368+
return nil
369+
}
370+
371+
if opConfig.OutputWrapperFieldPath == "" {
372+
return nil
373+
}
374+
return &opConfig.OutputWrapperFieldPath
375+
}
376+
355377
// GetCustomImplementation returns custom implementation method name for the
356378
// supplied operation as specified in generator config
357379
func (r *CRD) GetCustomImplementation(

0 commit comments

Comments
 (0)