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
2 changes: 1 addition & 1 deletion 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
17 changes: 17 additions & 0 deletions tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
cfnProducerNameFromType,
propStructNameFromResource,
staticRequiredTransform,
interfaceNameFromResource,
} from '../naming';
import { splitDocumentation } from '../util';

Expand All @@ -45,6 +46,7 @@ const $this = $E(expr.this_());

export class ResourceClass extends ClassType {
private readonly propsType: StructType;
private readonly interface: StructType;
private readonly decider: ResourceDecider;
private readonly converter: TypeConverter;
private readonly module: Module;
Expand Down Expand Up @@ -72,6 +74,15 @@ export class ResourceClass extends ClassType {

this.module = Module.of(this);

this.interface = new StructType(this.scope, {
export: true,
name: interfaceNameFromResource(this.resource, this.suffix),
docs: {
summary: `Shared attributes for both \`${classNameFromResource(this.resource)}\` and \`${this.resource.name}\`.`,
stability: Stability.External,
},
});

this.propsType = new StructType(this.scope, {
export: true,
name: propStructNameFromResource(this.resource, this.suffix),
Expand Down Expand Up @@ -105,6 +116,12 @@ export class ResourceClass extends ClassType {
cfnMapping.add(prop.cfnMapping);
}

// Build the shared interface
for (const identifier of this.decider.primaryIdentifier ?? []) {
this.interface.addProperty(identifier);
// cfnMapping.add(identifier.cfnMapping); // might not be needed because it duplicates the same line of propsProperties
}

// Build the members of this class
this.addProperty({
name: staticResourceTypeName(),
Expand Down
36 changes: 36 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,7 @@ export class ResourceDecider {

private readonly taggability?: TaggabilityStyle;

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 @@ -37,12 +38,47 @@ export class ResourceDecider {

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

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 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) {
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
103 changes: 103 additions & 0 deletions tools/@aws-cdk/spec2cdk/test/services.test.ts
kaizencc marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,106 @@ test('can codegen service with arbitrary suffix', () => {
expect(rendered).toContain('function convertCfnApplicationV2PropsToCloudFormation');
expect(rendered).toContain('function CfnApplicationV2ApplicationCodeConfigurationPropertyValidator');
});

test('resource interface when primaryIdentifier is an attribute', () => {
const service = db.lookup('service', 'name', 'equals', 'aws-voiceid').only();

const ast = AstBuilder.forService(service, { db });

const rendered = renderer.render(ast.module);

expect(rendered).toContain([
'/**',
' * Shared attributes for both `CfnDomain` and `Domain`.',
kaizencc marked this conversation as resolved.
Show resolved Hide resolved
' *',
' * @struct',
' * @stability external',
' */',
'export interface ICfnDomain {',
' /**',
' * The identifier of the domain.',
' *',
' * @cloudformationAttribute DomainId',
' */',
' readonly attrDomainId: string;',
'}',
].join('\n'));
});

test('resource interface when primaryIdentifier is a property', () => {
const service = db.lookup('service', 'name', 'equals', 'aws-kinesisanalyticsv2').only();

const ast = AstBuilder.forService(service, { db });

const rendered = renderer.render(ast.module);

expect(rendered).toContain([
'/**',
' * Shared attributes for both `CfnApplication` and `Application`.',
' *',
' * @struct',
' * @stability external',
' */',
'export interface ICfnApplication {',
' /**',
' * The name of the application.',
' *',
' * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kinesisanalyticsv2-application.html#cfn-kinesisanalyticsv2-application-applicationname',
' */',
' readonly attrApplicationName?: string;', // optional?
'}',
].join('\n'));
});

test('resource interface with multiple primaryIdentifiers', () => {
const service = db.lookup('service', 'name', 'equals', 'aws-lakeformation').only();

const ast = AstBuilder.forService(service, { db });

const rendered = renderer.render(ast.module);

expect(rendered).toContain([
'/**',
' * Shared attributes for both `CfnDataCellsFilter` and `DataCellsFilter`.',
' *',
' * @struct',
kaizencc marked this conversation as resolved.
Show resolved Hide resolved
' * @stability external',
' */',
'export interface ICfnDataCellsFilter {',
' /**',
' * Catalog id string, not less than 1 or more than 255 bytes long, matching the [single-line string pattern](https://docs.aws.amazon.com/lake-formation/latest/dg/aws-lake-formation-api-aws-lake-formation-api-common.html) .',
' *',
' * The ID of the catalog to which the table belongs.',
' *',
' * @cloudformationRef tableCatalogId',
' *',
' * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lakeformation-datacellsfilter.html#cfn-lakeformation-datacellsfilter-tablecatalogid',
' */',
' readonly attrTableCatalogId: string;',
// ' /**',
kaizencc marked this conversation as resolved.
Show resolved Hide resolved
// ' * UTF-8 string, not less than 1 or more than 255 bytes long, matching the [single-line string pattern](https://docs.aws.amazon.com/lake-formation/latest/dg/aws-lake-formation-api-aws-lake-formation-api-common.html) .',
// ' *',
// ' * A database in the Data Catalog .',
// ' *',
// ' * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lakeformation-datacellsfilter.html#cfn-lakeformation-datacellsfilter-databasename',
// ' */',
// ' readonly attrDatabaseName: string;',
// ' /**',
// ' * UTF-8 string, not less than 1 or more than 255 bytes long, matching the [single-line string pattern](https://docs.aws.amazon.com/lake-formation/latest/dg/aws-lake-formation-api-aws-lake-formation-api-common.html) .',
// ' *',
// ' * A table in the database.',
// ' *',
// ' * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lakeformation-datacellsfilter.html#cfn-lakeformation-datacellsfilter-tablename',
// ' */',
// ' readonly attrTableName: string;',
// ' /**',
// ' * UTF-8 string, not less than 1 or more than 255 bytes long, matching the [single-line string pattern](https://docs.aws.amazon.com/lake-formation/latest/dg/aws-lake-formation-api-aws-lake-formation-api-common.html) .',
// ' *',
// ' * The name given by the user to the data filter cell.',
// ' *',
// ' * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lakeformation-datacellsfilter.html#cfn-lakeformation-datacellsfilter-name',
// ' */',
// ' readonly attrName: string;',
// '}',
].join('\n'));
});
Loading