diff --git a/packages/@aws-cdk/mixins-preview/lib/services/aws-cases/index.ts b/packages/@aws-cdk/mixins-preview/lib/services/aws-cases/index.ts new file mode 100644 index 0000000000000..a3d81d506527a --- /dev/null +++ b/packages/@aws-cdk/mixins-preview/lib/services/aws-cases/index.ts @@ -0,0 +1 @@ +export * as mixins from './mixins'; diff --git a/packages/@aws-cdk/mixins-preview/lib/services/aws-cases/mixins.ts b/packages/@aws-cdk/mixins-preview/lib/services/aws-cases/mixins.ts new file mode 100644 index 0000000000000..6d0a7c5045bf1 --- /dev/null +++ b/packages/@aws-cdk/mixins-preview/lib/services/aws-cases/mixins.ts @@ -0,0 +1 @@ +export * from './cfn-props-mixins.generated'; diff --git a/packages/@aws-cdk/mixins-preview/lib/services/aws-codebuild/events.ts b/packages/@aws-cdk/mixins-preview/lib/services/aws-codebuild/events.ts new file mode 100644 index 0000000000000..40958b04aa549 --- /dev/null +++ b/packages/@aws-cdk/mixins-preview/lib/services/aws-codebuild/events.ts @@ -0,0 +1 @@ +export * from './events.generated'; diff --git a/packages/@aws-cdk/mixins-preview/lib/services/aws-codebuild/index.ts b/packages/@aws-cdk/mixins-preview/lib/services/aws-codebuild/index.ts index a3d81d506527a..d69d5ecb489d3 100644 --- a/packages/@aws-cdk/mixins-preview/lib/services/aws-codebuild/index.ts +++ b/packages/@aws-cdk/mixins-preview/lib/services/aws-codebuild/index.ts @@ -1 +1,2 @@ +export * as events from './events'; export * as mixins from './mixins'; diff --git a/packages/@aws-cdk/mixins-preview/lib/services/aws-elasticache/mixins.ts b/packages/@aws-cdk/mixins-preview/lib/services/aws-elasticache/mixins.ts index 6d0a7c5045bf1..44c216ff3f090 100644 --- a/packages/@aws-cdk/mixins-preview/lib/services/aws-elasticache/mixins.ts +++ b/packages/@aws-cdk/mixins-preview/lib/services/aws-elasticache/mixins.ts @@ -1 +1,2 @@ export * from './cfn-props-mixins.generated'; +export * from './logs-delivery-mixins.generated'; diff --git a/packages/@aws-cdk/mixins-preview/lib/services/index.ts b/packages/@aws-cdk/mixins-preview/lib/services/index.ts index 9c47c8d532f4a..6a72ae9f5d64e 100644 --- a/packages/@aws-cdk/mixins-preview/lib/services/index.ts +++ b/packages/@aws-cdk/mixins-preview/lib/services/index.ts @@ -34,6 +34,7 @@ export * as aws_bedrock from './aws-bedrock'; export * as aws_bedrockagentcore from './aws-bedrockagentcore'; export * as aws_billingconductor from './aws-billingconductor'; export * as aws_budgets from './aws-budgets'; +export * as aws_cases from './aws-cases'; export * as aws_cassandra from './aws-cassandra'; export * as aws_ce from './aws-ce'; export * as aws_certificatemanager from './aws-certificatemanager'; diff --git a/packages/@aws-cdk/mixins-preview/package.json b/packages/@aws-cdk/mixins-preview/package.json index 6e3bd3b6f56c2..67d8fc873377c 100644 --- a/packages/@aws-cdk/mixins-preview/package.json +++ b/packages/@aws-cdk/mixins-preview/package.json @@ -84,6 +84,8 @@ "./aws-billingconductor/mixins": "./lib/services/aws-billingconductor/mixins.js", "./aws-budgets": "./lib/services/aws-budgets/index.js", "./aws-budgets/mixins": "./lib/services/aws-budgets/mixins.js", + "./aws-cases": "./lib/services/aws-cases/index.js", + "./aws-cases/mixins": "./lib/services/aws-cases/mixins.js", "./aws-cassandra": "./lib/services/aws-cassandra/index.js", "./aws-cassandra/mixins": "./lib/services/aws-cassandra/mixins.js", "./aws-ce": "./lib/services/aws-ce/index.js", @@ -110,6 +112,7 @@ "./aws-codeartifact": "./lib/services/aws-codeartifact/index.js", "./aws-codeartifact/mixins": "./lib/services/aws-codeartifact/mixins.js", "./aws-codebuild": "./lib/services/aws-codebuild/index.js", + "./aws-codebuild/events": "./lib/services/aws-codebuild/events.js", "./aws-codebuild/mixins": "./lib/services/aws-codebuild/mixins.js", "./aws-codecommit": "./lib/services/aws-codecommit/index.js", "./aws-codecommit/events": "./lib/services/aws-codecommit/events.js", @@ -658,7 +661,7 @@ "@aws-cdk/integ-runner": "^2.192.2", "@aws-cdk/integ-tests-alpha": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@aws-cdk/service-spec-types": "^0.0.199", + "@aws-cdk/service-spec-types": "^0.0.201", "@aws-cdk/spec2cdk": "0.0.0", "@cdklabs/tskb": "^0.0.4", "@cdklabs/typewriter": "^0.0.14", diff --git a/packages/aws-cdk-lib/aws-appmesh/lib/virtual-gateway.ts b/packages/aws-cdk-lib/aws-appmesh/lib/virtual-gateway.ts index 14bd859776d92..76ee651cc9f73 100644 --- a/packages/aws-cdk-lib/aws-appmesh/lib/virtual-gateway.ts +++ b/packages/aws-cdk-lib/aws-appmesh/lib/virtual-gateway.ts @@ -112,7 +112,6 @@ abstract class VirtualGatewayBase extends cdk.Resource implements IVirtualGatewa public get virtualGatewayRef(): VirtualGatewayReference { return { virtualGatewayArn: this.virtualGatewayArn, - virtualGatewayId: this.virtualGatewayName, }; } diff --git a/packages/aws-cdk-lib/aws-appmesh/lib/virtual-node.ts b/packages/aws-cdk-lib/aws-appmesh/lib/virtual-node.ts index 3ac73586244f6..0f782e9c303d1 100644 --- a/packages/aws-cdk-lib/aws-appmesh/lib/virtual-node.ts +++ b/packages/aws-cdk-lib/aws-appmesh/lib/virtual-node.ts @@ -125,7 +125,6 @@ abstract class VirtualNodeBase extends cdk.Resource implements IVirtualNode { public get virtualNodeRef(): VirtualNodeReference { return { virtualNodeArn: this.virtualNodeArn, - virtualNodeId: this.virtualNodeName, }; } diff --git a/packages/aws-cdk-lib/aws-appmesh/test/virtual-gateway.test.ts b/packages/aws-cdk-lib/aws-appmesh/test/virtual-gateway.test.ts index bb2f5d2fbcd22..3ef588d1eca70 100644 --- a/packages/aws-cdk-lib/aws-appmesh/test/virtual-gateway.test.ts +++ b/packages/aws-cdk-lib/aws-appmesh/test/virtual-gateway.test.ts @@ -977,12 +977,7 @@ describe('virtual gateway', () => { { Action: 'appmesh:StreamAggregatedResources', Effect: 'Allow', - Resource: { - 'Fn::GetAtt': [ - 'testGateway', - 'Arn', - ], - }, + Resource: { Ref: 'testGateway' }, }, ], }, diff --git a/packages/aws-cdk-lib/aws-cases/.jsiirc.json b/packages/aws-cdk-lib/aws-cases/.jsiirc.json new file mode 100644 index 0000000000000..e5e6b034f48cf --- /dev/null +++ b/packages/aws-cdk-lib/aws-cases/.jsiirc.json @@ -0,0 +1,13 @@ +{ + "targets": { + "java": { + "package": "software.amazon.awscdk.services.cases" + }, + "dotnet": { + "namespace": "Amazon.CDK.AWS.Cases" + }, + "python": { + "module": "aws_cdk.aws_cases" + } + } +} diff --git a/packages/aws-cdk-lib/aws-cases/README.md b/packages/aws-cdk-lib/aws-cases/README.md new file mode 100644 index 0000000000000..a826283f53caf --- /dev/null +++ b/packages/aws-cdk-lib/aws-cases/README.md @@ -0,0 +1,39 @@ +# AWS::Cases Construct Library + + +--- + +![cfn-resources: Stable](https://img.shields.io/badge/cfn--resources-stable-success.svg?style=for-the-badge) + +> All classes with the `Cfn` prefix in this module ([CFN Resources]) are always stable and safe to use. +> +> [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib + +--- + + + +This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. + +```ts nofixture +import * as cases from 'aws-cdk-lib/aws-cases'; +``` + + + +There are no official hand-written ([L2](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) constructs for this service yet. Here are some suggestions on how to proceed: + +- Search [Construct Hub for Cases construct libraries](https://constructs.dev/search?q=cases) +- Use the automatically generated [L1](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_l1_using) constructs, in the same way you would use [the CloudFormation AWS::Cases resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_Cases.html) directly. + + + + +There are no hand-written ([L2](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) constructs for this service yet. +However, you can still use the automatically generated [L1](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_l1_using) constructs, and use this service exactly as you would using CloudFormation directly. + +For more information on the resources and properties available for this service, see the [CloudFormation documentation for AWS::Cases](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_Cases.html). + +(Read the [CDK Contributing Guide](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and submit an RFC if you are interested in contributing to this construct library.) + + diff --git a/packages/aws-cdk-lib/aws-cases/index.ts b/packages/aws-cdk-lib/aws-cases/index.ts new file mode 100644 index 0000000000000..f41a696fd204d --- /dev/null +++ b/packages/aws-cdk-lib/aws-cases/index.ts @@ -0,0 +1 @@ +export * from './lib'; diff --git a/packages/aws-cdk-lib/aws-cases/lib/index.ts b/packages/aws-cdk-lib/aws-cases/lib/index.ts new file mode 100644 index 0000000000000..ede7d59e1c3a3 --- /dev/null +++ b/packages/aws-cdk-lib/aws-cases/lib/index.ts @@ -0,0 +1,2 @@ +// AWS::Cases Cloudformation Resources +export * from './cases.generated'; diff --git a/packages/aws-cdk-lib/aws-elasticsearch/lib/domain.ts b/packages/aws-cdk-lib/aws-elasticsearch/lib/domain.ts index fe2816d8da7ef..86705cb03eb93 100644 --- a/packages/aws-cdk-lib/aws-elasticsearch/lib/domain.ts +++ b/packages/aws-cdk-lib/aws-elasticsearch/lib/domain.ts @@ -961,7 +961,7 @@ abstract class DomainBase extends cdk.Resource implements IDomain { public get domainRef(): DomainReference { return { - domainId: this.domainName, + domainName: this.domainName, domainArn: this.domainArn, }; } diff --git a/packages/aws-cdk-lib/index.ts b/packages/aws-cdk-lib/index.ts index b2ece6c52d8b1..968c4687ac39e 100644 --- a/packages/aws-cdk-lib/index.ts +++ b/packages/aws-cdk-lib/index.ts @@ -40,6 +40,7 @@ export * as aws_bedrock from './aws-bedrock'; export * as aws_bedrockagentcore from './aws-bedrockagentcore'; export * as aws_billingconductor from './aws-billingconductor'; export * as aws_budgets from './aws-budgets'; +export * as aws_cases from './aws-cases'; export * as aws_cassandra from './aws-cassandra'; export * as aws_ce from './aws-ce'; export * as aws_certificatemanager from './aws-certificatemanager'; diff --git a/packages/aws-cdk-lib/package.json b/packages/aws-cdk-lib/package.json index ea79ecfc7ebb3..6098ab96ac85a 100644 --- a/packages/aws-cdk-lib/package.json +++ b/packages/aws-cdk-lib/package.json @@ -131,7 +131,7 @@ }, "devDependencies": { "@aws-cdk/lambda-layer-kubectl-v31": "^2.1.0", - "@aws-cdk/aws-service-spec": "^0.1.133", + "@aws-cdk/aws-service-spec": "^0.1.135", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/custom-resource-handlers": "0.0.0", "@aws-cdk/pkglint": "0.0.0", @@ -250,6 +250,7 @@ "./aws-billing": "./aws-billing/index.js", "./aws-billingconductor": "./aws-billingconductor/index.js", "./aws-budgets": "./aws-budgets/index.js", + "./aws-cases": "./aws-cases/index.js", "./aws-cassandra": "./aws-cassandra/index.js", "./aws-ce": "./aws-ce/index.js", "./aws-certificatemanager": "./aws-certificatemanager/index.js", diff --git a/packages/aws-cdk-lib/scripts/scope-map.json b/packages/aws-cdk-lib/scripts/scope-map.json index 0a88c0cea7c29..30a62a728da26 100644 --- a/packages/aws-cdk-lib/scripts/scope-map.json +++ b/packages/aws-cdk-lib/scripts/scope-map.json @@ -276,6 +276,13 @@ } ] }, + "aws-cases": { + "scopes": [ + { + "namespace": "AWS::Cases" + } + ] + }, "aws-cassandra": { "scopes": [ { diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/arn.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/arn.ts index 929d2f89029cd..514b0b9caf8ef 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/arn.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/arn.ts @@ -7,7 +7,8 @@ import { Resource } from '@aws-cdk/service-spec-types'; * included in the primary identifier. */ export function findNonIdentifierArnProperty(resource: Resource) { - return findArnProperty(resource, (name) => !resource.primaryIdentifier?.includes(name)); + const refIdentifier = resource.cfnRefIdentifier ?? resource.primaryIdentifier; + return findArnProperty(resource, (name) => !refIdentifier?.includes(name)); } export function findArnProperty(resource: Resource, filter: (name: string) => boolean = () => true): string | undefined { @@ -15,11 +16,13 @@ export function findArnProperty(resource: Resource, filter: (name: string) => bo const suffixes = ['Arn', 'ARN']; const primaryIdentifierSuffixes = ['Id', 'ID']; + const refIdentifier = resource.cfnRefIdentifier ?? resource.primaryIdentifier; + // if the primary identifier uses a prefix that is different than the resource name, we add that to the list - if (resource.primaryIdentifier?.length === 1) { + if (refIdentifier?.length === 1) { for (const suffix of primaryIdentifierSuffixes) { - if (resource.primaryIdentifier[0].endsWith(suffix)) { - const prefix = resource.primaryIdentifier[0].slice(0, -suffix.length); + if (refIdentifier[0].endsWith(suffix)) { + const prefix = refIdentifier[0].slice(0, -suffix.length); if (prefix && !prefixes.includes(prefix)) { prefixes.push(prefix); } diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/reference-props.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/reference-props.ts index 31417b9be72fc..1aaf4d6bdd42d 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/reference-props.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/reference-props.ts @@ -1,6 +1,6 @@ import { Resource } from '@aws-cdk/service-spec-types'; import { $this, expr, Expression, PropertySpec, Type } from '@cdklabs/typewriter'; -import { attributePropertyName, referencePropertyName } from '../naming'; +import { attributePropertyName, propertyNameFromCloudFormation, referencePropertyName } from '../naming'; import { extractResourceVariablesFromArnFormat, findArnProperty, findNonIdentifierArnProperty } from './arn'; import { CDK_CORE } from './cdk'; @@ -9,6 +9,37 @@ export interface ReferenceProp { readonly cfnValue: Expression; } +/** + * Calculations for to the resource reference interface. + * + * This class is slightly complicated because it needs to account for the differences between CloudFormation + * and CC-API identifiers. + * + * - In principle, the CC-API identifier is leading: it uniquely identifies a resource inside an environment. + * - For backwards compatibility reasons, the CFN identifier (the value returned by `{ Ref }`) isn't necessarily + * always the same. If it isn't the same, there can be two reasons for them to diverge: + * + * - SPECIFICITY: the CC-API identifier is more specific than the CFN + * identifier. For example, for `ApiGateway::Stage`, the unique identifier is + * `[ApiId, StageName]` but the value that `{ Ref }` returns is just + * `StageName`. + * + * This distinction happens for subresources, and in those cases the + * primary resource will be a required input property. For maximum + * flexibility we generate the interface according to the CC-API + * identifier, and get values from the CFN identifier, attributes and input + * properties as necessary. + * + * - ALIASING: the CC-API uses a different form of identifying the resource than the + * CFN identifier. For example, for `Batch::JobDefinition` the spec says the primary + * identifier is the `Name` but the actual value that `{ Ref }` returns is the + * the `Arn`. We will just use the CFN value as leading. + * + * We will identify the difference between these 2 cases by the length of the primary + * identifier: equal length = aliasing, different length = specificity. + * + * If available, we also add an ARN field into the reference interface. + */ export class ResourceReference { public readonly resource: Resource; public readonly arnPropertyName?: string; @@ -44,36 +75,20 @@ export class ResourceReference { } private collectReferencesProps() { - // Primary identifier. We assume all parts are strings. - const primaryIdentifier = this.resource.primaryIdentifier ?? []; - if (primaryIdentifier.length === 1) { - const name = referencePropertyName(primaryIdentifier[0], this.resource.name); + // Reference fields + for (const cfnName of this.referenceFields) { + const name = referencePropertyName(cfnName, this.resource.name); this._referenceProps.setIfAbsent(name, { declaration: { name, type: Type.STRING, immutable: true, docs: { - summary: `The ${primaryIdentifier[0]} of the ${this.resource.name} resource.`, + summary: `The ${cfnName} of the ${this.resource.name} resource.`, }, }, - cfnValue: $this.ref, + cfnValue: this.getStringValue(cfnName), }); - } else if (primaryIdentifier.length > 1) { - for (const [i, cfnName] of primaryIdentifier.entries()) { - const name = referencePropertyName(cfnName, this.resource.name); - this._referenceProps.setIfAbsent(name, { - declaration: { - name, - type: Type.STRING, - immutable: true, - docs: { - summary: `The ${cfnName} of the ${this.resource.name} resource.`, - }, - }, - cfnValue: splitSelect('|', i, $this.ref), - }); - } } // Arn identifier @@ -137,6 +152,71 @@ export class ResourceReference { // we don't have a matching value return undefined; } + + /** + * The actual reference fields + * + * The CFN values if present and the same length as the CC-API values, otherwise the CC-API values. + * + * For a CC-API identifier we filter out optional properties, such as for `ECS::Cluster`: the real + * unique identifier includes `Cluster` but that is an optional property because the Service will fall + * back to some implicit default Cluster that we can never replicate. + */ + public get referenceFields(): string[] { + if (this.resource.cfnRefIdentifier && this.resource.cfnRefIdentifier.length === this.resource.primaryIdentifier?.length) { + return this.resource.cfnRefIdentifier; + } + + // Filter out properties we can't find a value for (will only be optional properties) + return (this.resource.primaryIdentifier ?? []) + .filter(p => this.tryGetStringValue(p) !== undefined); + } + + /** + * What `{ Ref }` returns in CloudFormation + */ + public get cfnRefComponents(): string[] { + return this.resource.cfnRefIdentifier ?? this.resource.primaryIdentifier ?? []; + } + + /** + * Return an expression to return the given value from the { Ref } or any of the attributes or properties + */ + private tryGetStringValue(name: string): Expression | undefined { + for (const [i, field] of this.cfnRefComponents.entries()) { + if (field === name) { + // Return entire field or Split expression, depending on whether we need to split at all + return this.cfnRefComponents.length > 1 ? splitSelect('|', i, $this.ref) : $this.ref; + } + } + + // Is it an attr? + if (this.resource.attributes[name]) { + return $this[attributePropertyName(name)]; + } + + // A required prop? + if (this.resource.properties[name]?.required) { + return $this[propertyNameFromCloudFormation(name)]; + } + + return undefined; + } + + /** + * Return a value, failing if it doesn't exist. + */ + private getStringValue(name: string): Expression { + const ret = this.tryGetStringValue(name); + if (ret) { + return ret; + } + + const attributeNames = Object.keys(this.resource.attributes); + const requiredPropertyNames = Object.entries(this.resource.properties).filter(([_, p]) => p.required).map(([n, _]) => n); + + throw new Error(`Cannot find reference interface value name ${name} for resource ${this.resource.cloudFormationType} (Ref components: ${this.cfnRefComponents}, attributes: ${attributeNames}, requiredProps: ${requiredPropertyNames})`); + } } class FirstOccurrenceMap extends Map { diff --git a/tools/@aws-cdk/spec2cdk/package.json b/tools/@aws-cdk/spec2cdk/package.json index 9f2dd40c171a1..2d4c8eee5ad5a 100644 --- a/tools/@aws-cdk/spec2cdk/package.json +++ b/tools/@aws-cdk/spec2cdk/package.json @@ -31,9 +31,9 @@ }, "license": "Apache-2.0", "dependencies": { - "@aws-cdk/aws-service-spec": "^0.1.133", + "@aws-cdk/aws-service-spec": "^0.1.135", "@aws-cdk/service-spec-importers": "^0.0.101", - "@aws-cdk/service-spec-types": "^0.0.199", + "@aws-cdk/service-spec-types": "^0.0.201", "@cdklabs/tskb": "^0.0.4", "@cdklabs/typewriter": "^0.0.14", "camelcase": "^6", @@ -46,6 +46,7 @@ "@aws-cdk/pkglint": "0.0.0", "@types/jest": "^29.5.14", "@types/node": "^18", + "diff": "^8.0.2", "jest": "^29.7.0" }, "keywords": [ 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 e02e8b920cbd2..036afd261cf10 100644 --- a/tools/@aws-cdk/spec2cdk/test/__snapshots__/resources.test.ts.snap +++ b/tools/@aws-cdk/spec2cdk/test/__snapshots__/resources.test.ts.snap @@ -189,7 +189,7 @@ function CfnResourcePropsFromCloudFormation(properties: any): cfn_parse.FromClou export type { IResourceRef, ResourceReference };" `; -exports[`resource interface when primaryIdentifier is a property 1`] = ` +exports[`resource interface when cfnRefIdentifier is a property 1`] = ` "/* eslint-disable prettier/prettier, @stylistic/max-len */ import * as cdk from "aws-cdk-lib/core"; import * as constructs from "constructs"; diff --git a/tools/@aws-cdk/spec2cdk/test/expect-to-contain-code.ts b/tools/@aws-cdk/spec2cdk/test/expect-to-contain-code.ts new file mode 100644 index 0000000000000..5d38d2d3d95c5 --- /dev/null +++ b/tools/@aws-cdk/spec2cdk/test/expect-to-contain-code.ts @@ -0,0 +1,51 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { ChangeObject, diffLines } from 'diff'; + +expect.extend({ + toContainCode(actual: string, expected: string) { + const ds = diffLines(actual, expected, { + ignoreWhitespace: true, + }); + + // We expect to see only 'removed' lines at the edges (lines in the "actual" + // we can't find in the "expected"). Then, what remains should match exactly. + while (ds.length > 0 && !ds[0].added && ds[0].removed) { + ds.splice(0, 1); + } + while (ds.length > 0 && !ds[ds.length - 1].added && ds[ds.length - 1].removed) { + ds.splice(ds.length - 1, 1); + } + + const ok = ds.every(d => !d.added && !d.removed); + + return { + pass: ok, + message: () => renderDiff(ds), + }; + }, +}); + +declare global { + namespace jest { + // Optionally, also as an Asymmetric Matcher + interface Matchers { + toContainCode(expected: string): CustomMatcherResult; + } + } +} + +function renderDiff(ds: ChangeObject[]) { + const ret = new Array(); + for (const d of ds) { + // Always ends with a `\n` we're not interested in + let lines = d.value.slice(0, -1).split('\n'); + if (d.added) { + lines = lines.map(x => `+${x}`); + } + if (d.removed) { + lines = lines.map(x => `-${x}`); + } + ret.push(...lines); + } + return ret.join('\n'); +} diff --git a/tools/@aws-cdk/spec2cdk/test/resources.test.ts b/tools/@aws-cdk/spec2cdk/test/resources.test.ts index 95f01e91ffa0b..176978156e235 100644 --- a/tools/@aws-cdk/spec2cdk/test/resources.test.ts +++ b/tools/@aws-cdk/spec2cdk/test/resources.test.ts @@ -3,6 +3,7 @@ import { Plain } from '@cdklabs/tskb'; import { TypeScriptRenderer } from '@cdklabs/typewriter'; import { moduleForResource } from './util'; import { AwsCdkLibBuilder } from '../lib/cdk/aws-cdk-lib'; +import './expect-to-contain-code'; const renderer = new TypeScriptRenderer(); let db: SpecDatabase; @@ -32,7 +33,7 @@ beforeEach(async () => { }); }); -test('resource interface when primaryIdentifier is a property', () => { +test('resource interface when cfnRefIdentifier is a property', () => { // GIVEN const resource = db.allocate('resource', BASE_RESOURCE); db.link('hasResource', service, resource); @@ -297,3 +298,161 @@ test('can generate interface types into a separate module', () => { expect(rendered.interfaces).toMatchSnapshot(); expect(rendered.resources).toMatchSnapshot(); }); + +test('reference interface is based on CC-API identifier with values pulled from attributes', () => { + // GIVEN + givenResource({ + ...BASE_RESOURCE, + attributes: { + ParentId: { + type: { type: 'string' }, + documentation: 'The identifier of the parent resource', + }, + }, + primaryIdentifier: ['ParentId', 'Id'], + cfnRefIdentifier: ['Id'], + }); + + // THEN + const rendered = renderResource(); + expect(rendered.interfaces).toContainCode( + `export interface ResourceReference { + /** + * The ParentId of the Resource resource. + */ + readonly parentId: string; + + /** + * The Id of the Resource resource. + */ + readonly resourceId: string; + }`); + expect(rendered.resources).toContainCode( + `public get resourceRef(): ResourceReference { + return { + parentId: this.attrParentId, + resourceId: this.ref + }; + }`, + ); +}); + +test('reference interface is based on CC-API identifier with values pulled required properties', () => { + // GIVEN + givenResource({ + ...BASE_RESOURCE, + properties: { + ...BASE_RESOURCE.properties, + ParentId: { + type: { type: 'string' }, + documentation: 'The identifier of the parent resource', + required: true, + }, + }, + primaryIdentifier: ['ParentId', 'Id'], + cfnRefIdentifier: ['Id'], + }); + + // THEN + const rendered = renderResource(); + expect(rendered.interfaces).toContainCode( + `export interface ResourceReference { + /** + * The ParentId of the Resource resource. + */ + readonly parentId: string; + + /** + * The Id of the Resource resource. + */ + readonly resourceId: string; + }`); + expect(rendered.resources).toContainCode( + `public get resourceRef(): ResourceReference { + return { + parentId: this.parentId, + resourceId: this.ref + }; + }`, + ); +}); + +test('optional properties are dropped from CC-API-based reference interface', () => { + // GIVEN + givenResource({ + ...BASE_RESOURCE, + properties: { + ...BASE_RESOURCE.properties, + ParentId: { + type: { type: 'string' }, + documentation: 'The identifier of the parent resource', + }, + }, + primaryIdentifier: ['ParentId', 'Id'], + cfnRefIdentifier: ['Id'], + }); + + // THEN + const rendered = renderResource(); + expect(rendered.interfaces).toContainCode( + `export interface ResourceReference { + /** + * The Id of the Resource resource. + */ + readonly resourceId: string; + }`); + expect(rendered.resources).toContainCode( + `public get resourceRef(): ResourceReference { + return { + resourceId: this.ref + }; + }`, + ); +}); + +test('CFN reference identifier of same length as CC-API identifier aliases field names', () => { + // GIVEN + givenResource({ + ...BASE_RESOURCE, + attributes: { + Arn: { + type: { type: 'string' }, + documentation: 'The ARN of this resource', + }, + }, + primaryIdentifier: ['Id'], + cfnRefIdentifier: ['Arn'], + }); + + // THEN + const rendered = renderResource(); + expect(rendered.interfaces).toContainCode( + `export interface ResourceReference { + /** + * The Arn of the Resource resource. + */ + readonly resourceArn: string; + }`); + expect(rendered.resources).toContainCode( + `public get resourceRef(): ResourceReference { + return { + resourceArn: this.ref + }; + }`, + ); +}); + +function givenResource(res: Plain) { + db.link('hasResource', service, db.allocate('resource', res)); +} + +function renderResource(resourceType = 'AWS::Some::Resource') { + const foundResource = db.lookup('resource', 'cloudFormationType', 'equals', resourceType).only(); + + const ast = new AwsCdkLibBuilder({ db }); + const info = ast.addResource(foundResource); + return { + interfaces: renderer.render(info.interfaces.module), + resources: renderer.render(info.resourcesMod.module), + }; +} diff --git a/tools/@aws-cdk/spec2cdk/tsconfig.json b/tools/@aws-cdk/spec2cdk/tsconfig.json index 310caf6bdbbb0..7b60e3eac05bf 100644 --- a/tools/@aws-cdk/spec2cdk/tsconfig.json +++ b/tools/@aws-cdk/spec2cdk/tsconfig.json @@ -14,6 +14,7 @@ "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "resolveJsonModule": true, + "esModuleInterop": false, "composite": true, "incremental": true, "isolatedModules": true diff --git a/yarn.lock b/yarn.lock index 0e643127c1fd9..7770b8209b5db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -58,12 +58,12 @@ "@aws-cdk/service-spec-types" "^0.0.164" "@cdklabs/tskb" "^0.0.3" -"@aws-cdk/aws-service-spec@^0.1.133": - version "0.1.133" - resolved "https://registry.npmjs.org/@aws-cdk/aws-service-spec/-/aws-service-spec-0.1.133.tgz#75a36a43225d5dfcd04ed821a892d6549c322839" - integrity sha512-6i6vjGJQ3vNpVn29RqBveyv3aadof7bkvB/WE3zlI7q+ZyxAma+8INPBgSifbL47t1kCcFQLCPQQulq3MjXfLA== +"@aws-cdk/aws-service-spec@^0.1.135": + version "0.1.135" + resolved "https://registry.npmjs.org/@aws-cdk/aws-service-spec/-/aws-service-spec-0.1.135.tgz#ef01b1e5fca6c6da1dafa294108fa73799ad2a51" + integrity sha512-XY98CaTyze7Bquun0vxZ1EoWpXRz+1hlnfzHtf1vtNhMfEp6JmdZeoN38kGcAtq1ags/Zfs84Ca5ubIZaMAwwg== dependencies: - "@aws-cdk/service-spec-types" "^0.0.199" + "@aws-cdk/service-spec-types" "^0.0.201" "@cdklabs/tskb" "^0.0.4" "@aws-cdk/cloud-assembly-schema@^48.20.0": @@ -149,6 +149,13 @@ dependencies: "@cdklabs/tskb" "^0.0.4" +"@aws-cdk/service-spec-types@^0.0.201": + version "0.0.201" + resolved "https://registry.npmjs.org/@aws-cdk/service-spec-types/-/service-spec-types-0.0.201.tgz#3481ff79a99a11d971c4a3387def84d7bdbbc866" + integrity sha512-dHBmmKU24ROarP4GN5NQBu1UjV3Me8tdzA2I+/GPSEtt0J1EEObi3megYeU3fYx50i6uzYQ1pJVo9nVWgce9vA== + dependencies: + "@cdklabs/tskb" "^0.0.4" + "@aws-crypto/crc32@5.2.0": version "5.2.0" resolved "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz#cfcc22570949c98c6689cfcbd2d693d36cdae2e1" @@ -7552,6 +7559,11 @@ diff@^5.2.0: resolved "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== +diff@^8.0.2: + version "8.0.2" + resolved "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz#712156a6dd288e66ebb986864e190c2fc9eddfae" + integrity sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"