-
Notifications
You must be signed in to change notification settings - Fork 4.3k
chore(mixins-preview): implement Mixins RFC #36013
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import type { IConstruct } from 'constructs'; | ||
| import { ValidationError } from 'aws-cdk-lib/core'; | ||
| import type { IMixin } from './mixins'; | ||
| import { ConstructSelector } from './selectors'; | ||
|
|
||
| /** | ||
| * Applies mixins to constructs. | ||
| */ | ||
| export class MixinApplicator { | ||
| private readonly scope: IConstruct; | ||
| private readonly selector: ConstructSelector; | ||
|
|
||
| constructor( | ||
| scope: IConstruct, | ||
| selector: ConstructSelector = ConstructSelector.all(), | ||
| ) { | ||
| this.scope = scope; | ||
| this.selector = selector; | ||
| } | ||
|
|
||
| /** | ||
| * Applies a mixin to selected constructs. | ||
| */ | ||
| apply(mixin: IMixin): this { | ||
| const constructs = this.selector.select(this.scope); | ||
| for (const construct of constructs) { | ||
| if (mixin.supports(construct)) { | ||
| const errors = mixin.validate?.(construct) ?? []; | ||
| if (errors.length > 0) { | ||
| throw new ValidationError(`Mixin validation failed: ${errors.join(', ')}`, this.scope); | ||
| } | ||
| mixin.applyTo(construct); | ||
| } | ||
| } | ||
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * Applies a mixin and requires that it be applied to at least one construct. | ||
| */ | ||
| mustApply(mixin: IMixin): this { | ||
| const constructs = this.selector.select(this.scope); | ||
| let applied = false; | ||
| for (const construct of constructs) { | ||
| if (mixin.supports(construct)) { | ||
| const errors = mixin.validate?.(construct) ?? []; | ||
| if (errors.length > 0) { | ||
| throw new ValidationError(`Mixin validation failed: ${errors.join(', ')}`, construct); | ||
| } | ||
| mixin.applyTo(construct); | ||
| applied = true; | ||
| } | ||
| } | ||
| if (!applied) { | ||
| throw new ValidationError(`Mixin ${mixin.constructor.name} could not be applied to any constructs`, this.scope); | ||
| } | ||
| return this; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| // Re-export all core functionality from separate modules | ||
| export * from './mixins'; | ||
| export * from './selectors'; | ||
| export * from './applicator'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| import type { IConstruct } from 'constructs'; | ||
| import type { ConstructSelector } from './selectors'; | ||
| import { MixinApplicator } from './applicator'; | ||
|
|
||
| // this will change when we update the interface to deliberately break compatibility checks | ||
| const MIXIN_SYMBOL = Symbol.for('@aws-cdk/mixins-preview.Mixin.pre1'); | ||
|
|
||
| /** | ||
| * Main entry point for applying mixins. | ||
| */ | ||
| export class Mixins { | ||
| /** | ||
| * Creates a MixinApplicator for the given scope. | ||
| */ | ||
| static of(scope: IConstruct, selector?: ConstructSelector): MixinApplicator { | ||
| return new MixinApplicator(scope, selector); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * A mixin is a reusable piece of functionality that can be applied to constructs | ||
| * to add behavior, properties, or modify existing functionality without inheritance. | ||
| */ | ||
| export interface IMixin { | ||
| /** | ||
| * Determines whether this mixin can be applied to the given construct. | ||
| */ | ||
| supports(construct: IConstruct): boolean; | ||
|
|
||
| /** | ||
| * Validates the construct before applying the mixin. | ||
| */ | ||
| validate?(construct: IConstruct): string[]; | ||
|
|
||
| /** | ||
| * Applies the mixin functionality to the target construct. | ||
| */ | ||
| applyTo(construct: IConstruct): IConstruct; | ||
| } | ||
|
|
||
| /** | ||
| * Abstract base class for mixins that provides default implementations. | ||
| */ | ||
| export abstract class Mixin implements IMixin { | ||
mrgrain marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /** | ||
| * Checks if `x` is a Mixin. | ||
| * | ||
| * @param x Any object | ||
| * @returns true if `x` is an object created from a class which extends `Mixin`. | ||
| */ | ||
| static isMixin(x: any): x is Mixin { | ||
| return x != null && typeof x === 'object' && MIXIN_SYMBOL in x; | ||
| } | ||
|
|
||
| constructor() { | ||
| Object.defineProperty(this, MIXIN_SYMBOL, { value: true }); | ||
| } | ||
|
|
||
| public supports(_construct: IConstruct): boolean { | ||
| return true; | ||
| } | ||
|
|
||
| public validate(_construct: IConstruct): string[] { | ||
| return []; | ||
| } | ||
|
|
||
| abstract applyTo(construct: IConstruct): IConstruct; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| import type { IConstruct } from 'constructs'; | ||
| import { CfnResource } from 'aws-cdk-lib/core'; | ||
|
|
||
| /** | ||
| * Selects constructs from a construct tree based on various criteria. | ||
| */ | ||
| export abstract class ConstructSelector { | ||
| /** | ||
| * Selects all constructs in the tree. | ||
| */ | ||
| static all(): ConstructSelector { | ||
| return new AllConstructsSelector(); | ||
| } | ||
|
|
||
| /** | ||
| * Selects CfnResource constructs or the default CfnResource child. | ||
| */ | ||
| static cfnResource(): ConstructSelector { | ||
| return new CfnResourceSelector(); | ||
| } | ||
|
|
||
| /** | ||
| * Selects constructs of a specific type. | ||
| */ | ||
| static resourcesOfType(type: string | any): ConstructSelector { | ||
| return new ResourceTypeSelector(type); | ||
| } | ||
|
|
||
| /** | ||
| * Selects constructs whose IDs match a pattern. | ||
| */ | ||
| static byId(pattern: any): ConstructSelector { | ||
| return new IdPatternSelector(pattern); | ||
| } | ||
|
|
||
| /** | ||
| * Selects constructs from the given scope based on the selector's criteria. | ||
| */ | ||
| abstract select(scope: IConstruct): IConstruct[]; | ||
| } | ||
|
|
||
| class AllConstructsSelector extends ConstructSelector { | ||
| select(scope: IConstruct): IConstruct[] { | ||
| return scope.node.findAll(); | ||
| } | ||
| } | ||
|
|
||
| class CfnResourceSelector extends ConstructSelector { | ||
| select(scope: IConstruct): IConstruct[] { | ||
| if (CfnResource.isCfnResource(scope)) { | ||
| return [scope]; | ||
| } | ||
| const defaultChild = scope.node.defaultChild; | ||
| if (CfnResource.isCfnResource(defaultChild)) { | ||
| return [defaultChild]; | ||
| } | ||
| return []; | ||
| } | ||
| } | ||
|
|
||
| class ResourceTypeSelector extends ConstructSelector { | ||
| constructor(private readonly type: string | any) { | ||
| super(); | ||
| } | ||
|
|
||
| select(scope: IConstruct): IConstruct[] { | ||
| const result: IConstruct[] = []; | ||
| const visit = (node: IConstruct) => { | ||
| if (typeof this.type === 'string') { | ||
| if (CfnResource.isCfnResource(node) && node.cfnResourceType === this.type) { | ||
| result.push(node); | ||
| } | ||
| } else if ('isCfnResource' in this.type && 'CFN_RESOURCE_TYPE_NAME' in this.type) { | ||
| if (CfnResource.isCfnResource(node) && node.cfnResourceType === this.type.CFN_RESOURCE_TYPE_NAME) { | ||
| result.push(node); | ||
| } | ||
| } else { | ||
| if (node instanceof this.type) { | ||
| result.push(node); | ||
| } | ||
| } | ||
| for (const child of node.node.children) { | ||
| visit(child); | ||
| } | ||
| }; | ||
| visit(scope); | ||
| return result; | ||
| } | ||
| } | ||
|
|
||
| class IdPatternSelector extends ConstructSelector { | ||
| constructor(private readonly pattern: any) { | ||
| super(); | ||
| } | ||
|
|
||
| select(scope: IConstruct): IConstruct[] { | ||
| const result: IConstruct[] = []; | ||
| const visit = (node: IConstruct) => { | ||
| if (this.pattern && typeof this.pattern.test === 'function' && this.pattern.test(node.node.id)) { | ||
| result.push(node); | ||
| } | ||
| for (const child of node.node.children) { | ||
| visit(child); | ||
| } | ||
| }; | ||
| visit(scope); | ||
| return result; | ||
| } | ||
| } |
26 changes: 26 additions & 0 deletions
26
...ws-cdk/mixins-preview/lib/custom-resource-handlers/aws-s3/auto-delete-objects-provider.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import * as path from 'path'; | ||
| import type { Construct } from 'constructs'; | ||
| import type { CustomResourceProviderOptions } from 'aws-cdk-lib/core'; | ||
| import { Stack, CustomResourceProviderBase, determineLatestNodeRuntimeName } from 'aws-cdk-lib/core'; | ||
|
|
||
| export class AutoDeleteObjectsProvider extends CustomResourceProviderBase { | ||
| public static getOrCreate(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): string { | ||
| return this.getOrCreateProvider(scope, uniqueid, props).serviceToken; | ||
| } | ||
|
|
||
| public static getOrCreateProvider(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): AutoDeleteObjectsProvider { | ||
| const id = `${uniqueid}CustomResourceProvider`; | ||
| const stack = Stack.of(scope); | ||
| const existing = stack.node.tryFindChild(id) as AutoDeleteObjectsProvider; | ||
| return existing ?? new AutoDeleteObjectsProvider(stack, id, props); | ||
| } | ||
|
|
||
| private constructor(scope: Construct, id: string, props?: CustomResourceProviderOptions) { | ||
| super(scope, id, { | ||
| ...props, | ||
| codeDirectory: path.join(__dirname, '..', 'dist', 'aws-s3', 'auto-delete-objects-handler'), | ||
| runtimeName: determineLatestNodeRuntimeName(scope), | ||
| }); | ||
| this.node.addMetadata('aws:cdk:is-custom-resource-handler-customResourceProvider', true); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| export * from './mixins'; | ||
| export * as core from './core'; | ||
| export * as mixins from './mixins'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,73 +1 @@ | ||
| import type { IConstruct } from 'constructs'; | ||
|
|
||
| /** | ||
| * A mixin is a reusable piece of functionality that can be applied to constructs | ||
| * to add behavior, properties, or modify existing functionality without inheritance. | ||
| * | ||
| * Mixins follow a three-phase pattern: | ||
| * 1. Check if the mixin supports the target construct (supports) | ||
| * 2. Optionally validate the construct before applying (validate) | ||
| * 3. Apply the mixin functionality to the construct (applyTo) | ||
| */ | ||
| export interface IMixin { | ||
| /** | ||
| * Determines whether this mixin can be applied to the given construct. | ||
| * | ||
| * This method should perform type checking and compatibility validation | ||
| * to ensure the mixin can safely operate on the construct. | ||
| * | ||
| * @param construct - The construct to check for compatibility | ||
| * @returns true if the mixin supports this construct type, false otherwise | ||
| */ | ||
| supports(construct: IConstruct): boolean; | ||
|
|
||
| /** | ||
| * Validates the construct before applying the mixin. | ||
| * | ||
| * This optional method allows the mixin to perform additional validation | ||
| * beyond basic type compatibility. It can check for required properties, | ||
| * configuration constraints, or other preconditions. | ||
| * | ||
| * @param construct - The construct to validate | ||
| * @returns An array of validation error messages, or empty array if valid | ||
| */ | ||
| validate?(construct: IConstruct): string[]; | ||
|
|
||
| /** | ||
| * Applies the mixin functionality to the target construct. | ||
| * | ||
| * This method performs the actual work of the mixin, such as: | ||
| * - Adding new properties or methods | ||
| * - Modifying existing behavior | ||
| * - Setting up additional resources or configurations | ||
| * - Establishing relationships with other constructs | ||
| * | ||
| * @param construct - The construct to apply the mixin to | ||
| * @returns The modified construct (may be the same instance or a wrapper) | ||
| */ | ||
| applyTo(construct: IConstruct): IConstruct; | ||
| } | ||
|
|
||
| /** | ||
| * Abstract base class for mixins that provides default implementations | ||
| * and simplifies mixin creation. | ||
| */ | ||
| export abstract class Mixin implements IMixin { | ||
| /** | ||
| * Default implementation that supports any construct. | ||
| * Override this method to add type-specific support logic. | ||
| */ | ||
| public supports(_construct: IConstruct): boolean { | ||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Default validation implementation that returns no errors. | ||
| * Override this method to add custom validation logic. | ||
| */ | ||
| public validate(_construct: IConstruct): string[] { | ||
| return []; | ||
| } | ||
|
|
||
| abstract applyTo(construct: IConstruct): IConstruct; | ||
| } | ||
| export * from './property-mixins'; |
25 changes: 25 additions & 0 deletions
25
packages/@aws-cdk/mixins-preview/lib/mixins/property-mixins.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| /** | ||
| * Strategy for handling nested properties in L1 property mixins | ||
| */ | ||
| export enum PropertyMergeStrategy { | ||
| /** | ||
| * Override all properties | ||
| */ | ||
| OVERRIDE = 'override', | ||
| /** | ||
| * Deep merge nested objects, override primitives and arrays | ||
| */ | ||
| MERGE = 'merge', | ||
| } | ||
|
|
||
| /** | ||
| * Options for applying CfnProperty mixins | ||
| */ | ||
| export interface CfnPropertyMixinOptions { | ||
| /** | ||
| * Strategy for merging nested properties | ||
| * | ||
| * @default - PropertyMergeStrategy.MERGE | ||
| */ | ||
| readonly strategy?: PropertyMergeStrategy; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this not failing the jsii build?
I also still think
validate()doesn't need to be here.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why should it be failing the build (it doesn't)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That might be an oversight. There is approximately 0% chance this gets translated appropriately to Java.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like jsii is silently ignoring it 🙈
{ "abstract": true, "docs": { "stability": "experimental", "summary": "Validates the construct before applying the mixin." }, "locationInModule": { "filename": "lib/core/mixins.ts", "line": 33 }, "name": "validate", "parameters": [ { "name": "construct", "type": { "fqn": "constructs.IConstruct" } } ], "returns": { "type": { "collection": { "elementtype": { "primitive": "string" }, "kind": "array" } } } }