Skip to content

Commit

Permalink
feat(stepfunctions): add EvaluateExpression task (aws#4602)
Browse files Browse the repository at this point in the history
Use the EvaluateExpression to perform simple operations referencing state paths. The
expression referenced in the task will be evaluated in a Lambda function
(eval()). This allows you to not have to write Lambda code for simple operations.
  • Loading branch information
jogold authored and rix0rrr committed Nov 4, 2019
1 parent 390f00a commit 6dba637
Show file tree
Hide file tree
Showing 8 changed files with 486 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// tslint:disable:no-console no-eval
import { Event } from '../evaluate-expression';

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/evaluate-expression.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 EvaluateExpression
*
* @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 './evaluate-expression';
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]);
});
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

0 comments on commit 6dba637

Please sign in to comment.