diff --git a/packages/@aws-cdk/aws-apigateway/test/test.resource.ts b/packages/@aws-cdk/aws-apigateway/test/test.resource.ts index 627998d524f3c..c306bcd281098 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.resource.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.resource.ts @@ -286,4 +286,4 @@ export = { } } -}; \ No newline at end of file +}; diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts index 7d762ca10c3dc..34cdc9a2b4bcd 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts @@ -141,8 +141,8 @@ export class Alarm extends Resource implements IAlarm { ...metricJson(props.metric) }); - this.alarmArn = alarm.alarmArn; - this.alarmName = alarm.alarmName; + this.alarmArn = alarm.attrArn; + this.alarmName = alarm.ref; this.metric = props.metric; this.annotation = { // tslint:disable-next-line:max-line-length diff --git a/packages/@aws-cdk/aws-iam/lib/group.ts b/packages/@aws-cdk/aws-iam/lib/group.ts index c5de7b2352384..2a8e1a185cf9b 100644 --- a/packages/@aws-cdk/aws-iam/lib/group.ts +++ b/packages/@aws-cdk/aws-iam/lib/group.ts @@ -9,11 +9,15 @@ import { AttachedPolicies, undefinedIfEmpty } from './util'; export interface IGroup extends IIdentity { /** + * Returns the IAM Group Name + * * @attribute */ readonly groupName: string; /** + * Returns the IAM Group ARN + * * @attribute */ readonly groupArn: string; @@ -131,8 +135,8 @@ export class Group extends GroupBase { path: props.path, }); - this.groupName = group.groupName; - this.groupArn = group.groupArn; + this.groupName = group.ref; + this.groupArn = group.attrArn; } /** diff --git a/packages/@aws-cdk/aws-iam/lib/role.ts b/packages/@aws-cdk/aws-iam/lib/role.ts index 56bcd5de1d2bc..d0bba2eb46421 100644 --- a/packages/@aws-cdk/aws-iam/lib/role.ts +++ b/packages/@aws-cdk/aws-iam/lib/role.ts @@ -206,9 +206,9 @@ export class Role extends Resource implements IRole { maxSessionDuration: props.maxSessionDurationSec, }); - this.roleId = role.roleId; - this.roleArn = role.roleArn; - this.roleName = role.roleName; + this.roleId = role.attrRoleId; + this.roleArn = role.attrArn; + this.roleName = role.ref; this.policyFragment = new ArnPrincipal(this.roleArn).policyFragment; function _flatten(policies?: { [name: string]: PolicyDocument }) { diff --git a/packages/@aws-cdk/aws-iam/lib/user.ts b/packages/@aws-cdk/aws-iam/lib/user.ts index 759e99854ffbf..69d7f405f5cde 100644 --- a/packages/@aws-cdk/aws-iam/lib/user.ts +++ b/packages/@aws-cdk/aws-iam/lib/user.ts @@ -112,8 +112,8 @@ export class User extends Resource implements IIdentity { loginProfile: this.parseLoginProfile(props) }); - this.userName = user.userName; - this.userArn = user.userArn; + this.userName = user.ref; + this.userArn = user.attrArn; this.policyFragment = new ArnPrincipal(this.userArn).policyFragment; if (props.groups) { diff --git a/packages/@aws-cdk/aws-kms/lib/key.ts b/packages/@aws-cdk/aws-kms/lib/key.ts index 9950da6b13d05..05100be89d0f0 100644 --- a/packages/@aws-cdk/aws-kms/lib/key.ts +++ b/packages/@aws-cdk/aws-kms/lib/key.ts @@ -224,7 +224,7 @@ export class Key extends KeyBase { keyPolicy: this.policy, }); - this.keyArn = resource.keyArn; + this.keyArn = resource.attrArn; resource.options.deletionPolicy = props.retain === false ? DeletionPolicy.Delete : DeletionPolicy.Retain; diff --git a/packages/@aws-cdk/cdk/lib/cfn-resource.ts b/packages/@aws-cdk/cdk/lib/cfn-resource.ts index c8a8fb292dafd..c7410abc06531 100644 --- a/packages/@aws-cdk/cdk/lib/cfn-resource.ts +++ b/packages/@aws-cdk/cdk/lib/cfn-resource.ts @@ -67,22 +67,10 @@ export class CfnResource extends CfnRefElement { */ protected readonly properties: any; - /** - * AWS resource property overrides. - * - * During synthesis, the method "renderProperties(this.overrides)" is called - * with this object, and merged on top of the output of - * "renderProperties(this.properties)". - * - * Derived classes should expose a strongly-typed version of this object as - * a public property called `propertyOverrides`. - */ - protected readonly untypedPropertyOverrides: any = { }; - /** * An object to be merged on top of the entire resource definition. */ - private readonly rawOverrides: any = { }; + private readonly rawOverrides: any = {}; /** * Logical IDs of dependencies. @@ -103,7 +91,7 @@ export class CfnResource extends CfnRefElement { } this.resourceType = props.type; - this.properties = props.properties || { }; + this.properties = props.properties || {}; // if aws:cdk:enable-path-metadata is set, embed the current construct's // path in the CloudFormation template, so it will be possible to trace @@ -146,7 +134,7 @@ export class CfnResource extends CfnRefElement { // object overwrite it with an object. const isObject = curr[key] != null && typeof(curr[key]) === 'object' && !Array.isArray(curr[key]); if (!isObject) { - curr[key] = { }; + curr[key] = {}; } curr = curr[key]; @@ -205,33 +193,21 @@ export class CfnResource extends CfnRefElement { */ public _toCloudFormation(): object { try { - // merge property overrides onto properties and then render (and validate). - const tags = TagManager.isTaggable(this) ? this.tags.renderTags() : undefined; - const properties = deepMerge( - this.properties || {}, - { tags }, - this.untypedPropertyOverrides - ); - const ret = { Resources: { // Post-Resolve operation since otherwise deepMerge is going to mix values into // the Token objects returned by ignoreEmpty. [this.logicalId]: new PostResolveToken({ Type: this.resourceType, - Properties: ignoreEmpty(properties), + Properties: ignoreEmpty(this.renderProperties()), DependsOn: ignoreEmpty(renderDependsOn(this.dependsOn)), - CreationPolicy: capitalizePropertyNames(this, this.options.creationPolicy), + CreationPolicy: capitalizePropertyNames(this, this.options.creationPolicy), UpdatePolicy: capitalizePropertyNames(this, this.options.updatePolicy), UpdateReplacePolicy: capitalizePropertyNames(this, this.options.updateReplacePolicy), DeletionPolicy: capitalizePropertyNames(this, this.options.deletionPolicy), Metadata: ignoreEmpty(this.options.metadata), Condition: this.options.condition && this.options.condition.logicalId }, props => { - // let derived classes to influence how properties are rendered (e.g. change capitalization) - props.Properties = this.renderProperties(props.Properties); - - // merge overrides *after* rendering return deepMerge(props, this.rawOverrides); }) } @@ -262,8 +238,19 @@ export class CfnResource extends CfnRefElement { } } - protected renderProperties(properties: any): { [key: string]: any } { - return properties; + protected renderProperties(): { [key: string]: any } { + const tags = TagManager.isTaggable(this) ? this.tags.renderTags() : {}; + return deepMerge(this.properties || {}, {tags}); + } + + /** + * Return properties modified after initiation + * + * Resources that expose mutable properties should override this function to + * collect and return the properties object for this resource. + */ + protected get updatedProperites(): { [key: string]: any } { + return this.properties; } protected validateProperties(_properties: any) { @@ -339,7 +326,7 @@ export function deepMerge(target: any, ...sources: any[]) { // if the value at the target is not an object, override it with an // object so we can continue the recursion if (typeof(target[key]) !== 'object') { - target[key] = { }; + target[key] = {}; } deepMerge(target[key], value); diff --git a/packages/@aws-cdk/cdk/test/test.resource.ts b/packages/@aws-cdk/cdk/test/test.resource.ts index d66c13f871c63..6eaab15af1494 100644 --- a/packages/@aws-cdk/cdk/test/test.resource.ts +++ b/packages/@aws-cdk/cdk/test/test.resource.ts @@ -582,7 +582,7 @@ export = { test.done(); }, - 'untypedPropertyOverrides': { + 'updatedProperties': { 'can be used by derived classes to specify overrides before render()'(test: Test) { const stack = new Stack(); @@ -591,7 +591,7 @@ export = { prop1: 'foo' }); - r.setProperty('prop2', 'bar'); + r.prop2 = 'bar'; test.deepEqual(toCloudFormation(stack), { Resources: { MyResource: @@ -605,7 +605,7 @@ export = { const r = new CustomizableResource(stack, 'MyResource'); - r.setProperty('prop3', 'zoo'); + r.prop3 = 'zoo'; test.deepEqual(toCloudFormation(stack), { Resources: { MyResource: @@ -619,8 +619,8 @@ export = { const r = new CustomizableResource(stack, 'MyResource', { }); - r.setProperty('prop3', 'zoo'); - r.setProperty('prop2', 'hey'); + r.prop3 = 'zoo'; + r.prop2 = 'hey'; test.deepEqual(toCloudFormation(stack), { Resources: { MyResource: @@ -696,6 +696,10 @@ class Counter extends CfnResource { public increment(by = 1) { this.properties.Count += by; } + + protected get updatedProperites(): { [key: string]: any } { + return {Count: this.properties.Count}; + } } function withoutHash(logId: string) { @@ -703,19 +707,41 @@ function withoutHash(logId: string) { } class CustomizableResource extends CfnResource { + public prop1: any; + public prop2: any; + public prop3: any; + constructor(scope: Construct, id: string, props?: any) { super(scope, id, { type: 'MyResourceType', properties: props }); + if (props !== undefined) { + this.prop1 = props.prop1; + this.prop2 = props.prop2; + this.prop3 = props.prop3; + } } - public setProperty(key: string, value: any) { - this.untypedPropertyOverrides[key] = value; + public renderProperties(): { [key: string]: any } { + const props = this.updatedProperites; + const render: { [key: string]: any } = {}; + for (const key of Object.keys(props)) { + render[key.toUpperCase()] = props[key]; + } + return render; } - public renderProperties(properties: any) { - return { - PROP1: properties.prop1, - PROP2: properties.prop2, - PROP3: properties.prop3 + protected get updatedProperites(): { [key: string]: any } { + const props: { [key: string]: any } = { + prop1: this.prop1, + prop2: this.prop2, + prop3: this.prop3, }; + const cleanProps: { [key: string]: any } = { }; + for (const key of Object.keys(props)) { + if (props[key] === undefined) { + continue; + } + cleanProps[key] = props[key]; + } + return cleanProps; } } diff --git a/tools/awslint/lib/rules/cfn-resource.ts b/tools/awslint/lib/rules/cfn-resource.ts index a77b4f92d6183..fee7e4a4d10b5 100644 --- a/tools/awslint/lib/rules/cfn-resource.ts +++ b/tools/awslint/lib/rules/cfn-resource.ts @@ -67,12 +67,7 @@ export class CfnResourceReflection { this.namespace = fullname.split('::').slice(0, 2).join('::'); - // special-case - const basename = this.basename - .replace(/VPC/g, 'Vpc') - .replace(/DB/g, 'Db'); - - this.attributePrefix = basename[0].toLowerCase() + basename.slice(1); + this.attributePrefix = 'attr'; this.attributeNames = cls.ownProperties .filter(p => (p.docs.docs.custom || {}).cloudformationAttribute) @@ -89,9 +84,6 @@ export class CfnResourceReflection { return 'securityGroupId'; } - const cfnName = name.startsWith(this.basename) ? name.slice(this.basename.length) : name; - - // if the CFN attribute name already have the type name as a prefix (i.e. RoleId), we only take the "Id" as the "name". - return this.attributePrefix + camelcase(cfnName, { pascalCase: true }); + return this.attributePrefix + camelcase(name, { pascalCase: true }); } -} \ No newline at end of file +} diff --git a/tools/awslint/lib/rules/construct.ts b/tools/awslint/lib/rules/construct.ts index 341b005bdfa4b..ce7cc920e9a01 100644 --- a/tools/awslint/lib/rules/construct.ts +++ b/tools/awslint/lib/rules/construct.ts @@ -292,4 +292,4 @@ constructLinter.add({ e.assert(!property.type.isAny, `${e.ctx.propsFqn}.${property.name}`); } } -}); \ No newline at end of file +}); diff --git a/tools/awslint/lib/rules/resource.ts b/tools/awslint/lib/rules/resource.ts index 869ffd675ee58..a05e5f7e436ca 100644 --- a/tools/awslint/lib/rules/resource.ts +++ b/tools/awslint/lib/rules/resource.ts @@ -69,15 +69,16 @@ export class ResourceReflection { private findAttributeProperties(): Attribute[] { const result = new Array(); + const attrPrefix = this.basename.charAt(0).toLowerCase() + this.basename.slice(1); for (const p of this.construct.classType.allProperties) { if (p.protected) { continue; // skip any protected properties } - // an attribute property is a property which starts with the type name - // (e.g. "bucketXxx") and/or has an @attribute doc tag. + // an attribute property is a property which starts with `attr` + // and/or has an @attribute doc tag. const tag = getDocTag(p, 'attribute'); - if (!p.name.startsWith(this.cfn.attributePrefix) && !tag) { + if (!p.name.startsWith(attrPrefix) && !tag) { continue; } @@ -152,7 +153,12 @@ resourceLinter.add({ message: 'resources must represent all cloudformation attributes as attribute properties. missing property: ', eval: e => { for (const name of e.ctx.cfn.attributeNames) { - const found = e.ctx.attributes.find(a => a.names.includes(name)); + const basename = e.ctx.basename.charAt(0).toLowerCase() + e.ctx.basename.slice(1); + const stripAttr = name.replace('attr', ''); + const lookup = stripAttr.toLowerCase().startsWith(basename) ? + stripAttr.charAt(0).toLowerCase() + stripAttr.slice(1) : + basename + stripAttr; + const found = e.ctx.attributes.find(a => a.names.includes(lookup)); e.assert(found, `${e.ctx.fqn}.${name}`, name); } } diff --git a/tools/cfn2ts/lib/codegen.ts b/tools/cfn2ts/lib/codegen.ts index 23d66fab92150..82795ad74e938 100644 --- a/tools/cfn2ts/lib/codegen.ts +++ b/tools/cfn2ts/lib/codegen.ts @@ -69,8 +69,6 @@ export default class CodeGenerator { for (const name of Object.keys(this.spec.ResourceTypes).sort()) { const resourceType = this.spec.ResourceTypes[name]; - this.validateRefKindPresence(name, resourceType); - const cfnName = SpecName.parse(name); const resourceName = genspec.CodeName.forCfnResource(cfnName, this.affix); this.code.line(); @@ -121,7 +119,7 @@ export default class CodeGenerator { this.docLink(spec.Documentation, `Properties for defining a \`${resourceContext.specName!.fqn}\``); this.code.openBlock(`export interface ${name.className}`); - const conversionTable = this.emitPropsTypeProperties(resourceContext, spec.Properties); + const conversionTable = this.emitPropsTypeProperties(resourceContext, spec.Properties, Container.Interface); this.code.closeBlock(); @@ -138,13 +136,23 @@ export default class CodeGenerator { * * Return a mapping of { originalName -> newName }. */ - private emitPropsTypeProperties(resource: genspec.CodeName, propertiesSpec: { [name: string]: schema.Property }): Dictionary { + private emitPropsTypeProperties( + resource: genspec.CodeName, + propertiesSpec: { [name: string]: schema.Property }, + container: Container): Dictionary { const propertyMap: Dictionary = {}; Object.keys(propertiesSpec).sort(propertyComparator).forEach(propName => { + this.code.line(); const propSpec = propertiesSpec[propName]; const additionalDocs = resource.specName!.relativeName(propName).fqn; - const newName = this.emitProperty(resource, propName, propSpec, quoteCode(additionalDocs)); + const newName = this.emitProperty({ + context: resource, + propName, + spec: propSpec, + additionalDocs: quoteCode(additionalDocs)}, + container + ); propertyMap[propName] = newName; }); return propertyMap; @@ -223,7 +231,7 @@ export default class CodeGenerator { this.code.line(); this.docLink(undefined, `@cloudformationAttribute ${attributeName}`); - const attr = genspec.attributeDefinition(resourceName, attributeName, attributeSpec); + const attr = genspec.attributeDefinition(attributeName, attributeSpec); this.code.line(`public readonly ${attr.propertyName}: ${attr.attributeType};`); @@ -231,31 +239,10 @@ export default class CodeGenerator { } } - // - // Ref attribute - // - if (spec.RefKind !== schema.SpecialRefKind.None) { - const refAttribute = genspec.refAttributeDefinition(resourceName, spec.RefKind!); - - // If there's already an attribute with the same name, ref is not needed - if (!attributes.some(a => a.propertyName === refAttribute.propertyName)) { - this.code.line(`public readonly ${refAttribute.propertyName}: ${refAttribute.attributeType};`); - attributes.push(refAttribute); - } - } - // set the TagType to help format tags later - const tagEnum = tagType(spec); - if (tagEnum !== `${TAG_TYPE}.NotTaggable`) { - this.code.line(); - this.code.line('/**'); - this.code.line(' * The `TagManager` handles setting, removing and formatting tags'); - this.code.line(' *'); - this.code.line(' * Tags should be managed either passing them as properties during'); - this.code.line(' * initiation or by calling methods on this object. If both techniques are'); - this.code.line(' * used only the tags from the TagManager will be used. `Tag` (aspect)'); - this.code.line(' * will use the manager.'); - this.code.line(' */'); - this.code.line(`public readonly tags: ${TAG_MANAGER};`); + // set class properties to match CloudFormation Properties spec + let propMap; + if (propsType) { + propMap = this.emitPropsTypeProperties(resourceName, spec.Properties!, Container.Class); } // @@ -271,7 +258,7 @@ export default class CodeGenerator { this.code.line(` * @param props - resource properties`); this.code.line(' */'); const optionalProps = spec.Properties && !Object.values(spec.Properties).some(p => p.Required || false); - const propsArgument = propsType ? `, props${optionalProps ? '?' : ''}: ${propsType.className}` : ''; + const propsArgument = propsType ? `, props: ${propsType.className}${optionalProps ? ' = {}' : ''}` : ''; this.code.openBlock(`constructor(scope: ${CONSTRUCT_CLASS}, id: string${propsArgument})`); this.code.line(`super(scope, id, { type: ${resourceName.className}.resourceTypeName${propsType ? ', properties: props' : ''} });`); // verify all required properties @@ -308,20 +295,24 @@ export default class CodeGenerator { if (deprecated) { this.code.line(`this.node.addWarning('DEPRECATION: ${deprecation}');`); } - if (tagEnum !== `${TAG_TYPE}.NotTaggable`) { - this.code.line('const tags = props === undefined ? undefined : props.tags;'); - this.code.line(`this.tags = new ${TAG_MANAGER}(${tagEnum}, ${resourceTypeName}, tags);`); - } + // initialize all property class members + if (propsType && propMap) { + this.code.line(); + for (const prop of Object.values(propMap)) { + if (prop === 'tags') { + this.code.line(`this.tags = new ${TAG_MANAGER}(${tagType(spec)}, ${resourceTypeName}, props.tags);`); + } else { + this.code.line(`this.${prop} = props.${prop};`); + } + } + } this.code.closeBlock(); - // - // propertyOverrides - // - - if (propsType) { + // setup render properties + if (propsType && propMap) { this.code.line(); - this.emitCloudFormationPropertiesOverride(propsType); + this.emitCloudFormationProperties(propsType, propMap); } this.closeClass(resourceName); @@ -334,12 +325,18 @@ export default class CodeGenerator { * * Since resolve() deep-resolves, we only need to do this once. */ - private emitCloudFormationPropertiesOverride(propsType: genspec.CodeName) { - this.code.openBlock(`public get propertyOverrides(): ${propsType.className}`); - this.code.line(`return this.untypedPropertyOverrides;`); - this.code.closeBlock(); - - this.code.openBlock('protected renderProperties(properties: any): { [key: string]: any } '); + private emitCloudFormationProperties(propsType: genspec.CodeName, propMap: Dictionary) { + this.code.openBlock('protected renderProperties(): { [key: string]: any } '); + this.code.indent('const properties = {'); + for (const prop of Object.values(propMap)) { + // handle tag rendering because of special cases + if (prop === 'tags') { + this.code.line(`${prop}: this.tags.renderTags(),`); + continue; + } + this.code.line(`${prop}: this.${prop},`); + } + this.code.unindent('};'); this.code.line(`return ${genspec.cfnMapperName(propsType).fqn}(properties);`); this.code.closeBlock(); } @@ -514,15 +511,43 @@ export default class CodeGenerator { this.code.closeBlock(); } - private emitProperty(context: genspec.CodeName, propName: string, spec: schema.Property, additionalDocs: string): string { - const question = spec.Required ? '' : '?'; - const javascriptPropertyName = genspec.cloudFormationToScriptName(propName); + private emitInterfaceProperty(props: EmitPropertyProps): string { + const javascriptPropertyName = genspec.cloudFormationToScriptName(props.propName); - this.docLink(spec.Documentation, additionalDocs); - this.code.line(`readonly ${javascriptPropertyName}${question}: ${this.findNativeType(context, spec, propName)};`); + this.docLink(props.spec.Documentation, props.additionalDocs); + const line = `: ${this.findNativeType(props.context, props.spec, props.propName)};`; + const question = props.spec.Required ? '' : '?'; + this.code.line(`readonly ${javascriptPropertyName}${question}${line}`); return javascriptPropertyName; } + + private emitClassProperty(props: EmitPropertyProps): string { + const javascriptPropertyName = genspec.cloudFormationToScriptName(props.propName); + + this.docLink(props.spec.Documentation, props.additionalDocs); + const question = props.spec.Required ? ';' : ' | undefined;'; + const line = `: ${this.findNativeType(props.context, props.spec, props.propName)}${question}`; + if (props.propName === 'Tags' && schema.isTagProperty(props.spec)) { + this.code.line(`public readonly tags: ${TAG_MANAGER};`); + } else { + this.code.line(`public ${javascriptPropertyName}${line}`); + } + return javascriptPropertyName; + } + + private emitProperty(props: EmitPropertyProps, container: Container): string { + switch (container) { + case Container.Class: + return this.emitClassProperty(props); + case Container.Interface: + return this.emitInterfaceProperty(props); + default: + throw new Error(`Unsupported container ${container}`); + } + + } + private beginNamespace(type: genspec.CodeName) { if (type.namespace) { const parts = type.namespace.split('.'); @@ -555,7 +580,12 @@ export default class CodeGenerator { Object.keys(propTypeSpec.Properties).forEach(propName => { const propSpec = propTypeSpec.Properties[propName]; const additionalDocs = quoteCode(`${typeName.fqn}.${propName}`); - const newName = this.emitProperty(resourceContext, propName, propSpec, additionalDocs); + const newName = this.emitInterfaceProperty({ + context: resourceContext, + propName, + spec: propSpec, + additionalDocs, + }); conversionTable[propName] = newName; }); } @@ -648,12 +678,6 @@ export default class CodeGenerator { this.code.line(' */'); return; } - - private validateRefKindPresence(name: string, resourceType: schema.ResourceType): any { - if (!resourceType.RefKind) { // Both empty string and undefined - throw new Error(`Resource ${name} does not have a RefKind; please run in @aws-cdk/cfnspec: npm run set-refkind ${name} Arn|Id|None|...`); - } - } } /** @@ -702,3 +726,15 @@ function tagType(resource: schema.ResourceType): string { } return `${TAG_TYPE}.NotTaggable`; } + +enum Container { + Interface = 'INTERFACE', + Class = 'CLASS', +} + +interface EmitPropertyProps { + context: genspec.CodeName; + propName: string; + spec: schema.Property; + additionalDocs: string; +} diff --git a/tools/cfn2ts/lib/genspec.ts b/tools/cfn2ts/lib/genspec.ts index 4603e4b9b092f..9cc7a4bacb8c4 100644 --- a/tools/cfn2ts/lib/genspec.ts +++ b/tools/cfn2ts/lib/genspec.ts @@ -167,9 +167,10 @@ export function validatorName(typeName: CodeName): CodeName { * - The type we will generate for the attribute, including its base class and docs. * - The property name we will use to refer to the attribute. */ -export function attributeDefinition(resourceName: CodeName, attributeName: string, spec: schema.Attribute): Attribute { - const descriptiveName = descriptiveAttributeName(resourceName, attributeName); // "BucketArn" - const propertyName = cloudFormationToScriptName(descriptiveName); // "bucketArn" +export function attributeDefinition(attributeName: string, spec: schema.Attribute): Attribute { + const descriptiveName = attributeName.replace(/\./g, ''); + const suffixName = codemaker.toPascalCase(cloudFormationToScriptName(descriptiveName)); + const propertyName = `attr${suffixName}`; // "attrArn" let attrType: string; if ('PrimitiveType' in spec && spec.PrimitiveType === 'String') { @@ -186,43 +187,6 @@ export function attributeDefinition(resourceName: CodeName, attributeName: strin return new Attribute(propertyName, attrType, constructorArguments); } -/** - * Return an attribute definition name for the RefKind for this class - */ -export function refAttributeDefinition(resourceName: CodeName, refKind: string): Attribute { - const propertyName = codemaker.toCamelCase(descriptiveAttributeName(resourceName, refKind)); - - const constructorArguments = 'this.ref'; - - return new Attribute(propertyName, 'string', constructorArguments); -} - -/** - * In the CDK, attribute names will be prefixed with the name of the resource (unless they already - * have the name of the resource as a prefix). There are a few reasons for that, mainly to avoid name - * collisions with base class properties, but also to allow certain constructs to expose multiple attributes - * of different sub-resources using the same names (i.e. 'bucketArn' and 'topicArn' can co-exist while 'arn' and 'arn' cannot). - */ -function descriptiveAttributeName(resourceName: CodeName, attributeName: string): string { - // remove '.'s - attributeName = attributeName.replace(/\./g, ''); - - const resName = resourceName.specName!.resourceName; - - // special case (someone was smart) - if (resName === 'SecurityGroup' && attributeName === 'GroupId') { - attributeName = 'SecurityGroupId'; - } - - // if property name already starts with the resource name, then just use it as-is - // otherwise, prepend the resource name - if (!attributeName.toLowerCase().startsWith(resName.toLowerCase())) { - attributeName = `${resName}${codemaker.toPascalCase(attributeName)}`; - } - - return attributeName; -} - /** * Convert a CloudFormation name to a nice TypeScript name *