From 86ab7344e549364e411f4eb15ba2134711531138 Mon Sep 17 00:00:00 2001 From: Kaizen Conroy <36202692+kaizencc@users.noreply.github.com> Date: Mon, 13 Nov 2023 11:20:15 -0500 Subject: [PATCH] chore(spec2cdk): Resource class implements ICfnResource (#27781) This PR implements the following: - If all `primaryIdentifiers` are `attributes`, there is nothing to do; `ICfnResource` is already implemented by `Resource` - if all `primaryIdentifiers` are `properties`, then - if there is 1 `primaryIdentifier` initialize it as `this.attrId = this.ref;` - if there are multiple `primaryIdentifiers` initialize each as `this.attrId = cdk.Fn.select(0, cdk.Fn.split('|', this.ref));` and so on ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --------- Co-authored-by: Momo Kornher --- .../spec2cdk/lib/cdk/resource-class.ts | 2 +- .../spec2cdk/lib/cdk/resource-decider.ts | 68 ++- .../test/__snapshots__/resources.test.ts.snap | 422 +++++++++++++++++- .../@aws-cdk/spec2cdk/test/resources.test.ts | 83 +++- 4 files changed, 519 insertions(+), 56 deletions(-) diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts index 93fc2055fc530..656e465cd7ed9 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-class.ts @@ -87,7 +87,7 @@ export class ResourceClass extends ClassType { }), }, extends: CDK_CORE.CfnResource, - implements: [CDK_CORE.IInspectable, ...ResourceDecider.taggabilityInterfaces(props.resource)], + implements: [CDK_CORE.IInspectable, resourceInterface.type, ...ResourceDecider.taggabilityInterfaces(props.resource)], }); this.db = props.db; diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-decider.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-decider.ts index 6a88b5e3e1927..b4548d732902b 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/resource-decider.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/resource-decider.ts @@ -13,6 +13,9 @@ export const HAS_25610 = false; // This convenience typewriter builder is used all over the place const $this = $E(expr.this_()); +// This convenience typewriter builder is used for cloudformation intrinsics +const $Fn = $E(expr.directCode('cdk.Fn')); + /** * Decide how properties get mapped between model types, Typescript types, and CloudFormation */ @@ -56,41 +59,70 @@ export class ResourceDecider { // 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`]; + const possibleArnNames = ['Arn', `${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; - } + const att = this.classAttributeProperties.find((a) => a.propertySpec.name === attributePropertyName(arn)); + if (att) { return att.propertySpec; } } return; } + private convertPropertySpecToRefAttribute(propSpec: PropertySpec): PropertySpec { + return { + ...propSpec, + name: attributePropertyName(propSpec.name[0].toUpperCase() + propSpec.name.slice(1)), + docs: { + ...propSpec.docs, + summary: (propSpec.docs?.summary ?? '').concat('\nThis property gets determined after the resource is created.'), + remarks: (propSpec.docs?.remarks ?? '').concat('@cloudformationAttribute Ref'), + }, + immutable: true, + }; + } + private convertPrimaryIdentifier() { - for (const cfnName of this.resource.primaryIdentifier ?? []) { + if (this.resource.primaryIdentifier === undefined) { return; } + for (let i = 0; i < (this.resource.primaryIdentifier).length; i++) { + const cfnName = this.resource.primaryIdentifier[i]; 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')), - }, + const propSpec = prop.propertySpec; + + // Build an attribute out of the property we're getting + // Create initializer for new attribute, if possible + let initializer: Expression | undefined = undefined; + if (propSpec.type === Type.STRING) { // handling only this case for now + if (this.resource.primaryIdentifier!.length === 1) { + initializer = CDK_CORE.tokenAsString($this.ref); + } else { + initializer = CDK_CORE.tokenAsString($Fn.select(expr.lit(i), $Fn.split(expr.lit('|'), $this.ref))); + } + } + + // If we cannot come up with an initializer, we're dropping this property on the floor + if (!initializer) { continue; } + + // Build an attribute spec out of the property spec + const attrPropertySpec = this.convertPropertySpecToRefAttribute(propSpec); + + // Add the new attribute to the relevant places + this.classAttributeProperties.push({ + propertySpec: attrPropertySpec, + initializer, }); + + this.primaryIdentifier.push(attrPropertySpec); } } } - private findPropertyByName(name: string): PropertySpec | undefined { - const props = this.propsProperties.filter((prop) => prop.propertySpec.name === name); + private findPropertyByName(name: string): ClassProperty | undefined { + const props = this.classProperties.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; } + if (props.length > 0) { return props[0]; } return; } 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 56c2330a0f941..2129619e82fd9 100644 --- a/tools/@aws-cdk/spec2cdk/test/__snapshots__/resources.test.ts.snap +++ b/tools/@aws-cdk/spec2cdk/test/__snapshots__/resources.test.ts.snap @@ -14,8 +14,9 @@ import * as cfn_parse from "aws-cdk-lib/core/lib/helpers-internal"; export interface ICfnResource extends constructs.IConstruct { /** * The identifier of the resource. + * This property gets determined after the resource is created. * - * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-some-resource.html#cfn-some-resource-id + * @cloudformationAttribute Ref */ readonly attrId?: string; } @@ -25,7 +26,7 @@ export interface ICfnResource extends constructs.IConstruct { * @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 { +export class CfnResource extends cdk.CfnResource implements cdk.IInspectable, ICfnResource { /** * The CloudFormation resource type name for this resource class. */ @@ -55,6 +56,14 @@ export class CfnResource extends cdk.CfnResource implements cdk.IInspectable { return ret; } + /** + * The identifier of the resource. + * This property gets determined after the resource is created. + * + * @cloudformationAttribute Ref + */ + public readonly attrId?: string; + /** * The identifier of the resource. */ @@ -71,6 +80,7 @@ export class CfnResource extends cdk.CfnResource implements cdk.IInspectable { "properties": props }); + this.attrId = cdk.Token.asString(this.ref); this.id = props.id; } @@ -179,7 +189,7 @@ export interface ICfnResource extends constructs.IConstruct { * @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 { +export class CfnResource extends cdk.CfnResource implements cdk.IInspectable, ICfnResource { /** * The CloudFormation resource type name for this resource class. */ @@ -331,7 +341,7 @@ export interface ICfnSomething extends constructs.IConstruct { * @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 { +export class CfnSomething extends cdk.CfnResource implements cdk.IInspectable, ICfnSomething { /** * The CloudFormation resource type name for this resource class. */ @@ -491,7 +501,7 @@ export interface ICfnResource extends constructs.IConstruct { * @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 { +export class CfnResource extends cdk.CfnResource implements cdk.IInspectable, ICfnResource { /** * The CloudFormation resource type name for this resource class. */ @@ -619,7 +629,7 @@ function CfnResourcePropsFromCloudFormation(properties: any): cfn_parse.FromClou }" `; -exports[`resource interface with "ResourceArn" 1`] = ` +exports[`resource interface with Arn as a property and not a primaryIdentifier 1`] = ` "/* eslint-disable prettier/prettier,max-len */ import * as cdk from "aws-cdk-lib"; import * as constructs from "constructs"; @@ -637,13 +647,6 @@ export interface ICfnResource extends constructs.IConstruct { * @cloudformationAttribute Id */ readonly attrId: string; - - /** - * The arn for the resource - * - * @cloudformationAttribute ResourceArn - */ - readonly attrResourceArn: string; } /** @@ -651,7 +654,7 @@ export interface ICfnResource extends constructs.IConstruct { * @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 { +export class CfnResource extends cdk.CfnResource implements cdk.IInspectable, ICfnResource { /** * The CloudFormation resource type name for this resource class. */ @@ -689,11 +692,9 @@ export class CfnResource extends cdk.CfnResource implements cdk.IInspectable { public readonly attrId: string; /** - * The arn for the resource - * - * @cloudformationAttribute ResourceArn + * The arn for the resource. */ - public readonly attrResourceArn: string; + public arn?: string; /** * @param scope Scope in which this resource is defined @@ -707,11 +708,13 @@ export class CfnResource extends cdk.CfnResource implements cdk.IInspectable { }); this.attrId = cdk.Token.asString(this.getAtt("Id", cdk.ResolutionTypeHint.STRING)); - this.attrResourceArn = cdk.Token.asString(this.getAtt("ResourceArn", cdk.ResolutionTypeHint.STRING)); + this.arn = props.arn; } protected get cfnProperties(): Record { - return {}; + return { + "arn": this.arn + }; } /** @@ -737,7 +740,12 @@ export class CfnResource extends cdk.CfnResource implements cdk.IInspectable { * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-some-resource.html */ export interface CfnResourceProps { - + /** + * The arn for the resource. + * + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-some-resource.html#cfn-some-resource-arn + */ + readonly arn?: string; } /** @@ -754,6 +762,7 @@ function CfnResourcePropsValidator(properties: any): cdk.ValidationResult { 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("arn", cdk.validateString)(properties.arn)); return errors.wrap("supplied properties not correct for \\"CfnResourceProps\\""); } @@ -761,7 +770,9 @@ function CfnResourcePropsValidator(properties: any): cdk.ValidationResult { function convertCfnResourcePropsToCloudFormation(properties: any): any { if (!cdk.canInspect(properties)) return properties; CfnResourcePropsValidator(properties).assertSuccess(); - return {}; + return { + "Arn": cdk.stringToCloudFormation(properties.arn) + }; } // @ts-ignore TS6133 @@ -774,6 +785,7 @@ function CfnResourcePropsFromCloudFormation(properties: any): cfn_parse.FromClou return new cfn_parse.FromCloudFormationResult(properties); } const ret = new cfn_parse.FromCloudFormationPropertyObject(); + ret.addPropertyResult("arn", "Arn", (properties.Arn != null ? cfn_parse.FromCloudFormation.getString(properties.Arn) : undefined)); ret.addUnrecognizedPropertiesAsExtra(properties); return ret; }" @@ -804,7 +816,7 @@ export interface ICfnResource extends constructs.IConstruct { * @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 { +export class CfnResource extends cdk.CfnResource implements cdk.IInspectable, ICfnResource { /** * The CloudFormation resource type name for this resource class. */ @@ -956,7 +968,7 @@ export interface ICfnResource extends constructs.IConstruct { * @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 { +export class CfnResource extends cdk.CfnResource implements cdk.IInspectable, ICfnResource { /** * The CloudFormation resource type name for this resource class. */ @@ -1083,3 +1095,365 @@ function CfnResourcePropsFromCloudFormation(properties: any): cfn_parse.FromClou return ret; }" `; + +exports[`resource with multiple primaryIdentifiers as properties 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 identifier of the resource. + * This property gets determined after the resource is created. + * + * @cloudformationAttribute Ref + */ + readonly attrId?: string; + + /** + * Another identifier of the resource. + * This property gets determined after the resource is created. + * + * @cloudformationAttribute Ref + */ + readonly attrAnotherId?: 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, ICfnResource { + /** + * 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. + * This property gets determined after the resource is created. + * + * @cloudformationAttribute Ref + */ + public readonly attrAnotherId?: string; + + /** + * The identifier of the resource. + * This property gets determined after the resource is created. + * + * @cloudformationAttribute Ref + */ + public readonly attrId?: string; + + /** + * Another identifier of the resource. + */ + public anotherId?: string; + + /** + * 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.attrAnotherId = cdk.Token.asString(cdk.Fn.select(1, cdk.Fn.split("|", this.ref))); + this.attrId = cdk.Token.asString(cdk.Fn.select(0, cdk.Fn.split("|", this.ref))); + this.anotherId = props.anotherId; + this.id = props.id; + } + + protected get cfnProperties(): Record { + return { + "anotherId": this.anotherId, + "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); + } +} + +/** + * 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 { + /** + * Another identifier of the resource. + * + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-some-resource.html#cfn-some-resource-anotherid + */ + readonly anotherId?: string; + + /** + * 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("anotherId", cdk.validateString)(properties.anotherId)); + 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 { + "AnotherId": cdk.stringToCloudFormation(properties.anotherId), + "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("anotherId", "AnotherId", (properties.AnotherId != null ? cfn_parse.FromCloudFormation.getString(properties.AnotherId) : undefined)); + ret.addPropertyResult("id", "Id", (properties.Id != null ? cfn_parse.FromCloudFormation.getString(properties.Id) : undefined)); + ret.addUnrecognizedPropertiesAsExtra(properties); + return ret; +}" +`; + +exports[`resource with optional primary identifier gets property from ref 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 identifier of the resource. + * This property gets determined after the resource is created. + * + * @cloudformationAttribute Ref + */ + readonly attrId?: 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, ICfnResource { + /** + * 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. + * This property gets determined after the resource is created. + * + * @cloudformationAttribute Ref + */ + public readonly attrId?: string; + + /** + * 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.attrId = cdk.Token.asString(this.ref); + 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); + } +} + +/** + * 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; +}" +`; diff --git a/tools/@aws-cdk/spec2cdk/test/resources.test.ts b/tools/@aws-cdk/spec2cdk/test/resources.test.ts index 42ae48865adf5..9bbc1152111b8 100644 --- a/tools/@aws-cdk/spec2cdk/test/resources.test.ts +++ b/tools/@aws-cdk/spec2cdk/test/resources.test.ts @@ -43,13 +43,13 @@ test('resource interface when primaryIdentifier is a property', () => { expect(rendered).toMatchSnapshot(); }); -test('resource interface when primaryIdentifier is an attribute', () => { +test('resource with optional primary identifier gets property from ref', () => { // GIVEN const resource = db.allocate('resource', { name: 'Resource', primaryIdentifier: ['Id'], - properties: {}, - attributes: { + attributes: {}, + properties: { Id: { type: { type: 'string' }, documentation: 'The identifier of the resource', @@ -69,18 +69,18 @@ test('resource interface when primaryIdentifier is an attribute', () => { expect(rendered).toMatchSnapshot(); }); -test('resource interface with multiple primaryIdentifiers', () => { +test('resource with multiple primaryIdentifiers as properties', () => { // GIVEN const resource = db.allocate('resource', { name: 'Resource', - primaryIdentifier: ['Id', 'Another'], - properties: {}, - attributes: { + primaryIdentifier: ['Id', 'AnotherId'], + attributes: {}, + properties: { Id: { type: { type: 'string' }, documentation: 'The identifier of the resource', }, - Another: { + AnotherId: { type: { type: 'string' }, documentation: 'Another identifier of the resource', }, @@ -99,7 +99,33 @@ test('resource interface with multiple primaryIdentifiers', () => { expect(rendered).toMatchSnapshot(); }); -test('resource interface with "Arn"', () => { +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).toMatchSnapshot(); +}); + +test('resource interface with multiple primaryIdentifiers', () => { // GIVEN const resource = db.allocate('resource', { name: 'Resource', @@ -110,9 +136,9 @@ test('resource interface with "Arn"', () => { type: { type: 'string' }, documentation: 'The identifier of the resource', }, - Arn: { + Another: { type: { type: 'string' }, - documentation: 'The arn for the resource', + documentation: 'Another identifier of the resource', }, }, cloudFormationType: 'AWS::Some::Resource', @@ -129,7 +155,7 @@ test('resource interface with "Arn"', () => { expect(rendered).toMatchSnapshot(); }); -test('resource interface with "ResourceArn"', () => { +test('resource interface with "Arn"', () => { // GIVEN const resource = db.allocate('resource', { name: 'Resource', @@ -140,7 +166,7 @@ test('resource interface with "ResourceArn"', () => { type: { type: 'string' }, documentation: 'The identifier of the resource', }, - ResourceArn: { + Arn: { type: { type: 'string' }, documentation: 'The arn for the resource', }, @@ -189,6 +215,37 @@ test('resource interface with "Arn"', () => { expect(rendered).toMatchSnapshot(); }); +test('resource interface with Arn as a property and not a primaryIdentifier', () => { + // GIVEN + const resource = db.allocate('resource', { + name: 'Resource', + primaryIdentifier: ['Id'], + properties: { + Arn: { + type: { type: 'string' }, + documentation: 'The arn for the resource', + }, + }, + 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).toMatchSnapshot(); +}); + test('resource interface with Arn as primaryIdentifier', () => { // GIVEN const resource = db.allocate('resource', {