Skip to content

Commit

Permalink
chore(spec2cdk): generate ICfnResource interface (#27681)
Browse files Browse the repository at this point in the history
Generates something that looks like this for all resources:

```ts
interface ICfnResource {
  readonly attrName: string;    // { Ref }, i.e. the primaryIdentifier
  readonly attrArn: string;     // { Fn::GetAtt }, if applicable
}
```

In a separate PR, we will update all `CfnResource`s to extend
`ICfnResource`, and then later on, update some `Resource`s to extend
`ICfnResource` as well.

**This PR targets `conroy/generate` as a horribly named branch off of
`main` which will house all pieces of this puzzle before unleashing onto
`main`.**


----

*By submitting this pull request, I confirm that my contribution is made
under the terms of the Apache-2.0 license*

---------

Co-authored-by: Momo Kornher <kornherm@amazon.co.uk>
  • Loading branch information
kaizencc and mrgrain committed Nov 2, 2023
1 parent c096aa7 commit b2da9db
Show file tree
Hide file tree
Showing 7 changed files with 1,419 additions and 11 deletions.
1 change: 1 addition & 0 deletions packages/awslint/lib/rules/core-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export class CoreTypes {
*/
public static isCfnType(interfaceType: reflect.Type) {
return interfaceType.name.startsWith('Cfn')
|| interfaceType.name.startsWith('ICfn')
|| (interfaceType.namespace && interfaceType.namespace.startsWith('Cfn'))
// aws_service.CfnTheResource.SubType
|| (interfaceType.namespace && interfaceType.namespace.split('.', 2).at(1)?.startsWith('Cfn'));
Expand Down
8 changes: 6 additions & 2 deletions tools/@aws-cdk/spec2cdk/lib/cdk/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export class AstBuilder<T extends Module> {
}

/**
* Build an module for a single resource
* Build a module for a single resource
*/
public static forResource(resource: Resource, props: AstBuilderProps): AstBuilder<ResourceModule> {
const parts = resource.cloudFormationType.toLowerCase().split('::');
Expand Down Expand Up @@ -93,7 +93,11 @@ export class AstBuilder<T extends Module> {
}

public addResource(resource: Resource) {
const resourceClass = new ResourceClass(this.module, this.db, resource, this.nameSuffix);
const resourceClass = new ResourceClass(this.module, {
db: this.db,
resource,
suffix: this.nameSuffix,
});
this.resources[resource.cloudFormationType] = resourceClass.spec.name;

resourceClass.build();
Expand Down
58 changes: 49 additions & 9 deletions tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
Stability,
ObjectLiteral,
Module,
InterfaceType,
} from '@cdklabs/typewriter';
import { CDK_CORE, CONSTRUCTS } from './cdk';
import { CloudFormationMapping } from './cloudformation-mapping';
Expand All @@ -33,6 +34,7 @@ import {
cfnProducerNameFromType,
propStructNameFromResource,
staticRequiredTransform,
interfaceNameFromResource,
} from '../naming';
import { splitDocumentation } from '../util';

Expand All @@ -43,33 +45,55 @@ export interface ITypeHost {
// This convenience typewriter builder is used all over the place
const $this = $E(expr.this_());

export interface ResourceClassProps {
readonly db: SpecDatabase;
readonly resource: Resource;
readonly suffix?: string;
}

export class ResourceClass extends ClassType {
private readonly db: SpecDatabase;
private readonly resource: Resource;
private readonly propsType: StructType;
private readonly resourceInterface: InterfaceType;
private readonly decider: ResourceDecider;
private readonly converter: TypeConverter;
private readonly module: Module;
private readonly suffix?: string;

constructor(
scope: IScope,
private readonly db: SpecDatabase,
private readonly resource: Resource,
private readonly suffix?: string,
props: ResourceClassProps,
) {
const resourceInterface = new InterfaceType(scope, {
export: true,
name: interfaceNameFromResource(props.resource, props.suffix),
docs: {
summary: `Attributes for \`${classNameFromResource(props.resource)}\`.`,
stability: Stability.External,
},
});

super(scope, {
export: true,
name: classNameFromResource(resource, suffix),
name: classNameFromResource(props.resource, props.suffix),
docs: {
...splitDocumentation(resource.documentation),
...splitDocumentation(props.resource.documentation),
stability: Stability.External,
docTags: { cloudformationResource: resource.cloudFormationType },
docTags: { cloudformationResource: props.resource.cloudFormationType },
see: cloudFormationDocLink({
resourceType: resource.cloudFormationType,
resourceType: props.resource.cloudFormationType,
}),
},
extends: CDK_CORE.CfnResource,
implements: [CDK_CORE.IInspectable, ...ResourceDecider.taggabilityInterfaces(resource)],
implements: [CDK_CORE.IInspectable, ...ResourceDecider.taggabilityInterfaces(props.resource)],
});

this.db = props.db;
this.resource = props.resource;
this.resourceInterface = resourceInterface;
this.suffix = props.suffix;

this.module = Module.of(this);

this.propsType = new StructType(this.scope, {
Expand All @@ -85,7 +109,7 @@ export class ResourceClass extends ClassType {
});

this.converter = TypeConverter.forResource({
db: db,
db: this.db,
resource: this.resource,
resourceClass: this,
});
Expand All @@ -105,6 +129,22 @@ export class ResourceClass extends ClassType {
cfnMapping.add(prop.cfnMapping);
}

// Build the shared interface
for (const identifier of this.decider.primaryIdentifier ?? []) {
this.resourceInterface.addProperty({
...identifier,
immutable: true,
});
}

// Add the arn too, unless it is duplicated in the resourceIdentifier already
if (this.decider.arn && this.resourceInterface.properties.every((p) => p.name !== this.decider.arn!.name)) {
this.resourceInterface.addProperty({
...this.decider.arn,
immutable: true,
});
}

// Build the members of this class
this.addProperty({
name: staticResourceTypeName(),
Expand Down
58 changes: 58 additions & 0 deletions tools/@aws-cdk/spec2cdk/lib/cdk/resource-decider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export class ResourceDecider {

private readonly taggability?: TaggabilityStyle;

/**
* The arn returned by the resource, if applicable.
*/
public readonly arn?: PropertySpec;
public readonly primaryIdentifier = new Array<PropertySpec>();
public readonly propsProperties = new Array<PropsProperty>();
public readonly classProperties = new Array<ClassProperty>();
public readonly classAttributeProperties = new Array<ClassAttributeProperty>();
Expand All @@ -38,11 +43,64 @@ export class ResourceDecider {
this.convertProperties();
this.convertAttributes();

// must be called after convertProperties and convertAttributes
this.convertPrimaryIdentifier();
this.arn = this.findArn();

this.propsProperties.sort((p1, p2) => p1.propertySpec.name.localeCompare(p2.propertySpec.name));
this.classProperties.sort((p1, p2) => p1.propertySpec.name.localeCompare(p2.propertySpec.name));
this.classAttributeProperties.sort((p1, p2) => p1.propertySpec.name.localeCompare(p2.propertySpec.name));
}

private findArn() {
// A list of possible names for the arn, in order of importance.
// This is relevant because some resources, like AWS::VpcLattice::AccessLogSubscription
// has both `Arn` and `ResourceArn`, and we want to select the `Arn` property.
const possibleArnNames = ['Arn', 'ResourceArn', `${this.resource.name}Arn`];
for (const arn of possibleArnNames) {
const att = this.classAttributeProperties.filter((a) => a.propertySpec.name === attributePropertyName(arn));
const prop = this.propsProperties.filter((p) => p.propertySpec.name === propertyNameFromCloudFormation(arn));
if (att.length > 0 || prop.length > 0) {
return att[0] ? att[0].propertySpec : prop[0].propertySpec;
}
}
return;
}

private convertPrimaryIdentifier() {
for (const cfnName of this.resource.primaryIdentifier ?? []) {
const att = this.findAttributeByName(attributePropertyName(cfnName));
const prop = this.findPropertyByName(propertyNameFromCloudFormation(cfnName));
if (att) {
this.primaryIdentifier.push(att);
} else if (prop) {
// rename the prop name as an attribute name, since it is gettable by ref
this.primaryIdentifier.push({
...prop,
name: attributePropertyName(prop.name[0].toUpperCase() + prop.name.slice(1)),
docs: {
...prop.docs,
remarks: prop.docs?.remarks?.concat(['\n', `@cloudformationRef ${prop.name}`].join('\n')),
},
});
}
}
}

private findPropertyByName(name: string): PropertySpec | undefined {
const props = this.propsProperties.filter((prop) => prop.propertySpec.name === name);
// there's no way we have multiple properties with the same name
if (props.length > 0) { return props[0].propertySpec; }
return;
}

private findAttributeByName(name: string): PropertySpec | undefined {
const atts = this.classAttributeProperties.filter((att) => att.propertySpec.name === name);
// there's no way we have multiple attributes with the same name
if (atts.length > 0) { return atts[0].propertySpec; }
return;
}

private convertProperties() {
for (const [name, prop] of Object.entries(this.resource.properties)) {
if (name === this.taggability?.tagPropertyName) {
Expand Down
4 changes: 4 additions & 0 deletions tools/@aws-cdk/spec2cdk/lib/naming/conventions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export function propStructNameFromResource(res: Resource, suffix?: string) {
return `${classNameFromResource(res, suffix)}Props`;
}

export function interfaceNameFromResource(res: Resource, suffix?: string) {
return `I${classNameFromResource(res, suffix)}`;
}

export function cfnProducerNameFromType(struct: TypeDeclaration) {
return `convert${qualifiedName(struct)}ToCloudFormation`;
}
Expand Down
Loading

0 comments on commit b2da9db

Please sign in to comment.