Skip to content
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

feat(stepfunctions): support Map ItemSelector #28771

Merged
merged 9 commits into from
Jan 22, 2024
4 changes: 2 additions & 2 deletions packages/aws-cdk-lib/aws-stepfunctions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ execute the same steps for multiple entries of an array in the state input.
const map = new sfn.Map(this, 'Map State', {
maxConcurrency: 1,
itemsPath: sfn.JsonPath.stringAt('$.inputForMap'),
parameters: {
itemSelector: {
item: sfn.JsonPath.stringAt('$$.Map.Item.Value'),
},
resultPath: '$.mapOutput',
Expand Down Expand Up @@ -528,7 +528,7 @@ An `executionType` must be specified for the distributed `Map` workflow.
const map = new sfn.Map(this, 'Map State', {
maxConcurrency: 1,
itemsPath: sfn.JsonPath.stringAt('$.inputForMap'),
parameters: {
itemSelector: {
item: sfn.JsonPath.stringAt('$$.Map.Item.Value'),
},
resultPath: '$.mapOutput',
Expand Down
37 changes: 37 additions & 0 deletions packages/aws-cdk-lib/aws-stepfunctions/lib/states/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,29 @@ export interface MapProps {
/**
* The JSON that you want to override your default iteration input
*
* @deprecated Step Functions has deprecated the `parameters` field in favor of
* the new `itemSelector` field
*
* @see
* https://docs.aws.amazon.com/step-functions/latest/dg/input-output-itemselector.html
*
* @default $
*/
readonly parameters?: { [key: string]: any };

/**
* The JSON that you want to override your default iteration input
*
* Step Functions has deprecated the `parameters` field in favor of
* the new `itemSelector` field.
*
* @see
* https://docs.aws.amazon.com/step-functions/latest/dg/input-output-itemselector.html
*
* @default $
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does $ have a significance as a default? Can this docstring be descriptive?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$ represents the entire state input in Step Functions. Because this is essentially a replacement of parameters, I kept the docstring the same as what it was for parameters.

*/
readonly itemSelector?: { [key: string]: any };

/**
* The JSON that will replace the state's raw result and become the effective
* result before ResultPath is applied.
Expand Down Expand Up @@ -122,12 +141,14 @@ export class Map extends State implements INextable {

private readonly maxConcurrency: number | undefined;
private readonly itemsPath?: string;
private readonly itemSelector?: { [key: string]: any };

constructor(scope: Construct, id: string, props: MapProps = {}) {
super(scope, id, props);
this.endStates = [this];
this.maxConcurrency = props.maxConcurrency;
this.itemsPath = props.itemsPath;
this.itemSelector = props.itemSelector;
}

/**
Expand Down Expand Up @@ -200,6 +221,7 @@ export class Map extends State implements INextable {
...this.renderRetryCatch(),
...this.renderIterator(),
...this.renderItemsPath(),
...this.renderItemSelector(),
...this.renderItemProcessor(),
MaxConcurrency: this.maxConcurrency,
};
Expand All @@ -219,6 +241,10 @@ export class Map extends State implements INextable {
errors.push('Map state cannot have both an iterator and an item processor');
}

if (this.parameters && this.itemSelector) {
errors.push('Map state cannot have both parameters and an item selector');
}

if (this.processorConfig?.mode === ProcessorMode.DISTRIBUTED && !this.processorConfig?.executionType) {
errors.push('You must specify an execution type for the distributed Map workflow');
}
Expand All @@ -240,8 +266,19 @@ export class Map extends State implements INextable {
* Render Parameters in ASL JSON format
*/
private renderParameters(): any {
if (!this.parameters) return undefined;
abdelnn marked this conversation as resolved.
Show resolved Hide resolved
return FieldUtils.renderObject({
Parameters: this.parameters,
});
}

/**
* Render ItemSelector in ASL JSON format
*/
private renderItemSelector(): any {
if (!this.itemSelector) return undefined;
abdelnn marked this conversation as resolved.
Show resolved Hide resolved
return FieldUtils.renderObject({
ItemSelector: this.itemSelector,
});
}
}
84 changes: 84 additions & 0 deletions packages/aws-cdk-lib/aws-stepfunctions/test/map.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,52 @@ describe('Map State', () => {
});
}),

test('State Machine With Map State and Item Selector', () => {
// GIVEN
const stack = new cdk.Stack();

// WHEN
const map = new stepfunctions.Map(stack, 'Map State', {
stateName: 'My-Map-State',
maxConcurrency: 1,
itemsPath: stepfunctions.JsonPath.stringAt('$.inputForMap'),
itemSelector: {
foo: 'foo',
bar: stepfunctions.JsonPath.stringAt('$.bar'),
},
});
map.itemProcessor(new stepfunctions.Pass(stack, 'Pass State'));

// THEN
expect(render(map)).toStrictEqual({
StartAt: 'My-Map-State',
States: {
'My-Map-State': {
Type: 'Map',
End: true,
ItemSelector: {
'foo': 'foo',
'bar.$': '$.bar',
},
ItemProcessor: {
ProcessorConfig: {
Mode: 'INLINE',
},
StartAt: 'Pass State',
States: {
'Pass State': {
Type: 'Pass',
End: true,
},
},
},
ItemsPath: '$.inputForMap',
MaxConcurrency: 1,
},
},
});
}),

test('State Machine With Map State and Item Processor in distributed mode', () => {
// GIVEN
const stack = new cdk.Stack();
Expand Down Expand Up @@ -209,6 +255,23 @@ describe('Map State', () => {
app.synth();
}),

test('synth is successful with item selector', () => {
abdelnn marked this conversation as resolved.
Show resolved Hide resolved
const app = createAppWithMap((stack) => {
const map = new stepfunctions.Map(stack, 'Map State', {
maxConcurrency: 1,
itemsPath: stepfunctions.JsonPath.stringAt('$.inputForMap'),
itemSelector: {
foo: 'foo',
bar: stepfunctions.JsonPath.stringAt('$.bar'),
},
});
map.itemProcessor(new stepfunctions.Pass(stack, 'Pass State'));
return map;
});

app.synth();
}),

test('synth is successful with item processor and distributed mode', () => {
const app = createAppWithMap((stack) => {
const map = new stepfunctions.Map(stack, 'Map State', {
Expand Down Expand Up @@ -253,6 +316,27 @@ describe('Map State', () => {
expect(() => app.synth()).toThrow(/Map state cannot have both an iterator and an item processor/);
}),

test('fails in synthesis if parameters and item selector are defined', () => {
const app = createAppWithMap((stack) => {
const map = new stepfunctions.Map(stack, 'Map State', {
maxConcurrency: 1,
itemsPath: stepfunctions.JsonPath.stringAt('$.inputForMap'),
parameters: {
foo: 'foo',
bar: stepfunctions.JsonPath.stringAt('$.bar'),
},
itemSelector: {
foo: 'foo',
bar: stepfunctions.JsonPath.stringAt('$.bar'),
},
});

return map;
});

expect(() => app.synth()).toThrow(/Map state cannot have both parameters and an item selector/);
}),

test('fails in synthesis if distributed mode and execution type is not defined', () => {
const app = createAppWithMap((stack) => {
const map = new stepfunctions.Map(stack, 'Map State', {
Expand Down
Loading