Skip to content

Commit

Permalink
feat(core): new APIs for Aspects and Tags
Browse files Browse the repository at this point in the history
In [CDK 2.0], the `applyAspect` API will be removed and instead will be accessible through a "trait" pattern:

    Aspects.of(scope).add(aspect)

Similarly, we are normalizing the tagging API to use the same pattern:

    Tags.of(scope).add(x, y)
    Tags.of(scope).remove(x)
    
The existing APIs are still supported but marked as @deprecated.

Related: aws/aws-cdk-rfcs#192

[CDK 2.0]: https://github.com/aws/aws-cdk-rfcs/blob/master/text/0192-remove-constructs-compat.md
  • Loading branch information
Elad Ben-Israel committed Aug 10, 2020
1 parent dd0f4cb commit 4bc91ef
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 56 deletions.
47 changes: 47 additions & 0 deletions packages/@aws-cdk/core/lib/aspect.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { IConstruct } from './construct-compat';

const ASPECTS_SYMBOL = Symbol('cdk-aspects');

/**
* Represents an Aspect
*/
Expand All @@ -9,3 +11,48 @@ export interface IAspect {
*/
visit(node: IConstruct): void;
}

/**
* Aspects can be applied to CDK tree scopes and can operate on the tree before
* synthesis.
*/
export class Aspects {

/**
* Returns the `Aspects` object associated with a construct scope.
* @param scope The scope for which these aspects will apply.
*/
public static of(scope: IConstruct): Aspects {
let aspects = (scope as any)[ASPECTS_SYMBOL];
if (!aspects) {
aspects = new Aspects(scope);

Object.defineProperty(scope, ASPECTS_SYMBOL, {
value: aspects,
configurable: false,
enumerable: false,
});
}
return aspects;
}

// TODO(2.0): private readonly _aspects = new Array<IAspect>();
private constructor(private readonly scope: IConstruct) { }

/**
* Adds an aspect to apply this scope before synthesis.
* @param aspect The aspect to add.
*/
public add(aspect: IAspect) {
// TODO(2.0): this._aspects.push(aspect);
this.scope.node._actualNode.applyAspect(aspect);
}

/**
* The list of aspects which were directly applied on this scope.
*/
public get aspects(): IAspect[] {
// TODO(2.0): return [ ...this._aspects ];
return [ ...(this.scope.node._actualNode as any)._aspects ]; // clone
}
}
15 changes: 12 additions & 3 deletions packages/@aws-cdk/core/lib/construct-compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
import * as cxapi from '@aws-cdk/cx-api';
import * as constructs from 'constructs';
import { IAspect } from './aspect';
import { IAspect, Aspects } from './aspect';
import { IDependable } from './dependency';
import { Token } from './token';

Expand Down Expand Up @@ -267,7 +267,13 @@ export class ConstructNode {
*/
public readonly _actualNode: constructs.Node;

/**
* The Construct class that hosts this API.
*/
private readonly host: Construct;

constructor(host: Construct, scope: IConstruct, id: string) {
this.host = host;
this._actualNode = new constructs.Node(host, scope, id);

// store a back reference on _actualNode so we can our ConstructNode from it
Expand Down Expand Up @@ -433,9 +439,12 @@ export class ConstructNode {
}

/**
* Applies the aspect to this Constructs node
* DEPRECATED: Applies the aspect to this Constructs node
*
* @deprecated This API is going to be removed in the next major version of
* the AWS CDK. Please use `Aspects.of(scope).add()` instead.
*/
public applyAspect(aspect: IAspect): void { this._actualNode.applyAspect(aspect); }
public applyAspect(aspect: IAspect): void { Aspects.of(this.host).add(aspect); }

/**
* All parent scopes of this construct.
Expand Down
36 changes: 18 additions & 18 deletions packages/@aws-cdk/core/lib/private/synthesis.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as cxapi from '@aws-cdk/cx-api';
import * as constructs from 'constructs';
import { Aspects, IAspect } from '../aspect';
import { Construct, IConstruct, SynthesisOptions, ValidationError } from '../construct-compat';
import { Stack } from '../stack';
import { Stage, StageSynthesisOptions } from '../stage';
Expand Down Expand Up @@ -60,26 +61,35 @@ function synthNestedAssemblies(root: IConstruct, options: StageSynthesisOptions)
* twice for the same construct.
*/
function invokeAspects(root: IConstruct) {
const invokedByPath: { [nodePath: string]: IAspect[] } = { };

let nestedAspectWarning = false;
recurse(root, []);

function recurse(construct: IConstruct, inheritedAspects: constructs.IAspect[]) {
// hackery to be able to access some private members with strong types (yack!)
const node: NodeWithAspectPrivatesHangingOut = construct.node._actualNode as any;

const allAspectsHere = [...inheritedAspects ?? [], ...node._aspects];
const nodeAspectsCount = node._aspects.length;
const node = construct.node;
const aspects = Aspects.of(construct);
const allAspectsHere = [...inheritedAspects ?? [], ...aspects.aspects];
const nodeAspectsCount = aspects.aspects.length;
for (const aspect of allAspectsHere) {
if (node.invokedAspects.includes(aspect)) { continue; }
let invoked = invokedByPath[node.path];
if (!invoked) {
invoked = invokedByPath[node.path] = [];
}

if (invoked.includes(aspect)) { continue; }

aspect.visit(construct);

// if an aspect was added to the node while invoking another aspect it will not be invoked, emit a warning
// the `nestedAspectWarning` flag is used to prevent the warning from being emitted for every child
if (!nestedAspectWarning && nodeAspectsCount !== node._aspects.length) {
if (!nestedAspectWarning && nodeAspectsCount !== aspects.aspects.length) {
construct.node.addWarning('We detected an Aspect was added via another Aspect, and will not be applied');
nestedAspectWarning = true;
}
node.invokedAspects.push(aspect);

// mark as invoked for this node
invoked.push(aspect);
}

for (const child of construct.node.children) {
Expand Down Expand Up @@ -180,13 +190,3 @@ interface IProtectedConstructMethods extends IConstruct {
*/
onPrepare(): void;
}

/**
* The constructs Node type, but with some aspects-related fields public.
*
* Hackery!
*/
type NodeWithAspectPrivatesHangingOut = Omit<constructs.Node, 'invokedAspects' | '_aspects'> & {
readonly invokedAspects: constructs.IAspect[];
readonly _aspects: constructs.IAspect[];
};
43 changes: 38 additions & 5 deletions packages/@aws-cdk/core/lib/tag-aspect.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// import * as cxapi from '@aws-cdk/cx-api';
import { IAspect } from './aspect';
import { IAspect, Aspects } from './aspect';
import { Construct, IConstruct } from './construct-compat';
import { ITaggable, TagManager } from './tag-manager';

Expand Down Expand Up @@ -86,17 +86,21 @@ abstract class TagBase implements IAspect {
export class Tag extends TagBase {

/**
* add tags to the node of a construct and all its the taggable children
* DEPRECATED: add tags to the node of a construct and all its the taggable children
*
* @deprecated use `Tags.of(scope).add()`
*/
public static add(scope: Construct, key: string, value: string, props: TagProps = {}) {
scope.node.applyAspect(new Tag(key, value, props));
Tags.of(scope).add(key, value, props);
}

/**
* remove tags to the node of a construct and all its the taggable children
* DEPRECATED: remove tags to the node of a construct and all its the taggable children
*
* @deprecated use `Tags.of(scope).remove()`
*/
public static remove(scope: Construct, key: string, props: TagProps = {}) {
scope.node.applyAspect(new RemoveTag(key, props));
Tags.of(scope).remove(key, props);
}

/**
Expand Down Expand Up @@ -126,6 +130,35 @@ export class Tag extends TagBase {
}
}

/**
* Manages AWS tags for all resources within a construct scope.
*/
export class Tags {
/**
* Returns the tags API for this scope.
* @param scope The scope
*/
public static of(scope: IConstruct): Tags {
return new Tags(scope);
}

private constructor(private readonly scope: IConstruct) { }

/**
* add tags to the node of a construct and all its the taggable children
*/
public add(key: string, value: string, props: TagProps = {}) {
Aspects.of(this.scope).add(new Tag(key, value, props));
}

/**
* remove tags to the node of a construct and all its the taggable children
*/
public remove(key: string, props: TagProps = {}) {
Aspects.of(this.scope).add(new RemoveTag(key, props));
}
}

/**
* The RemoveTag Aspect will handle removing tags from this node and children
*/
Expand Down
10 changes: 5 additions & 5 deletions packages/@aws-cdk/core/test/test.aspect.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
import { Test } from 'nodeunit';
import { App } from '../lib';
import { IAspect } from '../lib/aspect';
import { IAspect, Aspects } from '../lib/aspect';
import { Construct, IConstruct } from '../lib/construct-compat';

class MyConstruct extends Construct {
Expand Down Expand Up @@ -29,7 +29,7 @@ export = {
'Aspects are invoked only once'(test: Test) {
const app = new App();
const root = new MyConstruct(app, 'MyConstruct');
root.node.applyAspect(new VisitOnce());
Aspects.of(root).add(new VisitOnce());
app.synth();
test.deepEqual(root.visitCounter, 1);
app.synth();
Expand All @@ -41,9 +41,9 @@ export = {
const app = new App();
const root = new MyConstruct(app, 'MyConstruct');
const child = new MyConstruct(root, 'ChildConstruct');
root.node.applyAspect({
Aspects.of(root).add({
visit(construct: IConstruct) {
construct.node.applyAspect({
Aspects.of(construct).add({
visit(inner: IConstruct) {
inner.node.addMetadata('test', 'would-be-ignored');
},
Expand All @@ -62,7 +62,7 @@ export = {
const app = new App();
const root = new MyConstruct(app, 'Construct');
const child = new MyConstruct(root, 'ChildConstruct');
root.node.applyAspect(new MyAspect());
Aspects.of(root).add(new MyAspect());
app.synth();
test.deepEqual(root.node.metadata[0].type, 'foo');
test.deepEqual(root.node.metadata[0].data, 'bar');
Expand Down
4 changes: 2 additions & 2 deletions packages/@aws-cdk/core/test/test.stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as cxapi from '@aws-cdk/cx-api';
import { Test } from 'nodeunit';
import {
App, CfnCondition, CfnInclude, CfnOutput, CfnParameter,
CfnResource, Construct, Lazy, ScopedAws, Stack, Tag, validateString, ISynthesisSession } from '../lib';
CfnResource, Construct, Lazy, ScopedAws, Stack, validateString, ISynthesisSession, Tags } from '../lib';
import { Intrinsic } from '../lib/private/intrinsic';
import { resolveReferences } from '../lib/private/refs';
import { PostResolveToken } from '../lib/util';
Expand Down Expand Up @@ -840,7 +840,7 @@ export = {
const stack2 = new Stack(stack1, 'stack2');

// WHEN
Tag.add(app, 'foo', 'bar');
Tags.of(app).add('foo', 'bar');

// THEN
const asm = app.synth();
Expand Down
6 changes: 3 additions & 3 deletions packages/@aws-cdk/core/test/test.stage.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as cxapi from '@aws-cdk/cx-api';
import { Test } from 'nodeunit';
import { App, CfnResource, Construct, IAspect, IConstruct, Stack, Stage } from '../lib';
import { App, CfnResource, Construct, IAspect, IConstruct, Stack, Stage, Aspects } from '../lib';

export = {
'Stack inherits unspecified part of the env from Stage'(test: Test) {
Expand Down Expand Up @@ -148,7 +148,7 @@ export = {

// WHEN
const aspect = new TouchingAspect();
stack.node.applyAspect(aspect);
Aspects.of(stack).add(aspect);

// THEN
app.synth();
Expand All @@ -168,7 +168,7 @@ export = {

// WHEN
const aspect = new TouchingAspect();
app.node.applyAspect(aspect);
Aspects.of(app).add(aspect);

// THEN
app.synth();
Expand Down
Loading

0 comments on commit 4bc91ef

Please sign in to comment.