From 3b8838396923ee31b2abb2506f31db9a315c3e4b Mon Sep 17 00:00:00 2001 From: Kaizen Conroy Date: Wed, 25 Oct 2023 14:32:13 -0400 Subject: [PATCH 01/10] initial codegen dump --- tools/@aws-cdk/spec2cdk/lib/cdk/ast.ts | 2 +- .../spec2cdk/lib/cdk/resource-class.ts | 17 +++ .../spec2cdk/lib/cdk/resource-decider.ts | 36 ++++++ .../spec2cdk/lib/naming/conventions.ts | 4 + tools/@aws-cdk/spec2cdk/test/services.test.ts | 103 ++++++++++++++++++ 5 files changed, 161 insertions(+), 1 deletion(-) diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/ast.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/ast.ts index 654c99e9c6d3e..739c13ad2aac9 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/ast.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/ast.ts @@ -57,7 +57,7 @@ export class AstBuilder { } /** - * Build an module for a single resource + * Build a module for a single resource */ public static forResource(resource: Resource, props: AstBuilderProps): AstBuilder { const parts = resource.cloudFormationType.toLowerCase().split('::'); diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts index d31ae552f096e..ba54b7ed47119 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts @@ -33,6 +33,7 @@ import { cfnProducerNameFromType, propStructNameFromResource, staticRequiredTransform, + interfaceNameFromResource, } from '../naming'; import { splitDocumentation } from '../util'; @@ -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; @@ -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), @@ -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(), diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-decider.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-decider.ts index d61bf118d299a..34449bbcb7c3d 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-decider.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-decider.ts @@ -28,6 +28,7 @@ export class ResourceDecider { private readonly taggability?: TaggabilityStyle; + public readonly primaryIdentifier = new Array(); public readonly propsProperties = new Array(); public readonly classProperties = new Array(); public readonly classAttributeProperties = new Array(); @@ -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')), + }, + }); + } + } + } + + 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) { diff --git a/tools/@aws-cdk/spec2cdk/lib/naming/conventions.ts b/tools/@aws-cdk/spec2cdk/lib/naming/conventions.ts index be7a7f7336ba7..a22926e56cb66 100644 --- a/tools/@aws-cdk/spec2cdk/lib/naming/conventions.ts +++ b/tools/@aws-cdk/spec2cdk/lib/naming/conventions.ts @@ -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`; } diff --git a/tools/@aws-cdk/spec2cdk/test/services.test.ts b/tools/@aws-cdk/spec2cdk/test/services.test.ts index 677a0cdc62eea..4eb7b1f8e97c2 100644 --- a/tools/@aws-cdk/spec2cdk/test/services.test.ts +++ b/tools/@aws-cdk/spec2cdk/test/services.test.ts @@ -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`.', + ' *', + ' * @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', + ' * @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;', + // ' /**', + // ' * 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')); +}); From 6bd21d9d1a224de096d86b8f1741926624215522 Mon Sep 17 00:00:00 2001 From: Kaizen Conroy Date: Wed, 25 Oct 2023 15:49:08 -0400 Subject: [PATCH 02/10] arn attribute also --- .../spec2cdk/lib/cdk/resource-class.ts | 4 +- .../spec2cdk/lib/cdk/resource-decider.ts | 23 ++++++++++- tools/@aws-cdk/spec2cdk/test/services.test.ts | 41 +++++++++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts index ba54b7ed47119..55eaad9cf2449 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts @@ -119,7 +119,9 @@ export class ResourceClass extends ClassType { // 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 + } + if (this.decider.arn) { + this.interface.addProperty(this.decider.arn); } // Build the members of this class diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-decider.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-decider.ts index 34449bbcb7c3d..2a07a25ce246e 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-decider.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-decider.ts @@ -28,6 +28,10 @@ export class ResourceDecider { private readonly taggability?: TaggabilityStyle; + /** + * The arn returned by the resource, if applicable. + */ + public readonly arn?: PropertySpec; public readonly primaryIdentifier = new Array(); public readonly propsProperties = new Array(); public readonly classProperties = new Array(); @@ -38,13 +42,30 @@ export class ResourceDecider { this.convertProperties(); this.convertAttributes(); - this.convertPrimaryIdentifier(); // must be called after convertProperties and 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)); } + // TODO: uh, make this not a loop of loops haha + private findArn() { + // a list of possible names for the arn, in order of most likely to least likely + 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)); diff --git a/tools/@aws-cdk/spec2cdk/test/services.test.ts b/tools/@aws-cdk/spec2cdk/test/services.test.ts index 4eb7b1f8e97c2..6887912b11331 100644 --- a/tools/@aws-cdk/spec2cdk/test/services.test.ts +++ b/tools/@aws-cdk/spec2cdk/test/services.test.ts @@ -128,3 +128,44 @@ test('resource interface with multiple primaryIdentifiers', () => { // '}', ].join('\n')); }); + +test('resource interface with "Arn"', () => { + const service = db.lookup('service', 'name', 'equals', 'aws-s3').only(); + + const ast = AstBuilder.forService(service, { db }); + + const rendered = renderer.render(ast.module); + + expect(rendered).toContain([ + '/**', + ' * Shared attributes for both `CfnBucket` and `Bucket`.', + ' *', + ' * @struct', + ' * @stability external', + ' */', + 'export interface ICfnBucket {', + ' /**', + ' * A name for the bucket.', + ' *', + ' * If you don\'t specify a name, AWS CloudFormation generates a unique ID and uses that ID for the bucket name. The bucket name must contain only lowercase letters, numbers, periods (.), and dashes (-) and must follow [Amazon S3 bucket restrictions and limitations](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html) . For more information, see [Rules for naming Amazon S3 buckets](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html#bucketnamingrules) in the *Amazon S3 User Guide* .', + ' *', + ' * > If you specify a name, you can\'t perform updates that require replacement of this resource. You can perform updates that require no or some interruption. If you need to replace the resource, specify a new name.', + ' *', + ' * @cloudformationRef bucketName', + ' *', + ' * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3-bucket.html#cfn-s3-bucket-bucketname', + ' */', + ' readonly attrBucketName?: string;', + // ' /**', + // ' * Returns the Amazon Resource Name (ARN) of the specified bucket.', + // ' *', + // ' * Example: `arn:aws:s3:::DOC-EXAMPLE-BUCKET`', + // ' *', + // ' * @cloudformationAttribute Arn', + // ' */', + // ' readonly attrArn: string;', + // '}', + ].join('\n')); +}); + +// TODO: test with ResourceArn and Arn too \ No newline at end of file From d11b7cd9d1fa21be313548a41ecb19db8eb37d9e Mon Sep 17 00:00:00 2001 From: Kaizen Conroy Date: Thu, 26 Oct 2023 11:38:57 -0400 Subject: [PATCH 03/10] test resources not services --- .../@aws-cdk/spec2cdk/test/resources.test.ts | 155 ++++++++++++++++++ tools/@aws-cdk/spec2cdk/test/services.test.ts | 144 ---------------- 2 files changed, 155 insertions(+), 144 deletions(-) create mode 100644 tools/@aws-cdk/spec2cdk/test/resources.test.ts diff --git a/tools/@aws-cdk/spec2cdk/test/resources.test.ts b/tools/@aws-cdk/spec2cdk/test/resources.test.ts new file mode 100644 index 0000000000000..4407b099bbc5e --- /dev/null +++ b/tools/@aws-cdk/spec2cdk/test/resources.test.ts @@ -0,0 +1,155 @@ +import { loadAwsServiceSpec } from '@aws-cdk/aws-service-spec'; +import { SpecDatabase } from '@aws-cdk/service-spec-types'; +import { TypeScriptRenderer } from '@cdklabs/typewriter'; +import { AstBuilder } from '../lib/cdk/ast'; + +const renderer = new TypeScriptRenderer(); +let db: SpecDatabase; + +beforeAll(async () => { + db = await loadAwsServiceSpec(); +}); + +test('resource interface when primaryIdentifier is an attribute', () => { + const resource = db.lookup('resource', 'cloudFormationType', 'equals', 'AWS::VoiceID::Domain').only(); + + const ast = AstBuilder.forResource(resource, { db }); + + const rendered = renderer.render(ast.module); + + expect(rendered).toContain([ + '/**', + ' * Shared attributes for both `CfnDomain` and `Domain`.', + ' *', + ' * @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 resource = db.lookup('resource', 'cloudFormationType', 'equals', 'AWS::KinesisAnalyticsV2::Application').only(); + + const ast = AstBuilder.forResource(resource, { 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 resource = db.lookup('resource', 'cloudFormationType', 'equals', 'AWS::LakeFormation::DataCellsFilter').only(); + + const ast = AstBuilder.forResource(resource, { db }); + + const rendered = renderer.render(ast.module); + + expect(rendered).toContain([ + '/**', + ' * Shared attributes for both `CfnDataCellsFilter` and `DataCellsFilter`.', + ' *', + ' * @struct', + ' * @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;', + // ' /**', + // ' * 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')); +}); + +test('resource interface with "Arn"', () => { + const resource = db.lookup('resource', 'cloudFormationType', 'equals', 'AWS::S3::Bucket').only(); + + const ast = AstBuilder.forResource(resource, { db }); + + const rendered = renderer.render(ast.module); + + expect(rendered).toContain([ + '/**', + ' * Shared attributes for both `CfnBucket` and `Bucket`.', + ' *', + ' * @struct', + ' * @stability external', + ' */', + 'export interface ICfnBucket {', + ' /**', + ' * A name for the bucket.', + ' *', + ' * If you don\'t specify a name, AWS CloudFormation generates a unique ID and uses that ID for the bucket name. The bucket name must contain only lowercase letters, numbers, periods (.), and dashes (-) and must follow [Amazon S3 bucket restrictions and limitations](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html) . For more information, see [Rules for naming Amazon S3 buckets](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html#bucketnamingrules) in the *Amazon S3 User Guide* .', + ' *', + ' * > If you specify a name, you can\'t perform updates that require replacement of this resource. You can perform updates that require no or some interruption. If you need to replace the resource, specify a new name.', + ' *', + ' * @cloudformationRef bucketName', + ' *', + ' * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3-bucket.html#cfn-s3-bucket-bucketname', + ' */', + ' readonly attrBucketName?: string;', + // ' /**', + // ' * Returns the Amazon Resource Name (ARN) of the specified bucket.', + // ' *', + // ' * Example: `arn:aws:s3:::DOC-EXAMPLE-BUCKET`', + // ' *', + // ' * @cloudformationAttribute Arn', + // ' */', + // ' readonly attrArn: string;', + // '}', + ].join('\n')); +}); + +// TODO: test with ResourceArn and Arn too \ No newline at end of file diff --git a/tools/@aws-cdk/spec2cdk/test/services.test.ts b/tools/@aws-cdk/spec2cdk/test/services.test.ts index 6887912b11331..677a0cdc62eea 100644 --- a/tools/@aws-cdk/spec2cdk/test/services.test.ts +++ b/tools/@aws-cdk/spec2cdk/test/services.test.ts @@ -25,147 +25,3 @@ 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`.', - ' *', - ' * @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', - ' * @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;', - // ' /**', - // ' * 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')); -}); - -test('resource interface with "Arn"', () => { - const service = db.lookup('service', 'name', 'equals', 'aws-s3').only(); - - const ast = AstBuilder.forService(service, { db }); - - const rendered = renderer.render(ast.module); - - expect(rendered).toContain([ - '/**', - ' * Shared attributes for both `CfnBucket` and `Bucket`.', - ' *', - ' * @struct', - ' * @stability external', - ' */', - 'export interface ICfnBucket {', - ' /**', - ' * A name for the bucket.', - ' *', - ' * If you don\'t specify a name, AWS CloudFormation generates a unique ID and uses that ID for the bucket name. The bucket name must contain only lowercase letters, numbers, periods (.), and dashes (-) and must follow [Amazon S3 bucket restrictions and limitations](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html) . For more information, see [Rules for naming Amazon S3 buckets](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html#bucketnamingrules) in the *Amazon S3 User Guide* .', - ' *', - ' * > If you specify a name, you can\'t perform updates that require replacement of this resource. You can perform updates that require no or some interruption. If you need to replace the resource, specify a new name.', - ' *', - ' * @cloudformationRef bucketName', - ' *', - ' * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3-bucket.html#cfn-s3-bucket-bucketname', - ' */', - ' readonly attrBucketName?: string;', - // ' /**', - // ' * Returns the Amazon Resource Name (ARN) of the specified bucket.', - // ' *', - // ' * Example: `arn:aws:s3:::DOC-EXAMPLE-BUCKET`', - // ' *', - // ' * @cloudformationAttribute Arn', - // ' */', - // ' readonly attrArn: string;', - // '}', - ].join('\n')); -}); - -// TODO: test with ResourceArn and Arn too \ No newline at end of file From b19811314a1b44d750265931d326524decd33648 Mon Sep 17 00:00:00 2001 From: Kaizen Conroy Date: Thu, 26 Oct 2023 13:09:29 -0400 Subject: [PATCH 04/10] add back readonly --- tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts | 15 +++++++++++---- tools/@aws-cdk/spec2cdk/test/resources.test.ts | 4 ---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts index 55eaad9cf2449..55aaaf5d8e0ef 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts @@ -20,6 +20,7 @@ import { Stability, ObjectLiteral, Module, + InterfaceType, } from '@cdklabs/typewriter'; import { CDK_CORE, CONSTRUCTS } from './cdk'; import { CloudFormationMapping } from './cloudformation-mapping'; @@ -46,7 +47,7 @@ const $this = $E(expr.this_()); export class ResourceClass extends ClassType { private readonly propsType: StructType; - private readonly interface: StructType; + private readonly interface: InterfaceType; private readonly decider: ResourceDecider; private readonly converter: TypeConverter; private readonly module: Module; @@ -74,7 +75,7 @@ export class ResourceClass extends ClassType { this.module = Module.of(this); - this.interface = new StructType(this.scope, { + this.interface = new InterfaceType(this.scope, { export: true, name: interfaceNameFromResource(this.resource, this.suffix), docs: { @@ -118,10 +119,16 @@ export class ResourceClass extends ClassType { // Build the shared interface for (const identifier of this.decider.primaryIdentifier ?? []) { - this.interface.addProperty(identifier); + this.interface.addProperty({ + ...identifier, + immutable: true, + }); } if (this.decider.arn) { - this.interface.addProperty(this.decider.arn); + this.interface.addProperty({ + ...this.decider.arn, + immutable: true, + }); } // Build the members of this class diff --git a/tools/@aws-cdk/spec2cdk/test/resources.test.ts b/tools/@aws-cdk/spec2cdk/test/resources.test.ts index 4407b099bbc5e..aa77d51a359cc 100644 --- a/tools/@aws-cdk/spec2cdk/test/resources.test.ts +++ b/tools/@aws-cdk/spec2cdk/test/resources.test.ts @@ -21,7 +21,6 @@ test('resource interface when primaryIdentifier is an attribute', () => { '/**', ' * Shared attributes for both `CfnDomain` and `Domain`.', ' *', - ' * @struct', ' * @stability external', ' */', 'export interface ICfnDomain {', @@ -46,7 +45,6 @@ test('resource interface when primaryIdentifier is a property', () => { '/**', ' * Shared attributes for both `CfnApplication` and `Application`.', ' *', - ' * @struct', ' * @stability external', ' */', 'export interface ICfnApplication {', @@ -71,7 +69,6 @@ test('resource interface with multiple primaryIdentifiers', () => { '/**', ' * Shared attributes for both `CfnDataCellsFilter` and `DataCellsFilter`.', ' *', - ' * @struct', ' * @stability external', ' */', 'export interface ICfnDataCellsFilter {', @@ -124,7 +121,6 @@ test('resource interface with "Arn"', () => { '/**', ' * Shared attributes for both `CfnBucket` and `Bucket`.', ' *', - ' * @struct', ' * @stability external', ' */', 'export interface ICfnBucket {', From e361b30a6004076f38cab7db887c3503855a13b8 Mon Sep 17 00:00:00 2001 From: Kaizen Conroy Date: Thu, 26 Oct 2023 13:12:23 -0400 Subject: [PATCH 05/10] rename documentaiton --- tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts | 2 +- tools/@aws-cdk/spec2cdk/test/resources.test.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts index 55aaaf5d8e0ef..b8157c43afbc2 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts @@ -79,7 +79,7 @@ export class ResourceClass extends ClassType { export: true, name: interfaceNameFromResource(this.resource, this.suffix), docs: { - summary: `Shared attributes for both \`${classNameFromResource(this.resource)}\` and \`${this.resource.name}\`.`, + summary: `Attributes for \`${classNameFromResource(this.resource)}\`.`, stability: Stability.External, }, }); diff --git a/tools/@aws-cdk/spec2cdk/test/resources.test.ts b/tools/@aws-cdk/spec2cdk/test/resources.test.ts index aa77d51a359cc..49049e2b78b68 100644 --- a/tools/@aws-cdk/spec2cdk/test/resources.test.ts +++ b/tools/@aws-cdk/spec2cdk/test/resources.test.ts @@ -19,7 +19,7 @@ test('resource interface when primaryIdentifier is an attribute', () => { expect(rendered).toContain([ '/**', - ' * Shared attributes for both `CfnDomain` and `Domain`.', + ' * Attributes for `CfnDomain`.', ' *', ' * @stability external', ' */', @@ -43,7 +43,7 @@ test('resource interface when primaryIdentifier is a property', () => { expect(rendered).toContain([ '/**', - ' * Shared attributes for both `CfnApplication` and `Application`.', + ' * Attributes for `CfnApplication`.', ' *', ' * @stability external', ' */', @@ -67,7 +67,7 @@ test('resource interface with multiple primaryIdentifiers', () => { expect(rendered).toContain([ '/**', - ' * Shared attributes for both `CfnDataCellsFilter` and `DataCellsFilter`.', + ' * Attributes for `CfnDataCellsFilter`.', ' *', ' * @stability external', ' */', @@ -119,7 +119,7 @@ test('resource interface with "Arn"', () => { expect(rendered).toContain([ '/**', - ' * Shared attributes for both `CfnBucket` and `Bucket`.', + ' * Attributes for `CfnBucket`.', ' *', ' * @stability external', ' */', From bc5971de11a1df7504295247165c8eb658656bbf Mon Sep 17 00:00:00 2001 From: Kaizen Conroy Date: Fri, 27 Oct 2023 18:14:33 -0400 Subject: [PATCH 06/10] tests with snappies --- .../test/__snapshots__/resources.test.ts.snap | 940 ++++++++++++++++++ .../@aws-cdk/spec2cdk/test/resources.test.ts | 301 +++--- 2 files changed, 1121 insertions(+), 120 deletions(-) create mode 100644 tools/@aws-cdk/spec2cdk/test/__snapshots__/resources.test.ts.snap diff --git a/tools/@aws-cdk/spec2cdk/test/__snapshots__/resources.test.ts.snap b/tools/@aws-cdk/spec2cdk/test/__snapshots__/resources.test.ts.snap new file mode 100644 index 0000000000000..f928d46ae9944 --- /dev/null +++ b/tools/@aws-cdk/spec2cdk/test/__snapshots__/resources.test.ts.snap @@ -0,0 +1,940 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`resource interface when primaryIdentifier is a property 1`] = ` +"/* eslint-disable prettier/prettier,max-len */ +import * as cdk from "aws-cdk-lib"; +import * as constructs from "constructs"; +import * as cfn_parse from "aws-cdk-lib/core/lib/helpers-internal"; + +/** + * @cloudformationResource AWS::Some::Resource + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-some-resource.html + */ +export class CfnResource extends cdk.CfnResource implements cdk.IInspectable { + /** + * The CloudFormation resource type name for this resource class. + */ + public static readonly CFN_RESOURCE_TYPE_NAME: string = "AWS::Some::Resource"; + + /** + * Build a CfnResource from CloudFormation properties + * + * A factory method that creates a new instance of this class from an object + * containing the CloudFormation properties of this resource. + * Used in the @aws-cdk/cloudformation-include module. + * + * @internal + */ + public static _fromCloudFormation(scope: constructs.Construct, id: string, resourceAttributes: any, options: cfn_parse.FromCloudFormationOptions): CfnResource { + resourceAttributes = resourceAttributes || {}; + const resourceProperties = options.parser.parseValue(resourceAttributes.Properties); + const propsResult = CfnResourcePropsFromCloudFormation(resourceProperties); + if (cdk.isResolvableObject(propsResult.value)) { + throw new Error("Unexpected IResolvable"); + } + const ret = new CfnResource(scope, id, propsResult.value); + for (const [propKey, propVal] of Object.entries(propsResult.extraProperties)) { + ret.addPropertyOverride(propKey, propVal); + } + options.parser.handleAttributes(ret, resourceAttributes, id); + return ret; + } + + /** + * The identifier of the resource. + */ + public id?: string; + + /** + * @param scope Scope in which this resource is defined + * @param id Construct identifier for this resource (unique in its scope) + * @param props Resource properties + */ + public constructor(scope: constructs.Construct, id: string, props: CfnResourceProps = {}) { + super(scope, id, { + "type": CfnResource.CFN_RESOURCE_TYPE_NAME, + "properties": props + }); + + this.id = props.id; + } + + protected get cfnProperties(): Record { + return { + "id": this.id + }; + } + + /** + * Examines the CloudFormation resource and discloses attributes + * + * @param inspector tree inspector to collect and process attributes + */ + public inspect(inspector: cdk.TreeInspector): void { + inspector.addAttribute("aws:cdk:cloudformation:type", CfnResource.CFN_RESOURCE_TYPE_NAME); + inspector.addAttribute("aws:cdk:cloudformation:props", this.cfnProperties); + } + + protected renderProperties(props: Record): Record { + return convertCfnResourcePropsToCloudFormation(props); + } +} + +/** + * Attributes for \`CfnResource\`. + * + * @stability external + */ +export interface ICfnResource { + /** + * The identifier of the resource. + * + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-some-resource.html#cfn-some-resource-id + */ + readonly attrId?: string; +} + +/** + * Properties for defining a \`CfnResource\` + * + * @struct + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-some-resource.html + */ +export interface CfnResourceProps { + /** + * The identifier of the resource. + * + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-some-resource.html#cfn-some-resource-id + */ + readonly id?: string; +} + +/** + * Determine whether the given properties match those of a \`CfnResourceProps\` + * + * @param properties - the TypeScript properties of a \`CfnResourceProps\` + * + * @returns the result of the validation. + */ +// @ts-ignore TS6133 +function CfnResourcePropsValidator(properties: any): cdk.ValidationResult { + if (!cdk.canInspect(properties)) return cdk.VALIDATION_SUCCESS; + const errors = new cdk.ValidationResults(); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + errors.collect(new cdk.ValidationResult("Expected an object, but received: " + JSON.stringify(properties))); + } + errors.collect(cdk.propertyValidator("id", cdk.validateString)(properties.id)); + return errors.wrap("supplied properties not correct for \\"CfnResourceProps\\""); +} + +// @ts-ignore TS6133 +function convertCfnResourcePropsToCloudFormation(properties: any): any { + if (!cdk.canInspect(properties)) return properties; + CfnResourcePropsValidator(properties).assertSuccess(); + return { + "Id": cdk.stringToCloudFormation(properties.id) + }; +} + +// @ts-ignore TS6133 +function CfnResourcePropsFromCloudFormation(properties: any): cfn_parse.FromCloudFormationResult { + if (cdk.isResolvableObject(properties)) { + return new cfn_parse.FromCloudFormationResult(properties); + } + properties = ((properties == null) ? {} : properties); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + return new cfn_parse.FromCloudFormationResult(properties); + } + const ret = new cfn_parse.FromCloudFormationPropertyObject(); + ret.addPropertyResult("id", "Id", (properties.Id != null ? cfn_parse.FromCloudFormation.getString(properties.Id) : undefined)); + ret.addUnrecognizedPropertiesAsExtra(properties); + return ret; +}" +`; + +exports[`resource interface when primaryIdentifier is an attribute 1`] = ` +"/* eslint-disable prettier/prettier,max-len */ +import * as cdk from "aws-cdk-lib"; +import * as constructs from "constructs"; +import * as cfn_parse from "aws-cdk-lib/core/lib/helpers-internal"; + +/** + * @cloudformationResource AWS::Some::Resource + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-some-resource.html + */ +export class CfnResource extends cdk.CfnResource implements cdk.IInspectable { + /** + * The CloudFormation resource type name for this resource class. + */ + public static readonly CFN_RESOURCE_TYPE_NAME: string = "AWS::Some::Resource"; + + /** + * Build a CfnResource from CloudFormation properties + * + * A factory method that creates a new instance of this class from an object + * containing the CloudFormation properties of this resource. + * Used in the @aws-cdk/cloudformation-include module. + * + * @internal + */ + public static _fromCloudFormation(scope: constructs.Construct, id: string, resourceAttributes: any, options: cfn_parse.FromCloudFormationOptions): CfnResource { + resourceAttributes = resourceAttributes || {}; + const resourceProperties = options.parser.parseValue(resourceAttributes.Properties); + const propsResult = CfnResourcePropsFromCloudFormation(resourceProperties); + if (cdk.isResolvableObject(propsResult.value)) { + throw new Error("Unexpected IResolvable"); + } + const ret = new CfnResource(scope, id, propsResult.value); + for (const [propKey, propVal] of Object.entries(propsResult.extraProperties)) { + ret.addPropertyOverride(propKey, propVal); + } + options.parser.handleAttributes(ret, resourceAttributes, id); + return ret; + } + + /** + * The identifier of the resource + * + * @cloudformationAttribute Id + */ + public readonly attrId: string; + + /** + * @param scope Scope in which this resource is defined + * @param id Construct identifier for this resource (unique in its scope) + * @param props Resource properties + */ + public constructor(scope: constructs.Construct, id: string, props: CfnResourceProps = {}) { + super(scope, id, { + "type": CfnResource.CFN_RESOURCE_TYPE_NAME, + "properties": props + }); + + this.attrId = cdk.Token.asString(this.getAtt("Id", cdk.ResolutionTypeHint.STRING)); + } + + protected get cfnProperties(): Record { + return {}; + } + + /** + * Examines the CloudFormation resource and discloses attributes + * + * @param inspector tree inspector to collect and process attributes + */ + public inspect(inspector: cdk.TreeInspector): void { + inspector.addAttribute("aws:cdk:cloudformation:type", CfnResource.CFN_RESOURCE_TYPE_NAME); + inspector.addAttribute("aws:cdk:cloudformation:props", this.cfnProperties); + } + + protected renderProperties(props: Record): Record { + return convertCfnResourcePropsToCloudFormation(props); + } +} + +/** + * Attributes for \`CfnResource\`. + * + * @stability external + */ +export interface ICfnResource { + /** + * The identifier of the resource + * + * @cloudformationAttribute Id + */ + readonly attrId: string; +} + +/** + * Properties for defining a \`CfnResource\` + * + * @struct + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-some-resource.html + */ +export interface CfnResourceProps { + +} + +/** + * Determine whether the given properties match those of a \`CfnResourceProps\` + * + * @param properties - the TypeScript properties of a \`CfnResourceProps\` + * + * @returns the result of the validation. + */ +// @ts-ignore TS6133 +function CfnResourcePropsValidator(properties: any): cdk.ValidationResult { + if (!cdk.canInspect(properties)) return cdk.VALIDATION_SUCCESS; + const errors = new cdk.ValidationResults(); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + errors.collect(new cdk.ValidationResult("Expected an object, but received: " + JSON.stringify(properties))); + } + return errors.wrap("supplied properties not correct for \\"CfnResourceProps\\""); +} + +// @ts-ignore TS6133 +function convertCfnResourcePropsToCloudFormation(properties: any): any { + if (!cdk.canInspect(properties)) return properties; + CfnResourcePropsValidator(properties).assertSuccess(); + return {}; +} + +// @ts-ignore TS6133 +function CfnResourcePropsFromCloudFormation(properties: any): cfn_parse.FromCloudFormationResult { + if (cdk.isResolvableObject(properties)) { + return new cfn_parse.FromCloudFormationResult(properties); + } + properties = ((properties == null) ? {} : properties); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + return new cfn_parse.FromCloudFormationResult(properties); + } + const ret = new cfn_parse.FromCloudFormationPropertyObject(); + ret.addUnrecognizedPropertiesAsExtra(properties); + return ret; +}" +`; + +exports[`resource interface with "Arn" 1`] = ` +"/* eslint-disable prettier/prettier,max-len */ +import * as cdk from "aws-cdk-lib"; +import * as constructs from "constructs"; +import * as cfn_parse from "aws-cdk-lib/core/lib/helpers-internal"; + +/** + * @cloudformationResource AWS::Some::Something + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-some-something.html + */ +export class CfnSomething extends cdk.CfnResource implements cdk.IInspectable { + /** + * The CloudFormation resource type name for this resource class. + */ + public static readonly CFN_RESOURCE_TYPE_NAME: string = "AWS::Some::Something"; + + /** + * Build a CfnSomething from CloudFormation properties + * + * A factory method that creates a new instance of this class from an object + * containing the CloudFormation properties of this resource. + * Used in the @aws-cdk/cloudformation-include module. + * + * @internal + */ + public static _fromCloudFormation(scope: constructs.Construct, id: string, resourceAttributes: any, options: cfn_parse.FromCloudFormationOptions): CfnSomething { + resourceAttributes = resourceAttributes || {}; + const resourceProperties = options.parser.parseValue(resourceAttributes.Properties); + const propsResult = CfnSomethingPropsFromCloudFormation(resourceProperties); + if (cdk.isResolvableObject(propsResult.value)) { + throw new Error("Unexpected IResolvable"); + } + const ret = new CfnSomething(scope, id, propsResult.value); + for (const [propKey, propVal] of Object.entries(propsResult.extraProperties)) { + ret.addPropertyOverride(propKey, propVal); + } + options.parser.handleAttributes(ret, resourceAttributes, id); + return ret; + } + + /** + * The identifier of the resource + * + * @cloudformationAttribute Id + */ + public readonly attrId: string; + + /** + * The arn for something + * + * @cloudformationAttribute SomethingArn + */ + public readonly attrSomethingArn: string; + + /** + * @param scope Scope in which this resource is defined + * @param id Construct identifier for this resource (unique in its scope) + * @param props Resource properties + */ + public constructor(scope: constructs.Construct, id: string, props: CfnSomethingProps = {}) { + super(scope, id, { + "type": CfnSomething.CFN_RESOURCE_TYPE_NAME, + "properties": props + }); + + this.attrId = cdk.Token.asString(this.getAtt("Id", cdk.ResolutionTypeHint.STRING)); + this.attrSomethingArn = cdk.Token.asString(this.getAtt("SomethingArn", cdk.ResolutionTypeHint.STRING)); + } + + protected get cfnProperties(): Record { + return {}; + } + + /** + * Examines the CloudFormation resource and discloses attributes + * + * @param inspector tree inspector to collect and process attributes + */ + public inspect(inspector: cdk.TreeInspector): void { + inspector.addAttribute("aws:cdk:cloudformation:type", CfnSomething.CFN_RESOURCE_TYPE_NAME); + inspector.addAttribute("aws:cdk:cloudformation:props", this.cfnProperties); + } + + protected renderProperties(props: Record): Record { + return convertCfnSomethingPropsToCloudFormation(props); + } +} + +/** + * Attributes for \`CfnSomething\`. + * + * @stability external + */ +export interface ICfnSomething { + /** + * The identifier of the resource + * + * @cloudformationAttribute Id + */ + readonly attrId: string; + + /** + * The arn for something + * + * @cloudformationAttribute SomethingArn + */ + readonly attrSomethingArn: string; +} + +/** + * Properties for defining a \`CfnSomething\` + * + * @struct + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-some-something.html + */ +export interface CfnSomethingProps { + +} + +/** + * Determine whether the given properties match those of a \`CfnSomethingProps\` + * + * @param properties - the TypeScript properties of a \`CfnSomethingProps\` + * + * @returns the result of the validation. + */ +// @ts-ignore TS6133 +function CfnSomethingPropsValidator(properties: any): cdk.ValidationResult { + if (!cdk.canInspect(properties)) return cdk.VALIDATION_SUCCESS; + const errors = new cdk.ValidationResults(); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + errors.collect(new cdk.ValidationResult("Expected an object, but received: " + JSON.stringify(properties))); + } + return errors.wrap("supplied properties not correct for \\"CfnSomethingProps\\""); +} + +// @ts-ignore TS6133 +function convertCfnSomethingPropsToCloudFormation(properties: any): any { + if (!cdk.canInspect(properties)) return properties; + CfnSomethingPropsValidator(properties).assertSuccess(); + return {}; +} + +// @ts-ignore TS6133 +function CfnSomethingPropsFromCloudFormation(properties: any): cfn_parse.FromCloudFormationResult { + if (cdk.isResolvableObject(properties)) { + return new cfn_parse.FromCloudFormationResult(properties); + } + properties = ((properties == null) ? {} : properties); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + return new cfn_parse.FromCloudFormationResult(properties); + } + const ret = new cfn_parse.FromCloudFormationPropertyObject(); + ret.addUnrecognizedPropertiesAsExtra(properties); + return ret; +}" +`; + +exports[`resource interface with "Arn" 1`] = ` +"/* eslint-disable prettier/prettier,max-len */ +import * as cdk from "aws-cdk-lib"; +import * as constructs from "constructs"; +import * as cfn_parse from "aws-cdk-lib/core/lib/helpers-internal"; + +/** + * @cloudformationResource AWS::Some::Resource + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-some-resource.html + */ +export class CfnResource extends cdk.CfnResource implements cdk.IInspectable { + /** + * The CloudFormation resource type name for this resource class. + */ + public static readonly CFN_RESOURCE_TYPE_NAME: string = "AWS::Some::Resource"; + + /** + * Build a CfnResource from CloudFormation properties + * + * A factory method that creates a new instance of this class from an object + * containing the CloudFormation properties of this resource. + * Used in the @aws-cdk/cloudformation-include module. + * + * @internal + */ + public static _fromCloudFormation(scope: constructs.Construct, id: string, resourceAttributes: any, options: cfn_parse.FromCloudFormationOptions): CfnResource { + resourceAttributes = resourceAttributes || {}; + const resourceProperties = options.parser.parseValue(resourceAttributes.Properties); + const propsResult = CfnResourcePropsFromCloudFormation(resourceProperties); + if (cdk.isResolvableObject(propsResult.value)) { + throw new Error("Unexpected IResolvable"); + } + const ret = new CfnResource(scope, id, propsResult.value); + for (const [propKey, propVal] of Object.entries(propsResult.extraProperties)) { + ret.addPropertyOverride(propKey, propVal); + } + options.parser.handleAttributes(ret, resourceAttributes, id); + return ret; + } + + /** + * The arn for the resource + * + * @cloudformationAttribute Arn + */ + public readonly attrArn: string; + + /** + * The identifier of the resource + * + * @cloudformationAttribute Id + */ + public readonly attrId: string; + + /** + * @param scope Scope in which this resource is defined + * @param id Construct identifier for this resource (unique in its scope) + * @param props Resource properties + */ + public constructor(scope: constructs.Construct, id: string, props: CfnResourceProps = {}) { + super(scope, id, { + "type": CfnResource.CFN_RESOURCE_TYPE_NAME, + "properties": props + }); + + this.attrArn = cdk.Token.asString(this.getAtt("Arn", cdk.ResolutionTypeHint.STRING)); + this.attrId = cdk.Token.asString(this.getAtt("Id", cdk.ResolutionTypeHint.STRING)); + } + + protected get cfnProperties(): Record { + return {}; + } + + /** + * Examines the CloudFormation resource and discloses attributes + * + * @param inspector tree inspector to collect and process attributes + */ + public inspect(inspector: cdk.TreeInspector): void { + inspector.addAttribute("aws:cdk:cloudformation:type", CfnResource.CFN_RESOURCE_TYPE_NAME); + inspector.addAttribute("aws:cdk:cloudformation:props", this.cfnProperties); + } + + protected renderProperties(props: Record): Record { + return convertCfnResourcePropsToCloudFormation(props); + } +} + +/** + * Attributes for \`CfnResource\`. + * + * @stability external + */ +export interface ICfnResource { + /** + * The identifier of the resource + * + * @cloudformationAttribute Id + */ + readonly attrId: string; + + /** + * The arn for the resource + * + * @cloudformationAttribute Arn + */ + readonly attrArn: string; +} + +/** + * Properties for defining a \`CfnResource\` + * + * @struct + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-some-resource.html + */ +export interface CfnResourceProps { + +} + +/** + * Determine whether the given properties match those of a \`CfnResourceProps\` + * + * @param properties - the TypeScript properties of a \`CfnResourceProps\` + * + * @returns the result of the validation. + */ +// @ts-ignore TS6133 +function CfnResourcePropsValidator(properties: any): cdk.ValidationResult { + if (!cdk.canInspect(properties)) return cdk.VALIDATION_SUCCESS; + const errors = new cdk.ValidationResults(); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + errors.collect(new cdk.ValidationResult("Expected an object, but received: " + JSON.stringify(properties))); + } + return errors.wrap("supplied properties not correct for \\"CfnResourceProps\\""); +} + +// @ts-ignore TS6133 +function convertCfnResourcePropsToCloudFormation(properties: any): any { + if (!cdk.canInspect(properties)) return properties; + CfnResourcePropsValidator(properties).assertSuccess(); + return {}; +} + +// @ts-ignore TS6133 +function CfnResourcePropsFromCloudFormation(properties: any): cfn_parse.FromCloudFormationResult { + if (cdk.isResolvableObject(properties)) { + return new cfn_parse.FromCloudFormationResult(properties); + } + properties = ((properties == null) ? {} : properties); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + return new cfn_parse.FromCloudFormationResult(properties); + } + const ret = new cfn_parse.FromCloudFormationPropertyObject(); + ret.addUnrecognizedPropertiesAsExtra(properties); + return ret; +}" +`; + +exports[`resource interface with "ResourceArn" 1`] = ` +"/* eslint-disable prettier/prettier,max-len */ +import * as cdk from "aws-cdk-lib"; +import * as constructs from "constructs"; +import * as cfn_parse from "aws-cdk-lib/core/lib/helpers-internal"; + +/** + * @cloudformationResource AWS::Some::Resource + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-some-resource.html + */ +export class CfnResource extends cdk.CfnResource implements cdk.IInspectable { + /** + * The CloudFormation resource type name for this resource class. + */ + public static readonly CFN_RESOURCE_TYPE_NAME: string = "AWS::Some::Resource"; + + /** + * Build a CfnResource from CloudFormation properties + * + * A factory method that creates a new instance of this class from an object + * containing the CloudFormation properties of this resource. + * Used in the @aws-cdk/cloudformation-include module. + * + * @internal + */ + public static _fromCloudFormation(scope: constructs.Construct, id: string, resourceAttributes: any, options: cfn_parse.FromCloudFormationOptions): CfnResource { + resourceAttributes = resourceAttributes || {}; + const resourceProperties = options.parser.parseValue(resourceAttributes.Properties); + const propsResult = CfnResourcePropsFromCloudFormation(resourceProperties); + if (cdk.isResolvableObject(propsResult.value)) { + throw new Error("Unexpected IResolvable"); + } + const ret = new CfnResource(scope, id, propsResult.value); + for (const [propKey, propVal] of Object.entries(propsResult.extraProperties)) { + ret.addPropertyOverride(propKey, propVal); + } + options.parser.handleAttributes(ret, resourceAttributes, id); + return ret; + } + + /** + * The identifier of the resource + * + * @cloudformationAttribute Id + */ + public readonly attrId: string; + + /** + * The arn for the resource + * + * @cloudformationAttribute ResourceArn + */ + public readonly attrResourceArn: string; + + /** + * @param scope Scope in which this resource is defined + * @param id Construct identifier for this resource (unique in its scope) + * @param props Resource properties + */ + public constructor(scope: constructs.Construct, id: string, props: CfnResourceProps = {}) { + super(scope, id, { + "type": CfnResource.CFN_RESOURCE_TYPE_NAME, + "properties": props + }); + + this.attrId = cdk.Token.asString(this.getAtt("Id", cdk.ResolutionTypeHint.STRING)); + this.attrResourceArn = cdk.Token.asString(this.getAtt("ResourceArn", cdk.ResolutionTypeHint.STRING)); + } + + protected get cfnProperties(): Record { + return {}; + } + + /** + * Examines the CloudFormation resource and discloses attributes + * + * @param inspector tree inspector to collect and process attributes + */ + public inspect(inspector: cdk.TreeInspector): void { + inspector.addAttribute("aws:cdk:cloudformation:type", CfnResource.CFN_RESOURCE_TYPE_NAME); + inspector.addAttribute("aws:cdk:cloudformation:props", this.cfnProperties); + } + + protected renderProperties(props: Record): Record { + return convertCfnResourcePropsToCloudFormation(props); + } +} + +/** + * Attributes for \`CfnResource\`. + * + * @stability external + */ +export interface ICfnResource { + /** + * The identifier of the resource + * + * @cloudformationAttribute Id + */ + readonly attrId: string; + + /** + * The arn for the resource + * + * @cloudformationAttribute ResourceArn + */ + readonly attrResourceArn: string; +} + +/** + * Properties for defining a \`CfnResource\` + * + * @struct + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-some-resource.html + */ +export interface CfnResourceProps { + +} + +/** + * Determine whether the given properties match those of a \`CfnResourceProps\` + * + * @param properties - the TypeScript properties of a \`CfnResourceProps\` + * + * @returns the result of the validation. + */ +// @ts-ignore TS6133 +function CfnResourcePropsValidator(properties: any): cdk.ValidationResult { + if (!cdk.canInspect(properties)) return cdk.VALIDATION_SUCCESS; + const errors = new cdk.ValidationResults(); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + errors.collect(new cdk.ValidationResult("Expected an object, but received: " + JSON.stringify(properties))); + } + return errors.wrap("supplied properties not correct for \\"CfnResourceProps\\""); +} + +// @ts-ignore TS6133 +function convertCfnResourcePropsToCloudFormation(properties: any): any { + if (!cdk.canInspect(properties)) return properties; + CfnResourcePropsValidator(properties).assertSuccess(); + return {}; +} + +// @ts-ignore TS6133 +function CfnResourcePropsFromCloudFormation(properties: any): cfn_parse.FromCloudFormationResult { + if (cdk.isResolvableObject(properties)) { + return new cfn_parse.FromCloudFormationResult(properties); + } + properties = ((properties == null) ? {} : properties); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + return new cfn_parse.FromCloudFormationResult(properties); + } + const ret = new cfn_parse.FromCloudFormationPropertyObject(); + ret.addUnrecognizedPropertiesAsExtra(properties); + return ret; +}" +`; + +exports[`resource interface with multiple primaryIdentifiers 1`] = ` +"/* eslint-disable prettier/prettier,max-len */ +import * as cdk from "aws-cdk-lib"; +import * as constructs from "constructs"; +import * as cfn_parse from "aws-cdk-lib/core/lib/helpers-internal"; + +/** + * @cloudformationResource AWS::Some::Resource + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-some-resource.html + */ +export class CfnResource extends cdk.CfnResource implements cdk.IInspectable { + /** + * The CloudFormation resource type name for this resource class. + */ + public static readonly CFN_RESOURCE_TYPE_NAME: string = "AWS::Some::Resource"; + + /** + * Build a CfnResource from CloudFormation properties + * + * A factory method that creates a new instance of this class from an object + * containing the CloudFormation properties of this resource. + * Used in the @aws-cdk/cloudformation-include module. + * + * @internal + */ + public static _fromCloudFormation(scope: constructs.Construct, id: string, resourceAttributes: any, options: cfn_parse.FromCloudFormationOptions): CfnResource { + resourceAttributes = resourceAttributes || {}; + const resourceProperties = options.parser.parseValue(resourceAttributes.Properties); + const propsResult = CfnResourcePropsFromCloudFormation(resourceProperties); + if (cdk.isResolvableObject(propsResult.value)) { + throw new Error("Unexpected IResolvable"); + } + const ret = new CfnResource(scope, id, propsResult.value); + for (const [propKey, propVal] of Object.entries(propsResult.extraProperties)) { + ret.addPropertyOverride(propKey, propVal); + } + options.parser.handleAttributes(ret, resourceAttributes, id); + return ret; + } + + /** + * Another identifier of the resource + * + * @cloudformationAttribute Another + */ + public readonly attrAnother: string; + + /** + * The identifier of the resource + * + * @cloudformationAttribute Id + */ + public readonly attrId: string; + + /** + * @param scope Scope in which this resource is defined + * @param id Construct identifier for this resource (unique in its scope) + * @param props Resource properties + */ + public constructor(scope: constructs.Construct, id: string, props: CfnResourceProps = {}) { + super(scope, id, { + "type": CfnResource.CFN_RESOURCE_TYPE_NAME, + "properties": props + }); + + this.attrAnother = cdk.Token.asString(this.getAtt("Another", cdk.ResolutionTypeHint.STRING)); + this.attrId = cdk.Token.asString(this.getAtt("Id", cdk.ResolutionTypeHint.STRING)); + } + + protected get cfnProperties(): Record { + return {}; + } + + /** + * Examines the CloudFormation resource and discloses attributes + * + * @param inspector tree inspector to collect and process attributes + */ + public inspect(inspector: cdk.TreeInspector): void { + inspector.addAttribute("aws:cdk:cloudformation:type", CfnResource.CFN_RESOURCE_TYPE_NAME); + inspector.addAttribute("aws:cdk:cloudformation:props", this.cfnProperties); + } + + protected renderProperties(props: Record): Record { + return convertCfnResourcePropsToCloudFormation(props); + } +} + +/** + * Attributes for \`CfnResource\`. + * + * @stability external + */ +export interface ICfnResource { + /** + * The identifier of the resource + * + * @cloudformationAttribute Id + */ + readonly attrId: string; + + /** + * Another identifier of the resource + * + * @cloudformationAttribute Another + */ + readonly attrAnother: string; +} + +/** + * Properties for defining a \`CfnResource\` + * + * @struct + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-some-resource.html + */ +export interface CfnResourceProps { + +} + +/** + * Determine whether the given properties match those of a \`CfnResourceProps\` + * + * @param properties - the TypeScript properties of a \`CfnResourceProps\` + * + * @returns the result of the validation. + */ +// @ts-ignore TS6133 +function CfnResourcePropsValidator(properties: any): cdk.ValidationResult { + if (!cdk.canInspect(properties)) return cdk.VALIDATION_SUCCESS; + const errors = new cdk.ValidationResults(); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + errors.collect(new cdk.ValidationResult("Expected an object, but received: " + JSON.stringify(properties))); + } + return errors.wrap("supplied properties not correct for \\"CfnResourceProps\\""); +} + +// @ts-ignore TS6133 +function convertCfnResourcePropsToCloudFormation(properties: any): any { + if (!cdk.canInspect(properties)) return properties; + CfnResourcePropsValidator(properties).assertSuccess(); + return {}; +} + +// @ts-ignore TS6133 +function CfnResourcePropsFromCloudFormation(properties: any): cfn_parse.FromCloudFormationResult { + if (cdk.isResolvableObject(properties)) { + return new cfn_parse.FromCloudFormationResult(properties); + } + properties = ((properties == null) ? {} : properties); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + return new cfn_parse.FromCloudFormationResult(properties); + } + const ret = new cfn_parse.FromCloudFormationPropertyObject(); + ret.addUnrecognizedPropertiesAsExtra(properties); + return ret; +}" +`; diff --git a/tools/@aws-cdk/spec2cdk/test/resources.test.ts b/tools/@aws-cdk/spec2cdk/test/resources.test.ts index 49049e2b78b68..350288b35ac08 100644 --- a/tools/@aws-cdk/spec2cdk/test/resources.test.ts +++ b/tools/@aws-cdk/spec2cdk/test/resources.test.ts @@ -1,151 +1,212 @@ -import { loadAwsServiceSpec } from '@aws-cdk/aws-service-spec'; -import { SpecDatabase } from '@aws-cdk/service-spec-types'; +import { Service, SpecDatabase, emptyDatabase } from '@aws-cdk/service-spec-types'; import { TypeScriptRenderer } from '@cdklabs/typewriter'; import { AstBuilder } from '../lib/cdk/ast'; const renderer = new TypeScriptRenderer(); let db: SpecDatabase; +let service: Service; -beforeAll(async () => { - db = await loadAwsServiceSpec(); -}); +beforeEach(async () => { + db = emptyDatabase(); -test('resource interface when primaryIdentifier is an attribute', () => { - const resource = db.lookup('resource', 'cloudFormationType', 'equals', 'AWS::VoiceID::Domain').only(); + service = db.allocate('service', { + name: 'aws-some', + shortName: 'some', + capitalized: 'Some', + cloudFormationNamespace: 'AWS::Some', + }); +}); - const ast = AstBuilder.forResource(resource, { db }); +test('resource interface when primaryIdentifier is a property', () => { + // GIVEN + const resource = db.allocate('resource', { + name: 'Resource', + primaryIdentifier: ['Id'], + attributes: {}, + properties: { + Id: { + type: { type: 'string' }, + documentation: 'The identifier of the resource', + }, + }, + cloudFormationType: 'AWS::Some::Resource', + }); + db.link('hasResource', service, resource); + + // THEN + const foundResource = db.lookup('resource', 'cloudFormationType', 'equals', 'AWS::Some::Resource').only(); + + const ast = AstBuilder.forResource(foundResource, { db }); const rendered = renderer.render(ast.module); - expect(rendered).toContain([ - '/**', - ' * Attributes for `CfnDomain`.', - ' *', - ' * @stability external', - ' */', - 'export interface ICfnDomain {', - ' /**', - ' * The identifier of the domain.', - ' *', - ' * @cloudformationAttribute DomainId', - ' */', - ' readonly attrDomainId: string;', - '}', - ].join('\n')); + expect(rendered).toMatchSnapshot(); }); -test('resource interface when primaryIdentifier is a property', () => { - const resource = db.lookup('resource', 'cloudFormationType', 'equals', 'AWS::KinesisAnalyticsV2::Application').only(); - - const ast = AstBuilder.forResource(resource, { db }); +test('resource interface when primaryIdentifier is an attribute', () => { + // GIVEN + const resource = db.allocate('resource', { + name: 'Resource', + primaryIdentifier: ['Id'], + properties: {}, + attributes: { + Id: { + type: { type: 'string' }, + documentation: 'The identifier of the resource', + }, + }, + cloudFormationType: 'AWS::Some::Resource', + }); + db.link('hasResource', service, resource); + + // THEN + const foundResource = db.lookup('resource', 'cloudFormationType', 'equals', 'AWS::Some::Resource').only(); + + const ast = AstBuilder.forResource(foundResource, { db }); const rendered = renderer.render(ast.module); - expect(rendered).toContain([ - '/**', - ' * Attributes for `CfnApplication`.', - ' *', - ' * @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')); + expect(rendered).toMatchSnapshot(); }); test('resource interface with multiple primaryIdentifiers', () => { - const resource = db.lookup('resource', 'cloudFormationType', 'equals', 'AWS::LakeFormation::DataCellsFilter').only(); - - const ast = AstBuilder.forResource(resource, { db }); + // GIVEN + const resource = db.allocate('resource', { + name: 'Resource', + primaryIdentifier: ['Id', 'Another'], + properties: {}, + attributes: { + Id: { + type: { type: 'string' }, + documentation: 'The identifier of the resource', + }, + Another: { + type: { type: 'string' }, + documentation: 'Another identifier of the resource', + }, + }, + cloudFormationType: 'AWS::Some::Resource', + }); + db.link('hasResource', service, resource); + + // THEN + const foundResource = db.lookup('resource', 'cloudFormationType', 'equals', 'AWS::Some::Resource').only(); + + const ast = AstBuilder.forResource(foundResource, { db }); const rendered = renderer.render(ast.module); - expect(rendered).toContain([ - '/**', - ' * Attributes for `CfnDataCellsFilter`.', - ' *', - ' * @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;', - // ' /**', - // ' * 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')); + expect(rendered).toMatchSnapshot(); }); test('resource interface with "Arn"', () => { - const resource = db.lookup('resource', 'cloudFormationType', 'equals', 'AWS::S3::Bucket').only(); + // GIVEN + const resource = db.allocate('resource', { + name: 'Resource', + primaryIdentifier: ['Id', 'Another'], + properties: {}, + attributes: { + Id: { + type: { type: 'string' }, + documentation: 'The identifier of the resource', + }, + Arn: { + type: { type: 'string' }, + documentation: 'The arn for the resource', + }, + }, + cloudFormationType: 'AWS::Some::Resource', + }); + db.link('hasResource', service, resource); + + // THEN + const foundResource = db.lookup('resource', 'cloudFormationType', 'equals', 'AWS::Some::Resource').only(); + + const ast = AstBuilder.forResource(foundResource, { db }); - const ast = AstBuilder.forResource(resource, { db }); + const rendered = renderer.render(ast.module); + + expect(rendered).toMatchSnapshot(); +}); + +test('resource interface with "ResourceArn"', () => { + // GIVEN + const resource = db.allocate('resource', { + name: 'Resource', + primaryIdentifier: ['Id'], + properties: {}, + attributes: { + Id: { + type: { type: 'string' }, + documentation: 'The identifier of the resource', + }, + ResourceArn: { + type: { type: 'string' }, + documentation: 'The arn for the resource', + }, + }, + cloudFormationType: 'AWS::Some::Resource', + }); + db.link('hasResource', service, resource); + + // THEN + const foundResource = db.lookup('resource', 'cloudFormationType', 'equals', 'AWS::Some::Resource').only(); + + const ast = AstBuilder.forResource(foundResource, { db }); + + const rendered = renderer.render(ast.module); + + expect(rendered).toMatchSnapshot(); +}); + +test('resource interface with "Arn"', () => { + // GIVEN + const resource = db.allocate('resource', { + name: 'Something', + primaryIdentifier: ['Id'], + properties: {}, + attributes: { + Id: { + type: { type: 'string' }, + documentation: 'The identifier of the resource', + }, + SomethingArn: { + type: { type: 'string' }, + documentation: 'The arn for something', + }, + }, + cloudFormationType: 'AWS::Some::Something', + }); + db.link('hasResource', service, resource); + + // THEN + const foundResource = db.lookup('resource', 'cloudFormationType', 'equals', 'AWS::Some::Something').only(); + + const ast = AstBuilder.forResource(foundResource, { db }); const rendered = renderer.render(ast.module); - expect(rendered).toContain([ - '/**', - ' * Attributes for `CfnBucket`.', - ' *', - ' * @stability external', - ' */', - 'export interface ICfnBucket {', - ' /**', - ' * A name for the bucket.', - ' *', - ' * If you don\'t specify a name, AWS CloudFormation generates a unique ID and uses that ID for the bucket name. The bucket name must contain only lowercase letters, numbers, periods (.), and dashes (-) and must follow [Amazon S3 bucket restrictions and limitations](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html) . For more information, see [Rules for naming Amazon S3 buckets](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html#bucketnamingrules) in the *Amazon S3 User Guide* .', - ' *', - ' * > If you specify a name, you can\'t perform updates that require replacement of this resource. You can perform updates that require no or some interruption. If you need to replace the resource, specify a new name.', - ' *', - ' * @cloudformationRef bucketName', - ' *', - ' * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3-bucket.html#cfn-s3-bucket-bucketname', - ' */', - ' readonly attrBucketName?: string;', - // ' /**', - // ' * Returns the Amazon Resource Name (ARN) of the specified bucket.', - // ' *', - // ' * Example: `arn:aws:s3:::DOC-EXAMPLE-BUCKET`', - // ' *', - // ' * @cloudformationAttribute Arn', - // ' */', - // ' readonly attrArn: string;', - // '}', - ].join('\n')); + expect(rendered).toMatchSnapshot(); }); -// TODO: test with ResourceArn and Arn too \ No newline at end of file +// function getInterface(module: ResourceModule, intName: string) { +// const int = moduleInterfaces(module).find((i) => i.spec.name === intName); +// if (!int) throw new Error('wtf'); +// return new InterfaceRenderer().renderInterface(int); +// } + +// Should be in Module +// function moduleInterfaces(module: ResourceModule) { +// return module.types.filter((t) => t instanceof InterfaceType).map((t) => t as InterfaceType); +// } + +// class InterfaceRenderer extends TypeScriptRenderer { + +// public render() { +// super.r +// } + +// public renderInterface(interfaceType: InterfaceType) { +// super.renderInterface(interfaceType); +// } +// } From 59c4c773c762c1b373523977e666d94606aa0a61 Mon Sep 17 00:00:00 2001 From: Kaizen Conroy Date: Mon, 30 Oct 2023 18:24:47 -0400 Subject: [PATCH 07/10] refactoring... snapshot changes are just moving order around --- .../test/__snapshots__/resources.test.ts.snap | 224 +++++++++--------- 1 file changed, 112 insertions(+), 112 deletions(-) diff --git a/tools/@aws-cdk/spec2cdk/test/__snapshots__/resources.test.ts.snap b/tools/@aws-cdk/spec2cdk/test/__snapshots__/resources.test.ts.snap index f928d46ae9944..75bf4cd8c0ca0 100644 --- a/tools/@aws-cdk/spec2cdk/test/__snapshots__/resources.test.ts.snap +++ b/tools/@aws-cdk/spec2cdk/test/__snapshots__/resources.test.ts.snap @@ -6,6 +6,20 @@ import * as cdk from "aws-cdk-lib"; import * as constructs from "constructs"; import * as cfn_parse from "aws-cdk-lib/core/lib/helpers-internal"; +/** + * Attributes for \`CfnResource\`. + * + * @stability external + */ +export interface ICfnResource { + /** + * The identifier of the resource. + * + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-some-resource.html#cfn-some-resource-id + */ + readonly attrId?: string; +} + /** * @cloudformationResource AWS::Some::Resource * @stability external @@ -81,20 +95,6 @@ export class CfnResource extends cdk.CfnResource implements cdk.IInspectable { } } -/** - * Attributes for \`CfnResource\`. - * - * @stability external - */ -export interface ICfnResource { - /** - * The identifier of the resource. - * - * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-some-resource.html#cfn-some-resource-id - */ - readonly attrId?: string; -} - /** * Properties for defining a \`CfnResource\` * @@ -160,6 +160,20 @@ import * as cdk from "aws-cdk-lib"; import * as constructs from "constructs"; import * as cfn_parse from "aws-cdk-lib/core/lib/helpers-internal"; +/** + * Attributes for \`CfnResource\`. + * + * @stability external + */ +export interface ICfnResource { + /** + * The identifier of the resource + * + * @cloudformationAttribute Id + */ + readonly attrId: string; +} + /** * @cloudformationResource AWS::Some::Resource * @stability external @@ -235,20 +249,6 @@ export class CfnResource extends cdk.CfnResource implements cdk.IInspectable { } } -/** - * Attributes for \`CfnResource\`. - * - * @stability external - */ -export interface ICfnResource { - /** - * The identifier of the resource - * - * @cloudformationAttribute Id - */ - readonly attrId: string; -} - /** * Properties for defining a \`CfnResource\` * @@ -305,6 +305,27 @@ import * as cdk from "aws-cdk-lib"; import * as constructs from "constructs"; import * as cfn_parse from "aws-cdk-lib/core/lib/helpers-internal"; +/** + * Attributes for \`CfnSomething\`. + * + * @stability external + */ +export interface ICfnSomething { + /** + * The identifier of the resource + * + * @cloudformationAttribute Id + */ + readonly attrId: string; + + /** + * The arn for something + * + * @cloudformationAttribute SomethingArn + */ + readonly attrSomethingArn: string; +} + /** * @cloudformationResource AWS::Some::Something * @stability external @@ -388,27 +409,6 @@ export class CfnSomething extends cdk.CfnResource implements cdk.IInspectable { } } -/** - * Attributes for \`CfnSomething\`. - * - * @stability external - */ -export interface ICfnSomething { - /** - * The identifier of the resource - * - * @cloudformationAttribute Id - */ - readonly attrId: string; - - /** - * The arn for something - * - * @cloudformationAttribute SomethingArn - */ - readonly attrSomethingArn: string; -} - /** * Properties for defining a \`CfnSomething\` * @@ -465,6 +465,27 @@ import * as cdk from "aws-cdk-lib"; import * as constructs from "constructs"; import * as cfn_parse from "aws-cdk-lib/core/lib/helpers-internal"; +/** + * Attributes for \`CfnResource\`. + * + * @stability external + */ +export interface ICfnResource { + /** + * The identifier of the resource + * + * @cloudformationAttribute Id + */ + readonly attrId: string; + + /** + * The arn for the resource + * + * @cloudformationAttribute Arn + */ + readonly attrArn: string; +} + /** * @cloudformationResource AWS::Some::Resource * @stability external @@ -548,27 +569,6 @@ export class CfnResource extends cdk.CfnResource implements cdk.IInspectable { } } -/** - * Attributes for \`CfnResource\`. - * - * @stability external - */ -export interface ICfnResource { - /** - * The identifier of the resource - * - * @cloudformationAttribute Id - */ - readonly attrId: string; - - /** - * The arn for the resource - * - * @cloudformationAttribute Arn - */ - readonly attrArn: string; -} - /** * Properties for defining a \`CfnResource\` * @@ -625,6 +625,27 @@ import * as cdk from "aws-cdk-lib"; import * as constructs from "constructs"; import * as cfn_parse from "aws-cdk-lib/core/lib/helpers-internal"; +/** + * Attributes for \`CfnResource\`. + * + * @stability external + */ +export interface ICfnResource { + /** + * The identifier of the resource + * + * @cloudformationAttribute Id + */ + readonly attrId: string; + + /** + * The arn for the resource + * + * @cloudformationAttribute ResourceArn + */ + readonly attrResourceArn: string; +} + /** * @cloudformationResource AWS::Some::Resource * @stability external @@ -708,27 +729,6 @@ export class CfnResource extends cdk.CfnResource implements cdk.IInspectable { } } -/** - * Attributes for \`CfnResource\`. - * - * @stability external - */ -export interface ICfnResource { - /** - * The identifier of the resource - * - * @cloudformationAttribute Id - */ - readonly attrId: string; - - /** - * The arn for the resource - * - * @cloudformationAttribute ResourceArn - */ - readonly attrResourceArn: string; -} - /** * Properties for defining a \`CfnResource\` * @@ -785,6 +785,27 @@ import * as cdk from "aws-cdk-lib"; import * as constructs from "constructs"; import * as cfn_parse from "aws-cdk-lib/core/lib/helpers-internal"; +/** + * Attributes for \`CfnResource\`. + * + * @stability external + */ +export interface ICfnResource { + /** + * The identifier of the resource + * + * @cloudformationAttribute Id + */ + readonly attrId: string; + + /** + * Another identifier of the resource + * + * @cloudformationAttribute Another + */ + readonly attrAnother: string; +} + /** * @cloudformationResource AWS::Some::Resource * @stability external @@ -868,27 +889,6 @@ export class CfnResource extends cdk.CfnResource implements cdk.IInspectable { } } -/** - * Attributes for \`CfnResource\`. - * - * @stability external - */ -export interface ICfnResource { - /** - * The identifier of the resource - * - * @cloudformationAttribute Id - */ - readonly attrId: string; - - /** - * Another identifier of the resource - * - * @cloudformationAttribute Another - */ - readonly attrAnother: string; -} - /** * Properties for defining a \`CfnResource\` * From a4583b06e5f6f94d94106dc0257dc6842790150a Mon Sep 17 00:00:00 2001 From: Kaizen Conroy Date: Mon, 30 Oct 2023 18:25:48 -0400 Subject: [PATCH 08/10] lib changes refacotring --- tools/@aws-cdk/spec2cdk/lib/cdk/ast.ts | 6 ++- .../spec2cdk/lib/cdk/resource-class.ts | 54 +++++++++++-------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/ast.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/ast.ts index 739c13ad2aac9..62dae858aa2ec 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/ast.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/ast.ts @@ -93,7 +93,11 @@ export class AstBuilder { } 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(); diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts index b8157c43afbc2..7c09f999b5058 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts @@ -45,45 +45,57 @@ 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 interface: InterfaceType; + 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, ) { - super(scope, { + const resourceInterface = new InterfaceType(scope, { export: true, - name: classNameFromResource(resource, suffix), + name: interfaceNameFromResource(props.resource, props.suffix), docs: { - ...splitDocumentation(resource.documentation), + summary: `Attributes for \`${classNameFromResource(props.resource)}\`.`, stability: Stability.External, - docTags: { cloudformationResource: resource.cloudFormationType }, - see: cloudFormationDocLink({ - resourceType: resource.cloudFormationType, - }), }, - extends: CDK_CORE.CfnResource, - implements: [CDK_CORE.IInspectable, ...ResourceDecider.taggabilityInterfaces(resource)], }); - this.module = Module.of(this); - - this.interface = new InterfaceType(this.scope, { + super(scope, { export: true, - name: interfaceNameFromResource(this.resource, this.suffix), + name: classNameFromResource(props.resource, props.suffix), docs: { - summary: `Attributes for \`${classNameFromResource(this.resource)}\`.`, + ...splitDocumentation(props.resource.documentation), stability: Stability.External, + docTags: { cloudformationResource: props.resource.cloudFormationType }, + see: cloudFormationDocLink({ + resourceType: props.resource.cloudFormationType, + }), }, + extends: CDK_CORE.CfnResource, + 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, { export: true, name: propStructNameFromResource(this.resource, this.suffix), @@ -97,7 +109,7 @@ export class ResourceClass extends ClassType { }); this.converter = TypeConverter.forResource({ - db: db, + db: this.db, resource: this.resource, resourceClass: this, }); @@ -119,13 +131,13 @@ export class ResourceClass extends ClassType { // Build the shared interface for (const identifier of this.decider.primaryIdentifier ?? []) { - this.interface.addProperty({ + this.resourceInterface.addProperty({ ...identifier, immutable: true, }); } if (this.decider.arn) { - this.interface.addProperty({ + this.resourceInterface.addProperty({ ...this.decider.arn, immutable: true, }); From cd6a0e62f18ba1f61e61758cde9c92d574020b3c Mon Sep 17 00:00:00 2001 From: Kaizen Conroy Date: Wed, 1 Nov 2023 13:03:35 -0400 Subject: [PATCH 09/10] fix case where arn is also primaryIdentifier --- .../spec2cdk/lib/cdk/resource-class.ts | 4 +- .../spec2cdk/lib/cdk/resource-decider.ts | 7 +- .../test/__snapshots__/resources.test.ts.snap | 145 ++++++++++++++++++ .../@aws-cdk/spec2cdk/test/resources.test.ts | 46 +++--- 4 files changed, 177 insertions(+), 25 deletions(-) diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts index 7c09f999b5058..507029d005f1d 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts @@ -136,7 +136,9 @@ export class ResourceClass extends ClassType { immutable: true, }); } - if (this.decider.arn) { + + // 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, diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-decider.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-decider.ts index 2a07a25ce246e..6a88b5e3e1927 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-decider.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-decider.ts @@ -52,12 +52,13 @@ export class ResourceDecider { this.classAttributeProperties.sort((p1, p2) => p1.propertySpec.name.localeCompare(p2.propertySpec.name)); } - // TODO: uh, make this not a loop of loops haha private findArn() { - // a list of possible names for the arn, in order of most likely to least likely + // 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 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; diff --git a/tools/@aws-cdk/spec2cdk/test/__snapshots__/resources.test.ts.snap b/tools/@aws-cdk/spec2cdk/test/__snapshots__/resources.test.ts.snap index 75bf4cd8c0ca0..1087d3a892545 100644 --- a/tools/@aws-cdk/spec2cdk/test/__snapshots__/resources.test.ts.snap +++ b/tools/@aws-cdk/spec2cdk/test/__snapshots__/resources.test.ts.snap @@ -779,6 +779,151 @@ function CfnResourcePropsFromCloudFormation(properties: any): cfn_parse.FromClou }" `; +exports[`resource interface with Arn as primaryIdentifier 1`] = ` +"/* eslint-disable prettier/prettier,max-len */ +import * as cdk from "aws-cdk-lib"; +import * as constructs from "constructs"; +import * as cfn_parse from "aws-cdk-lib/core/lib/helpers-internal"; + +/** + * Attributes for \`CfnResource\`. + * + * @stability external + */ +export interface ICfnResource { + /** + * The arn of the resource + * + * @cloudformationAttribute Arn + */ + readonly attrArn: string; +} + +/** + * @cloudformationResource AWS::Some::Resource + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-some-resource.html + */ +export class CfnResource extends cdk.CfnResource implements cdk.IInspectable { + /** + * The CloudFormation resource type name for this resource class. + */ + public static readonly CFN_RESOURCE_TYPE_NAME: string = "AWS::Some::Resource"; + + /** + * Build a CfnResource from CloudFormation properties + * + * A factory method that creates a new instance of this class from an object + * containing the CloudFormation properties of this resource. + * Used in the @aws-cdk/cloudformation-include module. + * + * @internal + */ + public static _fromCloudFormation(scope: constructs.Construct, id: string, resourceAttributes: any, options: cfn_parse.FromCloudFormationOptions): CfnResource { + resourceAttributes = resourceAttributes || {}; + const resourceProperties = options.parser.parseValue(resourceAttributes.Properties); + const propsResult = CfnResourcePropsFromCloudFormation(resourceProperties); + if (cdk.isResolvableObject(propsResult.value)) { + throw new Error("Unexpected IResolvable"); + } + const ret = new CfnResource(scope, id, propsResult.value); + for (const [propKey, propVal] of Object.entries(propsResult.extraProperties)) { + ret.addPropertyOverride(propKey, propVal); + } + options.parser.handleAttributes(ret, resourceAttributes, id); + return ret; + } + + /** + * The arn of the resource + * + * @cloudformationAttribute Arn + */ + public readonly attrArn: string; + + /** + * @param scope Scope in which this resource is defined + * @param id Construct identifier for this resource (unique in its scope) + * @param props Resource properties + */ + public constructor(scope: constructs.Construct, id: string, props: CfnResourceProps = {}) { + super(scope, id, { + "type": CfnResource.CFN_RESOURCE_TYPE_NAME, + "properties": props + }); + + this.attrArn = cdk.Token.asString(this.getAtt("Arn", cdk.ResolutionTypeHint.STRING)); + } + + protected get cfnProperties(): Record { + return {}; + } + + /** + * Examines the CloudFormation resource and discloses attributes + * + * @param inspector tree inspector to collect and process attributes + */ + public inspect(inspector: cdk.TreeInspector): void { + inspector.addAttribute("aws:cdk:cloudformation:type", CfnResource.CFN_RESOURCE_TYPE_NAME); + inspector.addAttribute("aws:cdk:cloudformation:props", this.cfnProperties); + } + + protected renderProperties(props: Record): Record { + return convertCfnResourcePropsToCloudFormation(props); + } +} + +/** + * Properties for defining a \`CfnResource\` + * + * @struct + * @stability external + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-some-resource.html + */ +export interface CfnResourceProps { + +} + +/** + * Determine whether the given properties match those of a \`CfnResourceProps\` + * + * @param properties - the TypeScript properties of a \`CfnResourceProps\` + * + * @returns the result of the validation. + */ +// @ts-ignore TS6133 +function CfnResourcePropsValidator(properties: any): cdk.ValidationResult { + if (!cdk.canInspect(properties)) return cdk.VALIDATION_SUCCESS; + const errors = new cdk.ValidationResults(); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + errors.collect(new cdk.ValidationResult("Expected an object, but received: " + JSON.stringify(properties))); + } + return errors.wrap("supplied properties not correct for \\"CfnResourceProps\\""); +} + +// @ts-ignore TS6133 +function convertCfnResourcePropsToCloudFormation(properties: any): any { + if (!cdk.canInspect(properties)) return properties; + CfnResourcePropsValidator(properties).assertSuccess(); + return {}; +} + +// @ts-ignore TS6133 +function CfnResourcePropsFromCloudFormation(properties: any): cfn_parse.FromCloudFormationResult { + if (cdk.isResolvableObject(properties)) { + return new cfn_parse.FromCloudFormationResult(properties); + } + properties = ((properties == null) ? {} : properties); + if (!(properties && typeof properties == 'object' && !Array.isArray(properties))) { + return new cfn_parse.FromCloudFormationResult(properties); + } + const ret = new cfn_parse.FromCloudFormationPropertyObject(); + ret.addUnrecognizedPropertiesAsExtra(properties); + return ret; +}" +`; + exports[`resource interface with multiple primaryIdentifiers 1`] = ` "/* eslint-disable prettier/prettier,max-len */ import * as cdk from "aws-cdk-lib"; diff --git a/tools/@aws-cdk/spec2cdk/test/resources.test.ts b/tools/@aws-cdk/spec2cdk/test/resources.test.ts index 350288b35ac08..42ae48865adf5 100644 --- a/tools/@aws-cdk/spec2cdk/test/resources.test.ts +++ b/tools/@aws-cdk/spec2cdk/test/resources.test.ts @@ -189,24 +189,28 @@ test('resource interface with "Arn"', () => { expect(rendered).toMatchSnapshot(); }); -// function getInterface(module: ResourceModule, intName: string) { -// const int = moduleInterfaces(module).find((i) => i.spec.name === intName); -// if (!int) throw new Error('wtf'); -// return new InterfaceRenderer().renderInterface(int); -// } - -// Should be in Module -// function moduleInterfaces(module: ResourceModule) { -// return module.types.filter((t) => t instanceof InterfaceType).map((t) => t as InterfaceType); -// } - -// class InterfaceRenderer extends TypeScriptRenderer { - -// public render() { -// super.r -// } - -// public renderInterface(interfaceType: InterfaceType) { -// super.renderInterface(interfaceType); -// } -// } +test('resource interface with Arn as primaryIdentifier', () => { + // GIVEN + const resource = db.allocate('resource', { + name: 'Resource', + primaryIdentifier: ['Arn'], + properties: {}, + attributes: { + Arn: { + type: { type: 'string' }, + documentation: 'The arn of the resource', + }, + }, + cloudFormationType: 'AWS::Some::Resource', + }); + db.link('hasResource', service, resource); + + // THEN + const foundResource = db.lookup('resource', 'cloudFormationType', 'equals', 'AWS::Some::Resource').only(); + + const ast = AstBuilder.forResource(foundResource, { db }); + + const rendered = renderer.render(ast.module); + + expect(rendered).toMatchSnapshot(); +}); \ No newline at end of file From 22808331d9219da2a3b736f2d3ef6ab5088d848c Mon Sep 17 00:00:00 2001 From: Kaizen Conroy Date: Thu, 2 Nov 2023 12:43:51 -0400 Subject: [PATCH 10/10] one linter fixed --- packages/awslint/lib/rules/core-types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/awslint/lib/rules/core-types.ts b/packages/awslint/lib/rules/core-types.ts index 1c86ba2a60eab..3322b0172917a 100644 --- a/packages/awslint/lib/rules/core-types.ts +++ b/packages/awslint/lib/rules/core-types.ts @@ -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'));