Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support of injecting fields in existing structures: CustomNestedFields #462

Merged
merged 5 commits into from
Sep 27, 2023
Merged
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
.idea
/docs/site
bin
build
build
81 changes: 81 additions & 0 deletions pkg/model/crd.go
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,87 @@ func (r *CRD) addAdditionalPrinterColumns(additionalColumns []*ackgenconfig.Addi
}
}

// checkSpecOrStatus checks whether the new nested field is for Spec or Status struct
// and returns the top level field accordingly
func (crd *CRD) checkSpecOrStatus(
field string,
) (*Field, bool) {
fSpec, okSpec := crd.SpecFields[field]
if okSpec {
return fSpec, okSpec
}
fStatus, okStatus := crd.StatusFields[field]
if okStatus {
return fStatus, okStatus
}
return nil, false
}

// addCustomNestedFields injects the provided customNestedFields into the
// Spec or Status struct as a nested field. The customNestedFields are
// identified by the field path. The field path is a dot separated string
// that represents the path to the nested field. For example, if we want to
// inject a field called "Password" into the "User" struct, the field path
// would be "User.Password". The field path can be as deep as needed.
func (crd *CRD) addCustomNestedFields(customNestedFields map[string]*ackgenconfig.FieldConfig) {
// We have collected all the nested fields in `customNestedFields` map and now we can process
// and validate that they indeed are inject-able (i.e. the parent field is of type struct)
// and inject them into the Spec or Status struct.
for customNestedField, customNestedFieldConfig := range customNestedFields {
fieldParts := strings.Split(customNestedField, ".")
// we know that the length of fieldParts is at least 2
// it is safe to access the first element.
topLevelField := fieldParts[0]

f, ok := crd.checkSpecOrStatus(topLevelField)

if ok && f.ShapeRef.Shape.Type != "structure" {
// We need to panic here because the user is providing wrong configuration.
msg := fmt.Sprintf("Expected parent field to be of type structure, but found %s", f.ShapeRef.Shape.Type)
panic(msg)
}

// If the provided top level field is not in the crd.SpecFields or crd.StatusFields...
if !ok {
// We need to panic here because the user is providing wrong configuration.
msg := fmt.Sprintf("Expected top level field %s to be present in Spec or Status", topLevelField)
panic(msg)
}

// We will have to keep track of the previous field in the path
// to check it's member fields.
parentField := f

// loop over the all left fieldParts except the last one
for _, currentFieldName := range fieldParts[1 : len(fieldParts)-1] {
// Check if parentField contains current field
currentField, ok := parentField.MemberFields[currentFieldName]
if !ok || currentField.ShapeRef.Shape.Type != "structure" {
// Check if the field exists AND is of type structure
msg := fmt.Sprintf("Cannot inject field, %s member doesn't exist or isn't a structure", currentFieldName)
panic(msg)
}
parentField = currentField
}

// arriving here means that successfully walked the path and
// parentField is the parent of the new field.

// the last part is the field name
fieldName := fieldParts[len(fieldParts)-1]
typeOverride := customNestedFieldConfig.Type
shapeRef := crd.sdkAPI.GetShapeRefFromType(*typeOverride)

// Create a new field with the provided field name and shapeRef
newCustomNestedField := NewField(crd, fieldName, names.New(fieldName), shapeRef, customNestedFieldConfig)

// Add the new field to the parentField
parentField.MemberFields[fieldName] = newCustomNestedField
// Add the new field to the parentField's shapeRef
parentField.ShapeRef.Shape.MemberRefs[fieldName] = crd.sdkAPI.GetShapeRefFromType(*customNestedFieldConfig.Type)
}
}

// ReconcileRequeuOnSuccessSeconds returns the duration after which to requeue
// the custom resource as int
func (r *CRD) ReconcileRequeuOnSuccessSeconds() int {
Expand Down
36 changes: 33 additions & 3 deletions pkg/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,16 @@ func (m *Model) GetCRDs() ([]*CRD, error) {
crd.AddSpecField(memberNames, memberShapeRef)
}

// Now any additional Spec fields that are required from other API
// operations.
// A list of fields that should be processed after gathering
// the Spec and Status top level fields. The customNestedFields will be
// injected into the Spec or Status struct as a nested field.
//
// Note that we could reuse the Field struct here, but we don't because
// we don't need all the fields that the Field struct provides. We only
// need the field path and the FieldConfig. Using Field could lead to
// confusion.
customNestedFields := make(map[string]*ackgenconfig.FieldConfig)

for targetFieldName, fieldConfig := range m.cfg.GetFieldConfigs(crdName) {
if fieldConfig.IsReadOnly {
// It's a Status field...
Expand Down Expand Up @@ -176,6 +184,16 @@ func (m *Model) GetCRDs() ([]*CRD, error) {
panic(msg)
}
} else if fieldConfig.Type != nil {
// A nested field will always have a "." in the field path.
// Let's collect those fields and process them after we've
// gathered all the top level fields.
if strings.Contains(targetFieldName, ".") {
// This is a nested field
customNestedFields[targetFieldName] = fieldConfig
continue
}
// If we're here, we have a custom top level field (non-nested).

// We have a custom field that has a type override and has not
// been inferred via the normal Create Input shape or via the
// SourceFieldConfig. Manually construct the field and its
Expand All @@ -189,6 +207,7 @@ func (m *Model) GetCRDs() ([]*CRD, error) {

memberNames := names.New(targetFieldName)
crd.AddSpecField(memberNames, memberShapeRef)

}

// Now process the fields that will go into the Status struct. We want
Expand Down Expand Up @@ -278,6 +297,16 @@ func (m *Model) GetCRDs() ([]*CRD, error) {
panic(msg)
}
} else if fieldConfig.Type != nil {
// A nested field will always have a "." in the field path.
// Let's collect those fields and process them after we've
// gathered all the top level fields.
if strings.Contains(targetFieldName, ".") {
// This is a nested field
customNestedFields[targetFieldName] = fieldConfig
continue
}
// If we're here, we have a custom top level field (non-nested).

// We have a custom field that has a type override and has not
// been inferred via the normal Create Input shape or via the
// SourceFieldConfig. Manually construct the field and its
Expand All @@ -296,7 +325,8 @@ func (m *Model) GetCRDs() ([]*CRD, error) {
// Now add the additional printer columns that have been defined explicitly
// in additional_columns
crd.addAdditionalPrinterColumns(m.cfg.GetAdditionalColumns(crdName))

// Process the custom nested fields
crd.addCustomNestedFields(customNestedFields)
crds = append(crds, crd)
}
sort.Slice(crds, func(i, j int) bool {
Expand Down
75 changes: 75 additions & 0 deletions pkg/model/model_lambda_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,78 @@ func TestLambda_Function(t *testing.T) {
}
assert.Equal(expStatusFieldCamel, attrCamelNames(statusFields))
}

func TestLambda_customNestedFields_Spec_Depth2(t *testing.T) {
// This test is to check if a custom field
// defined as a nestedField using `type:`,
// is nested properly inside its parentField

assert := assert.New(t)
require := require.New(t)

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

crds, err := g.GetCRDs()
require.Nil(err)

crd := getCRDByName("Function", crds)
require.NotNil(crd)

assert.Contains(crd.SpecFields, "Code")
codeField := crd.SpecFields["Code"]

// Check if Nested Field is inside its Parent Field
assert.Contains(codeField.MemberFields, "S3SHA256")
assert.Contains(codeField.ShapeRef.Shape.MemberRefs, "S3SHA256")
Comment on lines +129 to +130
Copy link
Member

Choose a reason for hiding this comment

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

Good testing!

}
func TestLambda_customNestedFields_Spec_Depth3(t *testing.T) {
// This test is to check if a custom field
// defined as a nestedField using `type:`,
// is nested properly inside its parentField

assert := assert.New(t)
require := require.New(t)

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

crds, err := g.GetCRDs()
require.Nil(err)

crd := getCRDByName("EventSourceMapping", crds)
require.NotNil(crd)

assert.Contains(crd.SpecFields, "DestinationConfig")
OnSuccessField := crd.SpecFields["DestinationConfig"].MemberFields["OnSuccess"]

assert.Contains(OnSuccessField.MemberFields, "New")
assert.Contains(OnSuccessField.ShapeRef.Shape.MemberRefs, "New")
}

func TestLambda_customNestedFields_Status_Depth3(t *testing.T) {
// This test is to check if a custom field
// defined as a nestedField using `type:`,
// is nested properly inside its parentField

assert := assert.New(t)
require := require.New(t)

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

crds, err := g.GetCRDs()
require.Nil(err)

crd := getCRDByName("Function", crds)
require.NotNil(crd)

assert.Contains(crd.StatusFields, "ImageConfigResponse")
ErrorField := crd.StatusFields["ImageConfigResponse"].MemberFields["Error"]

assert.Contains(ErrorField.MemberFields, "New")
assert.Contains(ErrorField.ShapeRef.Shape.MemberRefs, "New")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
resources:
Function:
synced:
when:
- path: Status.State
in: [ "Active" ]
fields:
Code.S3SHA256:
type: string
compare:
is_ignored: true
ImageConfigResponse.Error.New:
is_read_only: true
type: string
EventSourceMapping:
fields:
DestinationConfig.OnSuccess.New:
type: string
compare:
is_ignored: true