Skip to content

Commit 7861e58

Browse files
committed
chore(mixins-preview): implement Mixins RFC
1 parent 47a9a20 commit 7861e58

File tree

23 files changed

+1253
-85
lines changed

23 files changed

+1253
-85
lines changed

packages/@aws-cdk/mixins-preview/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,22 @@ Mixins.of(bucket)
3737
.apply(new AutoDeleteObjects());
3838
```
3939

40+
### Fluent Syntax with `.with()`
41+
42+
For convenience, you can use the `.with()` method for a more fluent syntax:
43+
44+
```typescript
45+
import '@aws-cdk/mixins-preview/with';
46+
47+
const bucket = new s3.CfnBucket(scope, "MyBucket")
48+
.with(new EnableVersioning())
49+
.with(new AutoDeleteObjects());
50+
```
51+
52+
The `.with()` method is available after importing `@aws-cdk/mixins-preview/with`, which augments all constructs with this method. It provides the same functionality as `Mixins.of().apply()` but with a more chainable API.
53+
54+
> **Note**: The `.with()` fluent syntax is only available in JavaScript and TypeScript. Other jsii languages (Python, Java, C#, and Go) should use the `Mixins.of(...).mustApply()` syntax instead. The import requirement is temporary during the preview phase. Once the API is stable, the `.with()` method will be available by default on all constructs and in all languages.
55+
4056
## Creating Custom Mixins
4157

4258
Mixins are simple classes that implement the `IMixin` interface:
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type { IConstruct } from 'constructs';
2+
import { UnscopedValidationError, ValidationError } from 'aws-cdk-lib/core';
3+
import type { IMixin } from './mixins';
4+
import { ConstructSelector } from './selectors';
5+
6+
/**
7+
* Applies mixins to constructs.
8+
*/
9+
export class MixinApplicator {
10+
private readonly scope: IConstruct;
11+
private readonly selector: ConstructSelector;
12+
13+
constructor(
14+
scope: IConstruct,
15+
selector: ConstructSelector = ConstructSelector.all(),
16+
) {
17+
this.scope = scope;
18+
this.selector = selector;
19+
}
20+
21+
/**
22+
* Applies a mixin to selected constructs.
23+
*/
24+
apply(mixin: IMixin): this {
25+
const constructs = this.selector.select(this.scope);
26+
for (const construct of constructs) {
27+
if (mixin.supports(construct)) {
28+
mixin.applyTo(construct);
29+
const errors = mixin.validate?.(construct) ?? [];
30+
if (errors.length > 0) {
31+
throw new ValidationError(`Mixin validation failed: ${errors.join(', ')}`, construct);
32+
}
33+
}
34+
}
35+
return this;
36+
}
37+
38+
/**
39+
* Applies a mixin and requires that it be applied to at least one construct.
40+
*/
41+
mustApply(mixin: IMixin): this {
42+
const constructs = this.selector.select(this.scope);
43+
let applied = false;
44+
for (const construct of constructs) {
45+
if (mixin.supports(construct)) {
46+
mixin.applyTo(construct);
47+
const errors = mixin.validate?.(construct) ?? [];
48+
if (errors.length > 0) {
49+
throw new ValidationError(`Mixin validation failed: ${errors.join(', ')}`, construct);
50+
}
51+
applied = true;
52+
}
53+
}
54+
if (!applied) {
55+
throw new UnscopedValidationError(`Mixin ${mixin.constructor.name} could not be applied to any constructs`);
56+
}
57+
return this;
58+
}
59+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Re-export all core functionality from separate modules
2+
export * from './mixins';
3+
export * from './selectors';
4+
export * from './applicator';
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { IConstruct } from 'constructs';
2+
import type { ConstructSelector } from './selectors';
3+
import { MixinApplicator } from './applicator';
4+
5+
/**
6+
* Main entry point for applying mixins.
7+
*/
8+
export class Mixins {
9+
/**
10+
* Creates a MixinApplicator for the given scope.
11+
*/
12+
static of(scope: IConstruct, selector?: ConstructSelector): MixinApplicator {
13+
return new MixinApplicator(scope, selector);
14+
}
15+
}
16+
17+
/**
18+
* A mixin is a reusable piece of functionality that can be applied to constructs
19+
* to add behavior, properties, or modify existing functionality without inheritance.
20+
*/
21+
export interface IMixin {
22+
/**
23+
* Determines whether this mixin can be applied to the given construct.
24+
*/
25+
supports(construct: IConstruct): boolean;
26+
27+
/**
28+
* Validates the construct before applying the mixin.
29+
*/
30+
validate?(construct: IConstruct): string[];
31+
32+
/**
33+
* Applies the mixin functionality to the target construct.
34+
*/
35+
applyTo(construct: IConstruct): IConstruct;
36+
}
37+
38+
/**
39+
* Abstract base class for mixins that provides default implementations.
40+
*/
41+
export abstract class Mixin implements IMixin {
42+
public supports(_construct: IConstruct): boolean {
43+
return true;
44+
}
45+
46+
public validate(_construct: IConstruct): string[] {
47+
return [];
48+
}
49+
50+
abstract applyTo(construct: IConstruct): IConstruct;
51+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import type { IConstruct } from 'constructs';
2+
import { CfnResource } from 'aws-cdk-lib/core';
3+
4+
/**
5+
* Selects constructs from a construct tree based on various criteria.
6+
*/
7+
export abstract class ConstructSelector {
8+
/**
9+
* Selects all constructs in the tree.
10+
*/
11+
static all(): ConstructSelector {
12+
return new AllConstructsSelector();
13+
}
14+
15+
/**
16+
* Selects CfnResource constructs or the default CfnResource child.
17+
*/
18+
static cfnResource(): ConstructSelector {
19+
return new CfnResourceSelector();
20+
}
21+
22+
/**
23+
* Selects constructs of a specific type.
24+
*/
25+
static resourcesOfType(type: string | any): ConstructSelector {
26+
return new ResourceTypeSelector(type);
27+
}
28+
29+
/**
30+
* Selects constructs whose IDs match a pattern.
31+
*/
32+
static byId(pattern: any): ConstructSelector {
33+
return new IdPatternSelector(pattern);
34+
}
35+
36+
/**
37+
* Selects constructs from the given scope based on the selector's criteria.
38+
*/
39+
abstract select(scope: IConstruct): IConstruct[];
40+
}
41+
42+
class AllConstructsSelector extends ConstructSelector {
43+
select(scope: IConstruct): IConstruct[] {
44+
const result: IConstruct[] = [];
45+
const visit = (node: IConstruct) => {
46+
result.push(node);
47+
for (const child of node.node.children) {
48+
visit(child);
49+
}
50+
};
51+
visit(scope);
52+
return result;
53+
}
54+
}
55+
56+
class CfnResourceSelector extends ConstructSelector {
57+
select(scope: IConstruct): IConstruct[] {
58+
if (CfnResource.isCfnResource(scope)) {
59+
return [scope];
60+
}
61+
const defaultChild = scope.node.defaultChild;
62+
if (CfnResource.isCfnResource(defaultChild)) {
63+
return [defaultChild];
64+
}
65+
return [];
66+
}
67+
}
68+
69+
class ResourceTypeSelector extends ConstructSelector {
70+
constructor(private readonly type: string | any) {
71+
super();
72+
}
73+
74+
select(scope: IConstruct): IConstruct[] {
75+
const result: IConstruct[] = [];
76+
const visit = (node: IConstruct) => {
77+
if (typeof this.type === 'string') {
78+
if (CfnResource.isCfnResource(node) && node.cfnResourceType === this.type) {
79+
result.push(node);
80+
}
81+
} else if ('isCfnResource' in this.type && 'CFN_RESOURCE_TYPE_NAME' in this.type) {
82+
if (CfnResource.isCfnResource(node) && node.cfnResourceType === this.type.CFN_RESOURCE_TYPE_NAME) {
83+
result.push(node);
84+
}
85+
} else {
86+
if (node instanceof this.type) {
87+
result.push(node);
88+
}
89+
}
90+
for (const child of node.node.children) {
91+
visit(child);
92+
}
93+
};
94+
visit(scope);
95+
return result;
96+
}
97+
}
98+
99+
class IdPatternSelector extends ConstructSelector {
100+
constructor(private readonly pattern: any) {
101+
super();
102+
}
103+
104+
select(scope: IConstruct): IConstruct[] {
105+
const result: IConstruct[] = [];
106+
const visit = (node: IConstruct) => {
107+
if (this.pattern && typeof this.pattern.test === 'function' && this.pattern.test(node.node.id)) {
108+
result.push(node);
109+
}
110+
for (const child of node.node.children) {
111+
visit(child);
112+
}
113+
};
114+
visit(scope);
115+
return result;
116+
}
117+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import * as path from 'path';
2+
import type { Construct } from 'constructs';
3+
import type { CustomResourceProviderOptions } from 'aws-cdk-lib/core';
4+
import { Stack, CustomResourceProviderBase, determineLatestNodeRuntimeName } from 'aws-cdk-lib/core';
5+
6+
export class AutoDeleteObjectsProvider extends CustomResourceProviderBase {
7+
public static getOrCreate(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): string {
8+
return this.getOrCreateProvider(scope, uniqueid, props).serviceToken;
9+
}
10+
11+
public static getOrCreateProvider(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): AutoDeleteObjectsProvider {
12+
const id = `${uniqueid}CustomResourceProvider`;
13+
const stack = Stack.of(scope);
14+
const existing = stack.node.tryFindChild(id) as AutoDeleteObjectsProvider;
15+
return existing ?? new AutoDeleteObjectsProvider(stack, id, props);
16+
}
17+
18+
private constructor(scope: Construct, id: string, props?: CustomResourceProviderOptions) {
19+
super(scope, id, {
20+
...props,
21+
codeDirectory: path.join(__dirname, '..', 'dist', 'aws-s3', 'auto-delete-objects-handler'),
22+
runtimeName: determineLatestNodeRuntimeName(scope),
23+
});
24+
this.node.addMetadata('aws:cdk:is-custom-resource-handler-customResourceProvider', true);
25+
}
26+
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
export * from './mixins';
1+
export * as core from './core';
2+
export * as mixins from './mixins';
Lines changed: 1 addition & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1 @@
1-
import type { IConstruct } from 'constructs';
2-
3-
/**
4-
* A mixin is a reusable piece of functionality that can be applied to constructs
5-
* to add behavior, properties, or modify existing functionality without inheritance.
6-
*
7-
* Mixins follow a three-phase pattern:
8-
* 1. Check if the mixin supports the target construct (supports)
9-
* 2. Optionally validate the construct before applying (validate)
10-
* 3. Apply the mixin functionality to the construct (applyTo)
11-
*/
12-
export interface IMixin {
13-
/**
14-
* Determines whether this mixin can be applied to the given construct.
15-
*
16-
* This method should perform type checking and compatibility validation
17-
* to ensure the mixin can safely operate on the construct.
18-
*
19-
* @param construct - The construct to check for compatibility
20-
* @returns true if the mixin supports this construct type, false otherwise
21-
*/
22-
supports(construct: IConstruct): boolean;
23-
24-
/**
25-
* Validates the construct before applying the mixin.
26-
*
27-
* This optional method allows the mixin to perform additional validation
28-
* beyond basic type compatibility. It can check for required properties,
29-
* configuration constraints, or other preconditions.
30-
*
31-
* @param construct - The construct to validate
32-
* @returns An array of validation error messages, or empty array if valid
33-
*/
34-
validate?(construct: IConstruct): string[];
35-
36-
/**
37-
* Applies the mixin functionality to the target construct.
38-
*
39-
* This method performs the actual work of the mixin, such as:
40-
* - Adding new properties or methods
41-
* - Modifying existing behavior
42-
* - Setting up additional resources or configurations
43-
* - Establishing relationships with other constructs
44-
*
45-
* @param construct - The construct to apply the mixin to
46-
* @returns The modified construct (may be the same instance or a wrapper)
47-
*/
48-
applyTo(construct: IConstruct): IConstruct;
49-
}
50-
51-
/**
52-
* Abstract base class for mixins that provides default implementations
53-
* and simplifies mixin creation.
54-
*/
55-
export abstract class Mixin implements IMixin {
56-
/**
57-
* Default implementation that supports any construct.
58-
* Override this method to add type-specific support logic.
59-
*/
60-
public supports(_construct: IConstruct): boolean {
61-
return true;
62-
}
63-
64-
/**
65-
* Default validation implementation that returns no errors.
66-
* Override this method to add custom validation logic.
67-
*/
68-
public validate(_construct: IConstruct): string[] {
69-
return [];
70-
}
71-
72-
abstract applyTo(construct: IConstruct): IConstruct;
73-
}
1+
export * from './property-mixins';
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Strategy for handling nested properties in L1 property mixins
3+
*/
4+
export enum PropertyMergeStrategy {
5+
/**
6+
* Override all properties
7+
*/
8+
OVERRIDE = 'override',
9+
/**
10+
* Deep merge nested objects, override primitives and arrays
11+
*/
12+
MERGE = 'merge',
13+
}
14+
15+
/**
16+
* Options for applying CfnProperty mixins
17+
*/
18+
export interface CfnPropertyMixinOptions {
19+
/**
20+
* Strategy for merging nested properties
21+
*
22+
* @default - PropertyMergeStrategy.MERGE
23+
*/
24+
readonly strategy?: PropertyMergeStrategy;
25+
}

0 commit comments

Comments
 (0)