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): add EvaluateExpression task #4602

Merged
merged 6 commits into from
Nov 4, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// tslint:disable:no-console no-eval
import { Event } from '../eval-task';

export async function handler(event: Event): Promise<any> {
console.log('Event: %j', event);

const expression = Object.entries(event.expressionAttributeValues)
.reduce(
(exp, [k, v]) => exp.replace(k, JSON.stringify(v)),
event.expression
);
console.log(`Expression: ${expression}`);

return eval(expression);
}
104 changes: 104 additions & 0 deletions packages/@aws-cdk/aws-stepfunctions-tasks/lib/eval-task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import iam = require('@aws-cdk/aws-iam');
import lambda = require('@aws-cdk/aws-lambda');
import sfn = require('@aws-cdk/aws-stepfunctions');
import cdk = require('@aws-cdk/core');
import path = require('path');

/**
* Properties for EvalTask
*
* @experimental
*/
export interface EvaluateExpressionProps {
/**
* The expression to evaluate. It must contain state paths.
*
* @example '$.a + $.b'
*/
readonly expression: string;

/**
* The runtime language to use to evaluate the expression.
*
* @default lambda.Runtime.NODEJS_10_X
*/
readonly runtime?: lambda.Runtime;
}

/**
* The event received by the Lambda function
*
* @internal
*/
export interface Event {
/**
* The expression to evaluate
*/
readonly expression: string;

/**
* The expression attribute values
*/
readonly expressionAttributeValues: { [key: string]: any };
}

/**
* A Step Functions Task to evaluate an expression
*
* OUTPUT: the output of this task is the evaluated expression.
*
* @experimental
*/
export class EvaluateExpression implements sfn.IStepFunctionsTask {
constructor(private readonly props: EvaluateExpressionProps) {
}

public bind(task: sfn.Task): sfn.StepFunctionsTaskConfig {
const matches = this.props.expression.match(/\$[.\[][.a-zA-Z[\]0-9]+/g);

if (!matches) {
throw new Error('No paths found in expression');
}

const expressionAttributeValues = matches.reduce(
(acc, m) => ({
...acc,
[m]: sfn.Data.stringAt(m) // It's okay to always use `stringAt` here
}),
{}
);

const evalFn = createEvalFn(this.props.runtime || lambda.Runtime.NODEJS_10_X, task);

return {
resourceArn: evalFn.functionArn,
policyStatements: [new iam.PolicyStatement({
resources: [evalFn.functionArn],
actions: ['lambda:InvokeFunction'],
})],
parameters: {
expression: this.props.expression,
expressionAttributeValues,
} as Event
};
}
}

function createEvalFn(runtime: lambda.Runtime, scope: cdk.Construct) {
const code = lambda.Code.asset(path.join(__dirname, `eval-${runtime.name}-handler`));
const lambdaPurpose = 'Eval';

switch (runtime) {
case lambda.Runtime.NODEJS_10_X:
return new lambda.SingletonFunction(scope, 'EvalFunction', {
runtime,
handler: 'index.handler',
uuid: 'a0d2ce44-871b-4e74-87a1-f5e63d7c3bdc',
lambdaPurpose,
code,
});
// TODO: implement other runtimes
default:
throw new Error(`The runtime ${runtime.name} is currently not supported.`);
}
}
3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export * from './run-ecs-fargate-task';
export * from './sagemaker-task-base-types';
export * from './sagemaker-train-task';
export * from './sagemaker-transform-task';
export * from './start-execution';
export * from './start-execution';
export * from './eval-task';
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Event } from '../lib';
import { handler } from '../lib/eval-nodejs10.x-handler';

test('with numbers', async () => {
// GIVEN
const event: Event = {
expression: '$.a + $.b',
expressionAttributeValues: {
'$.a': 4,
'$.b': 5
}
};

// THEN
const evaluated = await handler(event);
expect(evaluated).toBe(9);
});

test('with strings', async () => {
// GIVEN
const event: Event = {
expression: '`${$.a} ${$.b}`',
expressionAttributeValues: {
'$.a': 'Hello',
'$.b': 'world!'
}
};

// THEN
const evaluated = await handler(event);
expect(evaluated).toBe('Hello world!');
});

test('with lists', async () => {
// GIVEN
const event: Event = {
expression: '$.a.map(x => x * 2)',
expressionAttributeValues: {
'$.a': [1, 2, 3],
}
};

// THEN
const evaluated = await handler(event);
expect(evaluated).toEqual([2, 4, 6]);
});
53 changes: 53 additions & 0 deletions packages/@aws-cdk/aws-stepfunctions-tasks/test/eval-task.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import '@aws-cdk/assert/jest';
import sfn = require('@aws-cdk/aws-stepfunctions');
import { Stack } from '@aws-cdk/core';
import tasks = require('../lib');

let stack: Stack;
beforeEach(() => {
stack = new Stack();
});

test('Eval with Node.js', () => {
// WHEN
const task = new sfn.Task(stack, 'Task', {
task: new tasks.EvaluateExpression({
expression: '$.a + $.b',
})
});
new sfn.StateMachine(stack, 'SM', {
definition: task
});

// THEN
expect(stack).toHaveResource('AWS::StepFunctions::StateMachine', {
DefinitionString: {
"Fn::Join": [
"",
[
"{\"StartAt\":\"Task\",\"States\":{\"Task\":{\"End\":true,\"Parameters\":{\"expression\":\"$.a + $.b\",\"expressionAttributeValues\":{\"$.a.$\":\"$.a\",\"$.b.$\":\"$.b\"}},\"Type\":\"Task\",\"Resource\":\"",
{
"Fn::GetAtt": [
"Evala0d2ce44871b4e7487a1f5e63d7c3bdc4DAC06E1",
"Arn"
]
},
"\"}}}"
]
]
},
});

expect(stack).toHaveResource('AWS::Lambda::Function', {
Runtime: 'nodejs10.x'
});
});

test('Throws when expression does not contain paths', () => {
// WHEN
expect(() => new sfn.Task(stack, 'Task', {
task: new tasks.EvaluateExpression({
expression: '2 + 2',
})
})).toThrow(/No paths found in expression/);
});
Loading