Skip to content

Commit

Permalink
BREAKING CHANGE: strongly type resource refs (#627)
Browse files Browse the repository at this point in the history
Annotate all CloudFormation resource types with the type of their 'Ref'
(whether it returns a `Name`, `Id` or `Arn`). We generate specific
classes for those types, just like we do for `{Fn::GetAtt}` attributes.

This makes it easier to write construct libraries: it removes the need
for every construct library to explicitly declare a custom type for the 
ref implicit type, and reduces chances of a `ClassCastException` in Java
if they do it wrong.

Generated resource classes no longer implicitly inherit from
`Referenceable`, because not all resources even have the `{Ref}`
operator defined.

Fixes #619.
  • Loading branch information
rix0rrr authored Aug 27, 2018
1 parent 9f49274 commit 755488e
Show file tree
Hide file tree
Showing 47 changed files with 3,978 additions and 368 deletions.
2 changes: 1 addition & 1 deletion packages/@aws-cdk/assets/lib/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export class Asset extends cdk.Construct {
this.s3BucketName = bucketParam.value;
this.s3Prefix = new cdk.FnSelect(0, new cdk.FnSplit(cxapi.ASSET_PREFIX_SEPARATOR, keyParam.value));
const s3Filename = new cdk.FnSelect(1, new cdk.FnSplit(cxapi.ASSET_PREFIX_SEPARATOR, keyParam.value));
this.s3ObjectKey = new cdk.FnConcat(this.s3Prefix, s3Filename);
this.s3ObjectKey = new s3.ObjectKey(new cdk.FnConcat(this.s3Prefix, s3Filename));

this.bucket = s3.BucketRef.import(parent, 'AssetBucket', {
bucketName: this.s3BucketName
Expand Down
53 changes: 30 additions & 23 deletions packages/@aws-cdk/assets/test/integ.assets.refs.lit.expected.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,29 +76,36 @@
},
"/",
{
"Fn::Select": [
0,
{
"Fn::Split": [
"||",
{
"Ref": "SampleAssetS3VersionKey3E106D34"
}
]
}
]
},
{
"Fn::Select": [
1,
{
"Fn::Split": [
"||",
{
"Ref": "SampleAssetS3VersionKey3E106D34"
}
]
}
"Fn::Join": [
"",
[
{
"Fn::Select": [
0,
{
"Fn::Split": [
"||",
{
"Ref": "SampleAssetS3VersionKey3E106D34"
}
]
}
]
},
{
"Fn::Select": [
1,
{
"Fn::Split": [
"||",
{
"Ref": "SampleAssetS3VersionKey3E106D34"
}
]
}
]
}
]
]
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,9 +336,9 @@ export = {

function mockVpc(stack: cdk.Stack) {
return ec2.VpcNetwork.import(stack, 'MyVpc', {
vpcId: new ec2.VpcNetworkId('my-vpc'),
vpcId: new ec2.VPCId('my-vpc'),
availabilityZones: [ 'az1' ],
publicSubnetIds: [ new ec2.VpcSubnetId('pub1') ],
privateSubnetIds: [ new ec2.VpcSubnetId('pri1') ],
publicSubnetIds: [ new ec2.SubnetId('pub1') ],
privateSubnetIds: [ new ec2.SubnetId('pri1') ],
});
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import { Arn, Construct, Output } from "@aws-cdk/cdk";

/**
* Represents the ARN of a certificate
*/
export class CertificateArn extends Arn {
}
import { Construct, Output } from "@aws-cdk/cdk";
import { CertificateArn } from './certificatemanager.generated';

/**
* Interface for certificate-like objects
Expand Down
4 changes: 2 additions & 2 deletions packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Construct } from '@aws-cdk/cdk';
import { CertificateArn, CertificateRef } from './certificate-ref';
import { cloudformation } from './certificatemanager.generated';
import { CertificateRef } from './certificate-ref';
import { CertificateArn, cloudformation } from './certificatemanager.generated';
import { apexDomain } from './util';

/**
Expand Down
6 changes: 2 additions & 4 deletions packages/@aws-cdk/aws-codebuild/lib/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import kms = require('@aws-cdk/aws-kms');
import s3 = require('@aws-cdk/aws-s3');
import cdk = require('@aws-cdk/cdk');
import { BuildArtifacts, CodePipelineBuildArtifacts, NoBuildArtifacts } from './artifacts';
import { cloudformation, ProjectArn } from './codebuild.generated';
import { cloudformation, ProjectArn, ProjectName } from './codebuild.generated';
import { BuildSource } from './source';

const CODEPIPELINE_TYPE = 'CODEPIPELINE';
Expand Down Expand Up @@ -704,6 +704,4 @@ export enum BuildEnvironmentVariableType {
* An environment variable stored in Systems Manager Parameter Store.
*/
ParameterStore = 'PARAMETER_STORE'
}

export class ProjectName extends cdk.Token { }
}
7 changes: 1 addition & 6 deletions packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,14 @@ import iam = require('@aws-cdk/aws-iam');
import s3 = require('@aws-cdk/aws-s3');
import cdk = require('@aws-cdk/cdk');
import util = require('@aws-cdk/util');
import { cloudformation, PipelineVersion } from './codepipeline.generated';
import { cloudformation, PipelineName, PipelineVersion } from './codepipeline.generated';
import { Stage } from './stage';

/**
* The ARN of a pipeline
*/
export class PipelineArn extends cdk.Arn { }

/**
* The name of the pipeline.
*/
export class PipelineName extends cdk.Token { }

export interface PipelineProps {
/**
* The S3 bucket used by this Pipeline to store artifacts.
Expand Down
123 changes: 1 addition & 122 deletions packages/@aws-cdk/aws-dynamodb/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,123 +1,2 @@
import { Construct, Token } from '@aws-cdk/cdk';
import { cloudformation } from './dynamodb.generated';

// AWS::DynamoDB CloudFormation Resources:
export * from './dynamodb.generated';

const HASH_KEY_TYPE = 'HASH';
const RANGE_KEY_TYPE = 'RANGE';

export interface TableProps {
/**
* The read capacity for the table. Careful if you add Global Secondary Indexes, as
* those will share the table's provisioned throughput.
* @default 5
*/
readCapacity?: number;
/**
* The write capacity for the table. Careful if you add Global Secondary Indexes, as
* those will share the table's provisioned throughput.
* @default 5
*/
writeCapacity?: number;

/**
* Enforces a particular physical table name.
* @default <generated>
*/
tableName?: string;
}

/**
* Provides a DynamoDB table.
*/
export class Table extends Construct {
private readonly table: cloudformation.TableResource;

private readonly keySchema = new Array<cloudformation.TableResource.KeySchemaProperty>();
private readonly attributeDefinitions = new Array<cloudformation.TableResource.AttributeDefinitionProperty>();

constructor(parent: Construct, name: string, props: TableProps = {}) {
super(parent, name);

const readCapacityUnits = props.readCapacity || 5;
const writeCapacityUnits = props.writeCapacity || 5;

this.table = new cloudformation.TableResource(this, 'Resource', {
tableName: props.tableName,
keySchema: this.keySchema,
attributeDefinitions: this.attributeDefinitions,
provisionedThroughput: { readCapacityUnits, writeCapacityUnits }
});

if (props.tableName) { this.addMetadata('aws:cdk:hasPhysicalName', props.tableName); }
}

public get tableArn() {
return this.table.tableArn;
}

public get tableName() {
return this.table.ref as TableName;
}

public get tableStreamArn() {
return this.table.tableStreamArn;
}

public addPartitionKey(name: string, type: KeyAttributeType): this {
this.addKey(name, type, HASH_KEY_TYPE);
return this;
}

public addSortKey(name: string, type: KeyAttributeType): this {
this.addKey(name, type, RANGE_KEY_TYPE);
return this;
}

public validate(): string[] {
const errors = new Array<string>();
if (!this.findKey(HASH_KEY_TYPE)) {
errors.push('a partition key must be specified');
}
return errors;
}

private findKey(keyType: string) {
return this.keySchema.find(prop => prop.keyType === keyType);
}

private addKey(name: string, type: KeyAttributeType, keyType: string) {
const existingProp = this.findKey(keyType);
if (existingProp) {
throw new Error(`Unable to set ${name} as a ${keyType} key, because ${existingProp.attributeName} is a ${keyType} key`);
}
this.registerAttribute(name, type);
this.keySchema.push({
attributeName: name,
keyType
});
return this;
}

private registerAttribute(name: string, type: KeyAttributeType) {
const existingDef = this.attributeDefinitions.find(def => def.attributeName === name);
if (existingDef && existingDef.attributeType !== type) {
throw new Error(`Unable to specify ${name} as ${type} because it was already defined as ${existingDef.attributeType}`);
}
if (!existingDef) {
this.attributeDefinitions.push({
attributeName: name,
attributeType: type
});
}
}
}

export class TableName extends Token {}

export enum KeyAttributeType {
Binary = 'B',
Number = 'N',
String = 'S',
}
export * from './table';
114 changes: 114 additions & 0 deletions packages/@aws-cdk/aws-dynamodb/lib/table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { Construct } from '@aws-cdk/cdk';
import { cloudformation, TableArn, TableName, TableStreamArn } from './dynamodb.generated';

const HASH_KEY_TYPE = 'HASH';
const RANGE_KEY_TYPE = 'RANGE';

export interface TableProps {
/**
* The read capacity for the table. Careful if you add Global Secondary Indexes, as
* those will share the table's provisioned throughput.
* @default 5
*/
readCapacity?: number;
/**
* The write capacity for the table. Careful if you add Global Secondary Indexes, as
* those will share the table's provisioned throughput.
* @default 5
*/
writeCapacity?: number;

/**
* Enforces a particular physical table name.
* @default <generated>
*/
tableName?: string;
}

/**
* Provides a DynamoDB table.
*/
export class Table extends Construct {
public readonly tableArn: TableArn;
public readonly tableName: TableName;
public readonly tableStreamArn: TableStreamArn;

private readonly table: cloudformation.TableResource;

private readonly keySchema = new Array<cloudformation.TableResource.KeySchemaProperty>();
private readonly attributeDefinitions = new Array<cloudformation.TableResource.AttributeDefinitionProperty>();

constructor(parent: Construct, name: string, props: TableProps = {}) {
super(parent, name);

const readCapacityUnits = props.readCapacity || 5;
const writeCapacityUnits = props.writeCapacity || 5;

this.table = new cloudformation.TableResource(this, 'Resource', {
tableName: props.tableName,
keySchema: this.keySchema,
attributeDefinitions: this.attributeDefinitions,
provisionedThroughput: { readCapacityUnits, writeCapacityUnits }
});

if (props.tableName) { this.addMetadata('aws:cdk:hasPhysicalName', props.tableName); }

this.tableArn = this.table.tableArn;
this.tableName = this.table.ref;
this.tableStreamArn = this.table.tableStreamArn;
}

public addPartitionKey(name: string, type: KeyAttributeType): this {
this.addKey(name, type, HASH_KEY_TYPE);
return this;
}

public addSortKey(name: string, type: KeyAttributeType): this {
this.addKey(name, type, RANGE_KEY_TYPE);
return this;
}

public validate(): string[] {
const errors = new Array<string>();
if (!this.findKey(HASH_KEY_TYPE)) {
errors.push('a partition key must be specified');
}
return errors;
}

private findKey(keyType: string) {
return this.keySchema.find(prop => prop.keyType === keyType);
}

private addKey(name: string, type: KeyAttributeType, keyType: string) {
const existingProp = this.findKey(keyType);
if (existingProp) {
throw new Error(`Unable to set ${name} as a ${keyType} key, because ${existingProp.attributeName} is a ${keyType} key`);
}
this.registerAttribute(name, type);
this.keySchema.push({
attributeName: name,
keyType
});
return this;
}

private registerAttribute(name: string, type: KeyAttributeType) {
const existingDef = this.attributeDefinitions.find(def => def.attributeName === name);
if (existingDef && existingDef.attributeType !== type) {
throw new Error(`Unable to specify ${name} as ${type} because it was already defined as ${existingDef.attributeType}`);
}
if (!existingDef) {
this.attributeDefinitions.push({
attributeName: name,
attributeType: type
});
}
}
}

export enum KeyAttributeType {
Binary = 'B',
Number = 'N',
String = 'S',
}
4 changes: 1 addition & 3 deletions packages/@aws-cdk/aws-ec2/lib/security-group.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Construct, Output, Token } from '@aws-cdk/cdk';
import { Connections, IConnectable } from './connections';
import { cloudformation, SecurityGroupId, SecurityGroupVpcId } from './ec2.generated';
import { cloudformation, SecurityGroupId, SecurityGroupName, SecurityGroupVpcId } from './ec2.generated';
import { IPortRange, ISecurityGroupRule } from './security-group-rule';
import { slugify } from './util';
import { VpcNetworkRef } from './vpc-ref';
Expand Down Expand Up @@ -198,8 +198,6 @@ export class SecurityGroup extends SecurityGroupRef {
}
}

export class SecurityGroupName extends Token { }

export interface ConnectionRule {
/**
* The IP protocol name (tcp, udp, icmp) or number (see Protocol Numbers).
Expand Down
Loading

0 comments on commit 755488e

Please sign in to comment.