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

chore(spec2cdk): generate ICfnResource interface #27681

Merged
merged 11 commits into from
Nov 2, 2023
1 change: 1 addition & 0 deletions packages/awslint/lib/rules/core-types.ts
Original file line number Diff line number Diff line change
@@ -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'));
8 changes: 6 additions & 2 deletions tools/@aws-cdk/spec2cdk/lib/cdk/ast.ts
Original file line number Diff line number Diff line change
@@ -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('::');
@@ -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();
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
@@ -20,6 +20,7 @@ import {
Stability,
ObjectLiteral,
Module,
InterfaceType,
} from '@cdklabs/typewriter';
import { CDK_CORE, CONSTRUCTS } from './cdk';
import { CloudFormationMapping } from './cloudformation-mapping';
@@ -33,6 +34,7 @@ import {
cfnProducerNameFromType,
propStructNameFromResource,
staticRequiredTransform,
interfaceNameFromResource,
} from '../naming';
import { splitDocumentation } from '../util';

@@ -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, {
@@ -85,7 +109,7 @@ export class ResourceClass extends ClassType {
});

this.converter = TypeConverter.forResource({
db: db,
db: this.db,
resource: this.resource,
resourceClass: this,
});
@@ -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)) {
kaizencc marked this conversation as resolved.
Show resolved Hide resolved
this.resourceInterface.addProperty({
...this.decider.arn,
immutable: true,
});
}

// Build the members of this class
this.addProperty({
name: staticResourceTypeName(),
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
@@ -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>();
@@ -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')),
kaizencc marked this conversation as resolved.
Show resolved Hide resolved
},
});
}
}
}

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) {
4 changes: 4 additions & 0 deletions tools/@aws-cdk/spec2cdk/lib/naming/conventions.ts
Original file line number Diff line number Diff line change
@@ -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`;
}
Loading