Skip to content

Commit 3dc8b7d

Browse files
committed
feat(mixins-preview): strongly-typed ConstructSelector interface
Changed `Mixins.of()` to take a `IConstructSelector` instead of only one of the provided selectors. This allows users to implement custom selectors. Added `ConstructSelector.byPath()` to match on a construct path, to complement the existing `byId()`. BREAKING CHANGE: `ConstructSelector.byId()` now takes a glob pattern instead of a regular expression. BREAKING CHANGE: `ConstructSelector.resourcesOfType()` now must receive a string.
1 parent 5858006 commit 3dc8b7d

File tree

8 files changed

+72
-49
lines changed

8 files changed

+72
-49
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,9 @@
9595
"scripts/@aws-cdk/script-tests"
9696
],
9797
"nohoist": [
98+
"**/@types/glob",
9899
"**/jszip",
99100
"**/jszip/**",
100-
"**/@types/glob",
101101
"@aws-cdk/assertions-alpha/fs-extra",
102102
"@aws-cdk/assertions-alpha/fs-extra/**",
103103
"@aws-cdk/assertions/fs-extra",
@@ -146,6 +146,8 @@
146146
"@aws-cdk/core/yaml/**",
147147
"@aws-cdk/cx-api/semver",
148148
"@aws-cdk/cx-api/semver/**",
149+
"@aws-cdk/mixins-preview/minimatch",
150+
"@aws-cdk/mixins-preview/minimatch/**",
149151
"@aws-cdk/pipelines/aws-sdk",
150152
"@aws-cdk/pipelines/aws-sdk/**",
151153
"@aws-cdk/yaml-cfn/yaml",

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,11 @@ Mixins operate on construct trees and can be applied selectively:
9595
Mixins.of(scope).apply(new EncryptionAtRest());
9696

9797
// Apply to specific resource types
98-
Mixins.of(scope, ConstructSelector.resourcesOfType(s3.CfnBucket))
98+
Mixins.of(scope, ConstructSelector.resourcesOfType(s3.CfnBucket.CFN_RESOURCE_TYPE_NAME))
9999
.apply(new EncryptionAtRest());
100100

101-
// Apply to constructs matching a pattern
102-
Mixins.of(scope, ConstructSelector.byId(/.*-prod-.*/))
101+
// Apply to constructs matching a path pattern
102+
Mixins.of(scope, ConstructSelector.byPath("**/*-prod-*/**"))
103103
.apply(new ProductionSecurityMixin());
104104
```
105105

packages/@aws-cdk/mixins-preview/lib/core/applicator.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import type { IConstruct } from 'constructs';
22
import { ValidationError } from 'aws-cdk-lib/core';
33
import type { IMixin } from './mixins';
4-
import { ConstructSelector } from './selectors';
4+
import { ConstructSelector, type IConstructSelector } from './selectors';
55

66
/**
77
* Applies mixins to constructs.
88
*/
99
export class MixinApplicator {
1010
private readonly scope: IConstruct;
11-
private readonly selector: ConstructSelector;
11+
private readonly selector: IConstructSelector;
1212

1313
constructor(
1414
scope: IConstruct,
15-
selector: ConstructSelector = ConstructSelector.all(),
15+
selector: IConstructSelector = ConstructSelector.all(),
1616
) {
1717
this.scope = scope;
1818
this.selector = selector;

packages/@aws-cdk/mixins-preview/lib/core/mixins.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { IConstruct } from 'constructs';
2-
import type { ConstructSelector } from './selectors';
2+
import type { IConstructSelector } from './selectors';
33
import { MixinApplicator } from './applicator';
44

55
// this will change when we update the interface to deliberately break compatibility checks
@@ -12,7 +12,7 @@ export class Mixins {
1212
/**
1313
* Creates a MixinApplicator for the given scope.
1414
*/
15-
static of(scope: IConstruct, selector?: ConstructSelector): MixinApplicator {
15+
static of(scope: IConstruct, selector?: IConstructSelector): MixinApplicator {
1616
return new MixinApplicator(scope, selector);
1717
}
1818
}

packages/@aws-cdk/mixins-preview/lib/core/selectors.ts

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,72 @@
1-
import type { IConstruct } from 'constructs';
1+
import type { IConstruct, Node } from 'constructs';
22
import { CfnResource } from 'aws-cdk-lib/core';
33

4+
/**
5+
* Selects constructs from a construct tree.
6+
*/
7+
export interface IConstructSelector {
8+
/**
9+
* Selects constructs from the given scope based on the selector's criteria.
10+
*/
11+
select(scope: IConstruct): IConstruct[];
12+
}
13+
414
/**
515
* Selects constructs from a construct tree based on various criteria.
616
*/
7-
export abstract class ConstructSelector {
17+
export class ConstructSelector {
818
/**
919
* Selects all constructs in the tree.
1020
*/
11-
static all(): ConstructSelector {
21+
static all(): IConstructSelector {
1222
return new AllConstructsSelector();
1323
}
1424

1525
/**
1626
* Selects CfnResource constructs or the default CfnResource child.
1727
*/
18-
static cfnResource(): ConstructSelector {
28+
static cfnResource(): IConstructSelector {
1929
return new CfnResourceSelector();
2030
}
2131

2232
/**
2333
* Selects only the provided construct.
2434
*/
25-
static onlyItself(): ConstructSelector {
35+
static onlyItself(): IConstructSelector {
2636
return new OnlyItselfSelector();
2737
}
2838

2939
/**
3040
* Selects constructs of a specific type.
3141
*/
32-
static resourcesOfType(type: string | any): ConstructSelector {
33-
return new ResourceTypeSelector(type);
42+
static resourcesOfType(...types: string[]): IConstructSelector {
43+
return new ResourceTypeSelector(types);
3444
}
3545

3646
/**
37-
* Selects constructs whose IDs match a pattern.
47+
* Selects constructs whose construct IDs match a pattern.
48+
* Uses glob like matching.
3849
*/
39-
static byId(pattern: any): ConstructSelector {
40-
return new IdPatternSelector(pattern);
50+
static byId(pattern: string): IConstructSelector {
51+
return new IdPatternSelector(pattern, 'id');
4152
}
4253

4354
/**
44-
* Selects constructs from the given scope based on the selector's criteria.
55+
* Selects constructs whose construct paths match a pattern.
56+
* Uses glob like matching.
4557
*/
46-
abstract select(scope: IConstruct): IConstruct[];
58+
static byPath(pattern: string): IConstructSelector {
59+
return new IdPatternSelector(pattern, 'path');
60+
}
4761
}
4862

49-
class AllConstructsSelector extends ConstructSelector {
63+
class AllConstructsSelector implements IConstructSelector {
5064
select(scope: IConstruct): IConstruct[] {
5165
return scope.node.findAll();
5266
}
5367
}
5468

55-
class CfnResourceSelector extends ConstructSelector {
69+
class CfnResourceSelector implements IConstructSelector {
5670
select(scope: IConstruct): IConstruct[] {
5771
if (CfnResource.isCfnResource(scope)) {
5872
return [scope];
@@ -65,26 +79,15 @@ class CfnResourceSelector extends ConstructSelector {
6579
}
6680
}
6781

68-
class ResourceTypeSelector extends ConstructSelector {
69-
constructor(private readonly type: string | any) {
70-
super();
82+
class ResourceTypeSelector implements IConstructSelector {
83+
constructor(private readonly types: string[]) {
7184
}
7285

7386
select(scope: IConstruct): IConstruct[] {
7487
const result: IConstruct[] = [];
7588
const visit = (node: IConstruct) => {
76-
if (typeof this.type === 'string') {
77-
if (CfnResource.isCfnResource(node) && node.cfnResourceType === this.type) {
78-
result.push(node);
79-
}
80-
} else if ('isCfnResource' in this.type && 'CFN_RESOURCE_TYPE_NAME' in this.type) {
81-
if (CfnResource.isCfnResource(node) && node.cfnResourceType === this.type.CFN_RESOURCE_TYPE_NAME) {
82-
result.push(node);
83-
}
84-
} else {
85-
if (node instanceof this.type) {
86-
result.push(node);
87-
}
89+
if (CfnResource.isCfnResource(node) && this.types.includes(node.cfnResourceType)) {
90+
result.push(node);
8891
}
8992
for (const child of node.node.children) {
9093
visit(child);
@@ -95,15 +98,17 @@ class ResourceTypeSelector extends ConstructSelector {
9598
}
9699
}
97100

98-
class IdPatternSelector extends ConstructSelector {
99-
constructor(private readonly pattern: any) {
100-
super();
101-
}
101+
// Must be a 'require' to not run afoul of ESM module import rules
102+
// eslint-disable-next-line @typescript-eslint/no-require-imports
103+
const minimatch = require('minimatch');
104+
105+
class IdPatternSelector implements IConstructSelector {
106+
constructor(private readonly pattern: string, private field: keyof Node) {}
102107

103108
select(scope: IConstruct): IConstruct[] {
104109
const result: IConstruct[] = [];
105110
const visit = (node: IConstruct) => {
106-
if (this.pattern && typeof this.pattern.test === 'function' && this.pattern.test(node.node.id)) {
111+
if (minimatch(node.node[this.field], this.pattern)) {
107112
result.push(node);
108113
}
109114
for (const child of node.node.children) {
@@ -115,7 +120,7 @@ class IdPatternSelector extends ConstructSelector {
115120
}
116121
}
117122

118-
class OnlyItselfSelector extends ConstructSelector {
123+
class OnlyItselfSelector implements IConstructSelector {
119124
select(scope: IConstruct): IConstruct[] {
120125
return [scope];
121126
}

packages/@aws-cdk/mixins-preview/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,7 @@
648648
"url": "https://aws.amazon.com",
649649
"organization": true
650650
},
651+
"homepage": "https://github.com/aws/aws-cdk",
651652
"license": "Apache-2.0",
652653
"devDependencies": {
653654
"@aws-cdk/cdk-build-tools": "0.0.0",
@@ -665,7 +666,12 @@
665666
"jest": "^29.7.0",
666667
"tsx": "^4.20.6"
667668
},
668-
"homepage": "https://github.com/aws/aws-cdk",
669+
"dependencies": {
670+
"minimatch": "^3.1.2"
671+
},
672+
"bundleDependencies": [
673+
"minimatch"
674+
],
669675
"peerDependencies": {
670676
"aws-cdk-lib": "^0.0.0",
671677
"constructs": "^10.0.0"

packages/@aws-cdk/mixins-preview/test/core/selectors.test.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ describe('ConstructSelector', () => {
3333
const bucket = new s3.CfnBucket(stack, 'Bucket');
3434
const logGroup = new logs.CfnLogGroup(stack, 'LogGroup');
3535

36-
const selected = ConstructSelector.resourcesOfType(s3.CfnBucket).select(stack);
36+
const selected = ConstructSelector.resourcesOfType(s3.CfnBucket.CFN_RESOURCE_TYPE_NAME).select(stack);
3737
expect(selected).toContain(bucket);
3838
expect(selected).not.toContain(logGroup);
3939
});
@@ -51,7 +51,17 @@ describe('ConstructSelector', () => {
5151
const prodBucket = new s3.CfnBucket(stack, 'prod-bucket');
5252
const devBucket = new s3.CfnBucket(stack, 'dev-bucket');
5353

54-
const selected = ConstructSelector.byId(/prod-.*/).select(stack);
54+
const selected = ConstructSelector.byId('*prod*').select(stack);
55+
expect(selected).toContain(prodBucket);
56+
expect(selected).not.toContain(devBucket);
57+
});
58+
59+
test('byPath() selects by construct path pattern', () => {
60+
const scope = new Construct(stack, 'Prefix');
61+
const prodBucket = new s3.CfnBucket(scope, 'prod-bucket');
62+
const devBucket = new s3.CfnBucket(stack, 'dev-bucket');
63+
64+
const selected = ConstructSelector.byPath('*/Prefix/**').select(stack);
5565
expect(selected).toContain(prodBucket);
5666
expect(selected).not.toContain(devBucket);
5767
});

packages/@aws-cdk/mixins-preview/test/mixins/full-example.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ describe('Integration Tests', () => {
2525
// Apply encryption only to production buckets
2626
Mixins.of(
2727
stack,
28-
ConstructSelector.byId(/.*Prod.*/),
28+
ConstructSelector.byId('*Prod*'),
2929
).apply(new s3Mixins.AutoDeleteObjects());
3030

3131
// Apply versioning to all S3 buckets
3232
Mixins.of(
3333
stack,
34-
ConstructSelector.resourcesOfType(s3.CfnBucket),
34+
ConstructSelector.resourcesOfType(s3.CfnBucket.CFN_RESOURCE_TYPE_NAME),
3535
).apply(new s3Mixins.EnableVersioning());
3636

3737
// Verify auto-delete only applied to prod bucket

0 commit comments

Comments
 (0)