From 6a76727e7884024d39c4dc8640f22c171b43884f Mon Sep 17 00:00:00 2001 From: Andrew DiLosa Date: Thu, 12 Jul 2018 01:25:36 -0700 Subject: [PATCH 01/29] Model the Amazon States Language --- .../lib/amazon-states-language.ts | 242 ++++++ .../@aws-cdk/aws-stepfunctions/lib/index.ts | 4 +- .../aws-stepfunctions/lib/state-machine.ts.bk | 732 ++++++++++++++++++ .../aws-stepfunctions/package-lock.json | 108 +++ .../test/test.amazon-states-language.ts | 29 + packages/aws-cdk/package-lock.json | 9 - 6 files changed, 1114 insertions(+), 10 deletions(-) create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/amazon-states-language.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts.bk create mode 100644 packages/@aws-cdk/aws-stepfunctions/package-lock.json create mode 100644 packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/amazon-states-language.ts b/packages/@aws-cdk/aws-stepfunctions/lib/amazon-states-language.ts new file mode 100644 index 0000000000000..d68ac0fa1a3eb --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/amazon-states-language.ts @@ -0,0 +1,242 @@ +export namespace amazon_states_language { + // tslint:disable + export interface Commentable { + Comment?: string + } + + export interface Branch extends Commentable { + States: {[index: string]: State}, + StartAt: string, + } + + export interface StateMachine extends Branch { + Version?: string, + TimeoutSeconds?: string + } + + export enum StateType { + Pass, + Task, + Choice, + Wait, + Succeed, + Fail, + Parallel + } + + export type State = PassState | TaskState | WaitState | ChoiceState | ParallelState | SucceedState | FailState; + + export interface NextField { + Next: string; + } + + export interface EndField { + End: true; + } + + export type NextOrEndField = NextField | EndField; + + export interface InputOutputPathFields { + InputPath?: string; + OutputPath?: string; + } + + export interface ResultPathField { + ResultPath?: string; + } + + export enum ErrorCode { + ALL = "States.ALL", + Timeout = "States.Timeout", + TaskFailed = "States.TaskFailed", + Permissions = "States.Permissions", + ResultPathMatchFailure = "States.ResultPathMatchFailure", + BranchFailed = "States.BranchFailed", + NoChoiceMatched = "States.NoChoiceMatched" + } + + export interface WithErrors { + ErrorEquals: string[]; + } + + export interface Retrier extends WithErrors { + IntervalSeconds?: number; + MaxAttempts?: number; + BackoffRate?: number; + } + + export interface Catcher extends WithErrors { + Next: string; + ResultPath?: string; + } + + export interface RetryCatchFields { + Retry?: Retrier[]; + Catch?: Catcher[]; + } + + export interface BasePassState extends Commentable, InputOutputPathFields, ResultPathField { + Type: StateType.Pass, + Result?: any; + } + + export type PassState = BasePassState & NextOrEndField; + + export interface BaseTaskState extends Commentable, InputOutputPathFields, ResultPathField, RetryCatchFields { + Type: StateType.Task; + Resource: string; + TimeoutSeconds?: number; + HeartbeatSeconds?: number; + } + + export type TaskState = BaseTaskState & NextOrEndField; + + export interface ChoiceState extends Commentable, InputOutputPathFields { + Type: StateType.Choice; + Choices: Array>; + Default?: string; + } + + export interface BaseWaitState extends Commentable, InputOutputPathFields { + Type: StateType.Wait + } + + export interface WaitSeconds extends BaseWaitState { + Seconds: number + } + + export interface WaitSecondsPath extends BaseWaitState { + SecondsPath: string + } + + export interface WaitTimestamp extends BaseWaitState { + Timestamp: string + } + + export interface WaitTimestampPath extends BaseWaitState { + TimestampPath: string + } + + export type WaitState = (WaitSeconds | WaitSecondsPath | WaitTimestamp | WaitTimestampPath) & NextOrEndField; + + export interface SucceedState extends Commentable, InputOutputPathFields { + Type: StateType.Succeed; + } + + export interface FailState extends Commentable { + Type: StateType.Fail; + Error: string | ErrorCode, + Cause: string + } + + export interface BaseParallelState extends Commentable, InputOutputPathFields, ResultPathField, RetryCatchFields { + Type: StateType.Parallel; + Branches: Branch[] + } + + export type ParallelState = BaseParallelState & NextOrEndField; + + export interface VariableComparisonOperation { + Variable: string; + } + + export interface StringEqualsComparisonOperation extends VariableComparisonOperation { + StringEquals: string; + } + + export interface StringLessThanComparisonOperation extends VariableComparisonOperation { + StringLessThan: string; + } + + export interface StringGreaterThanComparisonOperation extends VariableComparisonOperation { + StringGreaterThan: string; + } + + export interface StringLessThanEqualsComparisonOperation extends VariableComparisonOperation { + StringLessThanEquals: string; + } + + export interface StringGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { + StringGreaterThanEquals: string; + } + + export interface NumericEqualsComparisonOperation extends VariableComparisonOperation { + NumericEquals: number; + } + + export interface NumericLessThanComparisonOperation extends VariableComparisonOperation { + NumericLessThan: number; + } + + export interface NumericGreaterThanComparisonOperation extends VariableComparisonOperation { + NumericGreaterThan: number; + } + + export interface NumericLessThanEqualsComparisonOperation extends VariableComparisonOperation { + NumericLessThanEquals: number; + } + + export interface NumericGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { + NumericGreaterThanEquals: number; + } + + export interface BooleanEqualsComparisonOperation extends VariableComparisonOperation { + BooleanEquals: boolean; + } + + export interface TimestampEqualsComparisonOperation extends VariableComparisonOperation { + TimestampEquals: string; + } + + export interface TimestampLessThanComparisonOperation extends VariableComparisonOperation { + TimestampLessThan: string; + } + + export interface TimestampGreaterThanComparisonOperation extends VariableComparisonOperation { + TimestampGreaterThan: string; + } + + export interface TimestampLessThanEqualsComparisonOperation extends VariableComparisonOperation { + TimestampLessThanEquals: string; + } + + export interface TimestampGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { + TimestampGreaterThanEquals: string; + } + + export interface AndComparisonOperation { + And: ComparisonOperation[] + } + + export interface OrComparisonOperation { + Or: ComparisonOperation[] + } + + export interface NotComparisonOperation { + Not: ComparisonOperation + } + + export type ComparisonOperation = VariableComparisonOperation | AndComparisonOperation | OrComparisonOperation | NotComparisonOperation; + + export type ChoiceRule = NextField & T; + export type StringEqualsChoiceRule = ChoiceRule; + export type StringLessThanChoiceRule = ChoiceRule; + export type StringGreaterThanChoiceRule = ChoiceRule; + export type StringLessThanEqualsChoiceRule = ChoiceRule; + export type StringGreaterThanEqualsChoiceRule = ChoiceRule; + export type NumericEqualsChoiceRule = ChoiceRule; + export type NumericLessThanChoiceRule = ChoiceRule; + export type NumericGreaterThanChoiceRule = ChoiceRule; + export type NumericLessThanEqualsChoiceRule = ChoiceRule; + export type NumericGreaterThanEqualsChoiceRule = ChoiceRule; + export type BooleanEqualsChoiceRule = ChoiceRule; + export type TimestampEqualsChoiceRule = ChoiceRule; + export type TimestampLessThanChoiceRule = ChoiceRule; + export type TimestampGreaterThanChoiceRule = ChoiceRule; + export type TimestampLessThanEqualsChoiceRule = ChoiceRule; + export type TimestampGreaterThanEqualsChoiceRule = ChoiceRule; + export type AndChoiceRule = ChoiceRule; + export type OrChoiceRule = ChoiceRule; + export type NotChoiceRule = ChoiceRule; + // tslint:enable +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions/lib/index.ts index fd4033059ad29..cac43fa1099d2 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/index.ts @@ -1,2 +1,4 @@ +export * from './amazon-states-language'; + // AWS::StepFunctions CloudFormation Resources: -export * from './stepfunctions.generated'; +export * from './stepfunctions.generated'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts.bk b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts.bk new file mode 100644 index 0000000000000..ee08aa1339797 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts.bk @@ -0,0 +1,732 @@ +// import { stepfunctions } from '@aws-cdk/resources'; +// import { Construct, Token, tokenAwareJsonify, Arn, Stack } from '@aws-cdk/core'; +// import { isUndefined } from 'util'; + + +// export interface BranchProps extends Commentable { +// } + +// export interface StateMachineProps extends Commentable { +// roleArn: Arn, +// version?: string, +// timeoutSeconds?: number, +// stateMachineName?: string +// } + +// export interface IStateMachine { +// startAt(state: IState): IStateMachine +// next(state: IState): IStateMachine +// } + +// export class Branch implements Jsonable, IStateMachine { +// private startState?: IState; +// private head?: IState; +// private states: IState[] = []; +// private readonly props: BranchProps; + +// constructor(props: BranchProps = {}) { +// this.props = props; +// } + +// startAt(state: IState) { +// this.startState = this.head = state; +// return this; +// } + +// next(state: IState) { +// if (isUndefined(this.head)) { +// throw new Error("The first state must be added with startAt()"); +// } +// this.head.next = state; +// this.head = state; +// this.states.push(state); +// return this; +// } + +// toJson() { +// return { +// Comment: this.props && this.props.comment, +// States: Array.from(this.states).reduce( +// (map: { [index: string] : IState }, state) => { +// map[state.name] = state; +// return map; +// }, {} +// ), +// StartAt: (this.startState || this.states[0]).name +// } +// } +// } + +// export class StateMachine extends Construct implements IStateMachine { +// private readonly branch: Branch; +// constructor(parent: Construct, name: string, props: StateMachineProps) { +// super(parent, name); +// this.branch = new Branch({comment: props.comment}) + +// new stepfunctions.StateMachineResource(this, 'Resource', { +// definitionString: new Token(() => { +// return tokenAwareJsonify({ +// ...{ +// Version: props && props.version, +// TimeoutSeconds: props && props.timeoutSeconds, +// }, +// ...this.branch.toJson() +// }) +// }), +// roleArn: props.roleArn, +// stateMachineName: props.stateMachineName +// }) +// } + +// validate(): string[] { +// return [] +// } + +// startAt(state: IState) { +// this.branch.startAt(state); +// return this; +// } + +// next(state: IState) { +// this.branch.next(state); +// return this; +// } +// } + +// export enum StateType { +// Pass, +// Task, +// Choice, +// Wait, +// Succeed, +// Fail, +// Parallel +// } + +// export interface IState { +// readonly type: StateType; +// readonly name: string +// readonly comment?: string; + +// next?: IState; + +// toJson(): any +// } + +// export abstract class State implements IState { +// public readonly type: StateType +// public readonly name: string + +// constructor(type: StateType, name: string) { +// this.type = type +// this.name = name +// } + +// public abstract toJson(): any +// } + +// export interface Commentable { +// comment?: string +// } + +// export interface StateProps extends Commentable { +// name: string; +// } + +// export interface NextStateProps { +// next?: State; +// } + +// export interface InputOutputPathStateProps { +// inputPath?: string; +// outputPath?: string; +// } + +// export interface ResultPathStateProps { +// resultPath?: string; +// } + +// export enum ErrorCode { +// ALL = "States.ALL", +// Timeout = "States.Timeout", +// TaskFailed = "States.TaskFailed", +// Permissions = "States.Permissions", +// ResultPathMatchFailure = "States.ResultPathMatchFailure", +// BranchFailed = "States.BranchFailed", +// NoChoiceMatched = "States.NoChoiceMatched" +// } + +// export interface WithErrors { +// errorEquals: (string | ErrorCode)[]; +// } + +// export interface Retrier extends WithErrors { +// intervalSeconds?: number; +// maxAttempts?: number; +// backoffRate?: number; +// } + +// export interface Catcher extends WithErrors { +// next: IState; +// resultPath?: string; +// } + +// export interface RetryCatchStateProps { +// retry?: Retrier[]; +// catch?: Catcher[]; +// } + +// export interface PassStateProps extends StateProps, NextStateProps, InputOutputPathStateProps, ResultPathStateProps { +// result?: any; +// } + +// export class PassState extends State { +// private readonly props: PassStateProps; + +// constructor(props: PassStateProps) { +// super(StateType.Pass, props.name) +// this.props = props; +// } + +// public toJson(): any { +// return { +// ...stateJson(this.props, this.type), +// ...nextJson(this.props), +// ...inputOutputJson(this.props), +// ...resultPathJson(this.props), +// ...{ "Result": this.props.result } +// } +// } +// } + +// function stateJson(props: StateProps, type: StateType): any { +// return { +// "Type": type, +// "Name": props.name, +// "Comment": props.comment +// } +// } + +// function nextJson(props: NextStateProps): any { +// return (!isUndefined(props.next)) ? { "Next": props.next.name } : { "End": true } +// } + +// function inputOutputJson(props: InputOutputPathStateProps): any { +// return { +// "InputPath": props.inputPath, +// "OutputPath": props.outputPath +// } +// } + +// function resultPathJson(props: ResultPathStateProps): any { +// return { +// "ResultPath": props.resultPath +// } +// } + +// function retryJson(props: RetryCatchStateProps): any { +// var out : { [index: string] : any} = {} +// if (props.retry) { +// out["Retry"] = props.retry.map(retrierJson) +// } +// if (props.catch) { +// out["Catch"] = props.catch.map(catcherJson) +// } +// return out; +// } + +// function retrierJson(retrier: Retrier): any { +// return { +// "ErrorEquals": retrier.errorEquals, +// "IntervalSeconds": retrier.intervalSeconds, +// "MaxAttempts": retrier.maxAttempts, +// "BackoffRate": retrier.backoffRate +// } +// } + +// function catcherJson(catcher: Catcher): any { +// return { +// "ErrorEquals": catcher.errorEquals, +// "Next": catcher.next.name, +// "ResultPath": catcher.resultPath +// } +// } + +// export interface TaskStateProps extends StateProps, InputOutputPathStateProps, ResultPathStateProps, NextStateProps, RetryCatchStateProps { +// resource: string; +// timeoutSeconds?: number; +// heartbeatSeconds?: number; +// } + +// export class TaskState extends State { +// private readonly props: TaskStateProps; + +// constructor(props: TaskStateProps) { +// super(StateType.Task, props.name) +// this.props = props; +// } + +// public toJson(): any { +// return { +// ...stateJson(this.props, this.type), +// ...nextJson(this.props), +// ...inputOutputJson(this.props), +// ...resultPathJson(this.props), +// ...retryJson(this.props), +// ...{ +// "Resource": this.props.resource, +// "TimeoutSeconds": this.props.timeoutSeconds, +// "HeartbeatSeconds": this.props.heartbeatSeconds +// } +// } +// } +// } + +// export class ChoiceRules implements Jsonable { +// public readonly choiceRules: ChoiceRule[]; + +// constructor(...choiceRules: ChoiceRule[]) { +// this.choiceRules = choiceRules; +// } + +// toJson() { +// return this.choiceRules.map(choiceRule => choiceRule.toJson()) +// } +// } + +// export interface ChoiceRuleProps { +// comparisonOperation: IComparisonOperation; +// next: IState; +// } + +// export class ChoiceRule { +// public readonly comparisonOperation: IComparisonOperation; +// public readonly next: IState; + +// constructor(props: ChoiceRuleProps) { +// this.comparisonOperation = props.comparisonOperation; +// this.next = props.next; +// } + +// toJson() { +// return { +// ...this.comparisonOperation.toJson(), +// ...{"Next": this.next.name} +// } +// } +// } + +// export interface Jsonable { +// toJson(): any +// } + +// export interface IComparisonOperation extends Jsonable { +// operation: string; +// value: any; +// } + +// export class ComparisonOperations implements Jsonable { +// public readonly comparisons: IComparisonOperation[]; + +// constructor(...comparisons: IComparisonOperation[]) { +// this.comparisons = comparisons +// } + +// toJson() { +// return this.comparisons.map(comparison => comparison.toJson()) +// } +// } + +// export abstract class ComparisonOperation implements IComparisonOperation { +// public readonly operation: string; +// public readonly value: any; +// public readonly variable?: string + +// constructor(operation: string, value: any, variable?: string) { +// this.operation = operation; +// this.value = value +// this.variable = variable +// } + +// public toJson() { +// return { +// [this.operation]: (typeof this.value['toJson'] === 'function') ? this.value.toJson() : this.value, +// "Variable": this.variable +// } +// } +// } + +// export class StringEqualsOperation extends ComparisonOperation { +// constructor(variable: string, value: string) { +// super("StringEquals", value, variable); +// } +// } + +// export class StringLessThanOperation extends ComparisonOperation { +// constructor(variable: string, value: string) { +// super("StringLessThan", value, variable); +// } +// } + +// export class StringGreaterThanOperation extends ComparisonOperation { +// constructor(variable: string, value: string) { +// super("StringGreaterThan", value, variable); +// } +// } + +// export class StringLessThanEqualsOperation extends ComparisonOperation { +// constructor(variable: string, value: string) { +// super("StringLessThanEquals", value, variable); +// } +// } + +// export class StringGreaterThanEqualsOperation extends ComparisonOperation { +// constructor(variable: string, value: string) { +// super("StringGreaterThanEquals", value, variable); +// } +// } + +// export class NumericEqualsOperation extends ComparisonOperation { +// constructor(variable: string, value: number) { +// super("NumericEquals", value, variable); +// } +// } + +// export class NumericLessThanOperation extends ComparisonOperation { +// constructor(variable: string, value: number) { +// super("NumericLessThan", value, variable); +// } +// } + +// export class NumericGreaterThanOperation extends ComparisonOperation { +// constructor(variable: string, value: number) { +// super("NumericGreaterThan", value, variable); +// } +// } + +// export class NumericLessThanEqualsOperation extends ComparisonOperation { +// constructor(variable: string, value: number) { +// super("NumericLessThanEquals", value, variable); +// } +// } + +// export class NumericGreaterThanEqualsOperation extends ComparisonOperation { +// constructor(variable: string, value: number) { +// super("NumericGreaterThanEquals", value, variable); +// } +// } + +// export class BooleanEqualsOperation extends ComparisonOperation { +// constructor(variable: string, value: boolean) { +// super("BooleanEquals", value, variable); +// } +// } + +// export class TimestampEqualsOperation extends ComparisonOperation { +// constructor(variable: string, value: string) { +// super("TimestampEquals", value, variable); +// } +// } + +// export class TimestampLessThanOperation extends ComparisonOperation { +// constructor(variable: string, value: string) { +// super("TimestampLessThan", value, variable); +// } +// } + +// export class TimestampGreaterThanOperation extends ComparisonOperation { +// constructor(variable: string, value: string) { +// super("TimestampGreaterThan", value, variable); +// } +// } + +// export class TimestampLessThanEqualsOperation extends ComparisonOperation { +// constructor(variable: string, value: string) { +// super("TimestampLessThanEquals", value, variable); +// } +// } + +// export class TimestampGreaterThanEqualsOperation extends ComparisonOperation { +// constructor(variable: string, value: string) { +// super("TimestampGreaterThanEquals", value, variable); +// } +// } + +// export class AndOperation extends ComparisonOperation { +// constructor(...comparisons: IComparisonOperation[]) { +// super("And", new ComparisonOperations(...comparisons)); +// } +// } + +// export class OrOperation extends ComparisonOperation { +// constructor(...comparisons: IComparisonOperation[]) { +// super("Or", new ComparisonOperations(...comparisons)); +// } +// } + +// export class NotOperation extends ComparisonOperation { +// constructor(comparison: IComparisonOperation) { +// super("Not", comparison); +// } +// } + +// export interface ChoiceStateProps extends StateProps, InputOutputPathStateProps { +// choices: ChoiceRules; +// default?: State; +// } + +// export class ChoiceState extends State { +// private readonly props: ChoiceStateProps; + +// constructor(props: ChoiceStateProps) { +// super(StateType.Choice, props.name) +// this.props = props; +// } + +// public toJson(): any { +// return { +// ...stateJson(this.props, this.type), +// ...inputOutputJson(this.props), +// ...{ "Choices": this.props.choices.toJson() } +// } +// } +// } + +// export interface WaitStateProps extends StateProps, InputOutputPathStateProps, NextStateProps { +// seconds?: number; +// secondsPath?: string; +// timestamp?: string; +// timestampPath?: string; +// } + +// export class WaitState extends State { +// private readonly props: WaitStateProps; + +// constructor(sm: StateMachine, props: WaitStateProps) { +// super(sm, StateType.Wait, props.name); +// this.props = props; +// } + +// toJson() { +// return { +// ...stateJson(this.props, this.type), +// ...inputOutputJson(this.props), +// ...nextJson(this.props), +// ...{ +// "Seconds": this.props.seconds, +// "SecondsPath": this.props.secondsPath, +// "Timestamp": this.props.timestamp, +// "TimestampPath": this.props.timestampPath +// } +// } +// } +// } + +// export interface SucceedStateProps extends StateProps, InputOutputPathStateProps { +// } + +// export class SucceedState extends State { +// constructor(sm: StateMachine, props: SucceedStateProps) { +// super(sm, StateType.Succeed, props.name); +// } +// } + +// export interface FailStateProps extends StateProps { +// error: string; +// cause: string; +// } + +// export class FailState extends State { +// constructor(props: FailStateProps) { +// super(StateType.Fail, props.name); +// } +// } + +// export interface ParallelStateProps extends StateProps, InputOutputPathStateProps, ResultPathStateProps, NextStateProps, RetryCatchStateProps { +// branches: Branch[] +// } + +// export class ParallelState extends State { +// private readonly props: ParallelStateProps; + +// constructor(props: ParallelStateProps) { +// super(StateType.Parallel, props.name); +// this.props = props; +// } + +// toJson() { +// return ""; +// } +// } + +// // var n = new PassState(); +// // new ChoiceState( +// // [ +// // { +// // comparisonOperation: new NotOperation( +// // { +// // comparisonOperation: new StringEqualsOperation("$.type", "Private") +// // } +// // ), +// // next: new TaskState("arn:aws:lambda:us-east-1:123456789012:function:Foo", {next: n}) +// // }, +// // { +// // comparisonOperation: new AndOperation( +// // [ +// // { +// // comparisonOperation: new NumericGreaterThanEqualsOperation("$.value", 20) +// // }, +// // { +// // comparisonOperation: new NumericLessThanOperation("$.value", 30) +// // } +// // ] +// // ), +// // next: new TaskState("arn:aws:lambda:us-east-1:123456789012:function:Foo", {next: n}) +// // } +// // ], +// // { +// // default: new FailState({ +// // error: "ErrorA", +// // cause: "Kaiju Attack" +// // }) +// // } +// // ) + + +// var stack = new Stack() + +// var sm = new StateMachine(stack, "StateMachine", {roleArn: Arn.fromComponents(Arn.parse("arn::foo"))}) + +// new TaskState(sm, { +// name: "Hello World", +// resource: "arn:aws:lambda:us-east-1:123456789012:function:HelloWorld", +// }) +// new TaskState(sm, { name: "X", +// resource: "arn:aws:swf:us-east-1:123456789012:task:X", +// next: new PassState(sm, {name: "Y"}), +// retry: [ +// { +// errorEquals: [ "ErrorA", "ErrorB" ], +// intervalSeconds: 1, +// backoffRate: 2, +// maxAttempts: 2 +// }, +// { +// errorEquals: [ "ErrorC" ], +// intervalSeconds: 5 +// } +// ], +// catch: [ +// { +// errorEquals: ErrorCode.ALL, +// next: new PassState(sm, { name: "Z" }) +// } +// ] +// }) + +// var sm = new StateMachine(stack, "StateMachine", {roleArn: Arn.fromComponents(Arn.parse("arn::foo"))}) +// let nextstate = new PassState(sm, { name: "NextState" }) +// sm.startAt( +// new ChoiceState(sm, { +// name: "ChoiceStateX", +// choices: new ChoiceRules( +// new ChoiceRule({ +// comparisonOperation: new NotOperation( +// new StringEqualsOperation("$.type", "Private") +// ), +// next: new TaskState({ +// name: "Public", +// resource: "arn:aws:lambda:us-east-1:123456789012:function:Foo", +// next: nextstate +// }) +// }), +// new ChoiceRule({ +// comparisonOperation: new AndOperation( +// new NumericGreaterThanEqualsOperation("$.value", 20), +// new NumericLessThanOperation("$.value", 30) +// ), +// next: new TaskState({ +// name: "ValueInTwenties", +// resource: "arn:aws:lambda:us-east-1:123456789012:function:Bar", +// next: nextstate +// }) +// }) +// ), +// default: new FailState({ +// name: "DefaultState", +// error: "Error", +// cause: "No Matches!" +// }) +// }) +// ) +// // new TaskState({ +// // resource: "some-lambda-arn", +// // next: new PassState({ +// // result: "foo", +// // resultPath: "$.var" +// // }) +// // }) + +// // let lookupAddress = new TaskState("arn:aws:lambda:us-east-1:123456789012:function:AddressFinder") +// // let lookupPhone = new TaskState("arn:aws:lambda:us-east-1:123456789012:function:PhoneFinder") +// // new ParallelState(sm, { +// // name: "LookupCustomerInfo", +// // branches: [ +// // new Branch().startAt(lookupPhone), +// // new Branch().startAt(lookupAddress).addState(lookupPhone) +// // ] +// // }) + +// new StateMachine(stack, "", {roleArn: Arn.fromComponents({resource: "", service: ""})}) +// .startAt( +// new ParallelState({ +// name: "FunWithMath", +// branches: [ +// new Branch().startAt( +// new TaskState({ +// name: "Add", +// resource: "foo" +// }) +// ), +// new Branch().startAt( +// new TaskState({ +// name: "Subtract", +// resource: "bar" +// }) +// ) +// ] +// }) +// ); + +// let iteratorTask = new TaskState({ +// name: "Iterator", +// resource: "foo", +// resultPath: "$.iterator" +// }) + +// new StateMachine(stack, "", {roleArn: Arn.fromComponents({resource: "foo", service:" bar"})}) +// .startAt( +// new PassState({ +// name: "ConfigureCount", +// result: { "count": 10, "index": 0, "step": 1 }, +// resultPath: "$.iterator" +// }) +// ) +// .next(iteratorTask) +// .next( +// new ChoiceState({ +// name: "IsCountReached", +// choices: new ChoiceRules( +// new ChoiceRule({ +// comparisonOperation: new BooleanEqualsOperation("$.iterator.continue", true), +// next: new PassState({ +// name: "ExampleWork", +// comment: "Your application logic, to run a specific number of times", +// result: { "success": true }, +// resultPath: "$.result", +// next: iteratorTask +// }) +// }) +// ) +// }) +// ) \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/package-lock.json b/packages/@aws-cdk/aws-stepfunctions/package-lock.json new file mode 100644 index 0000000000000..1cd38e72da675 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/package-lock.json @@ -0,0 +1,108 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "aws-sdk": { + "version": "2.266.1", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.266.1.tgz", + "integrity": "sha512-b8lisloCETh0Fx0il540i+Hbgf3hyegQ6ezoJFggfc1HIbqzvIjVJYJhOsYl1fL1o+iMUaVU4ZH8cSyoMFR2Tw==", + "requires": { + "buffer": "4.9.1", + "events": "1.1.1", + "ieee754": "1.1.8", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.1.0", + "xml2js": "0.4.17" + } + }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, + "ieee754": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + }, + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + }, + "xml2js": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.17.tgz", + "integrity": "sha1-F76T6q4/O3eTWceVtBlwWogX6Gg=", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "^4.1.0" + } + }, + "xmlbuilder": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.2.1.tgz", + "integrity": "sha1-qlijBBoGb5DqoWwvU4n/GfP0YaU=", + "requires": { + "lodash": "^4.0.0" + } + } + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts new file mode 100644 index 0000000000000..f95c6bdb19b27 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts @@ -0,0 +1,29 @@ +import { Stack } from '@aws-cdk/core'; +import { Test } from 'nodeunit'; + +import { cloudformation, amazon_states_language as asl } from '../lib'; + +export = { + 'Hello World'(test: Test) { + const stack = new Stack(); + + new cloudformation.StateMachineResource(stack, "", { + roleArn: "", + definitionString: JSON.stringify( + { + Comment: "A simple minimal example of the States language", + StartAt: "Hello World", + States: { + "Hello World": { + Type: asl.StateType.Task, + Resource: "arn:aws:lambda:us-east-1:123456789012:function:HelloWorld", + End: true + } + } + } + ) + }) + + test.done(); + } +}; diff --git a/packages/aws-cdk/package-lock.json b/packages/aws-cdk/package-lock.json index 419d3693aec45..39ea89bf30562 100644 --- a/packages/aws-cdk/package-lock.json +++ b/packages/aws-cdk/package-lock.json @@ -1281,15 +1281,6 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", "requires": { - "camelcase": "^4.1.0" - }, - "dependencies": { - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" - } - } }, "zip-stream": { "version": "1.2.0", From d74e4c8272d73310b9c8d3513323a2d6075f9f89 Mon Sep 17 00:00:00 2001 From: Andrew DiLosa Date: Fri, 13 Jul 2018 18:36:40 -0700 Subject: [PATCH 02/29] Change to classes to make jsii happy --- .../lib/amazon-states-language.ts | 593 ++++++++++++------ .../@aws-cdk/aws-stepfunctions/lib/util.ts | 11 + .../test/test.amazon-states-language.ts | 27 +- 3 files changed, 431 insertions(+), 200 deletions(-) create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/util.ts diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/amazon-states-language.ts b/packages/@aws-cdk/aws-stepfunctions/lib/amazon-states-language.ts index d68ac0fa1a3eb..d9a2b58b5b405 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/amazon-states-language.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/amazon-states-language.ts @@ -1,17 +1,77 @@ +import { Token, tokenAwareJsonify } from "@aws-cdk/core"; +import { isArray, isObject } from "util"; +import { requireOneOf } from "./util"; + +// tslint:disable:variable-name export namespace amazon_states_language { - // tslint:disable + function requireNextOrEnd(props: any) { + requireOneOf(props, ['next', 'end']); + } + + // tslint:disable-next-line:no-shadowed-variable + function toPascalCase(x: string) { + return x[0].toUpperCase() + x.substring(1); + } + + export class Jsonable { + protected readonly props: any; + + constructor(props: any) { + this.props = props; + } + + public toJson() { + return this.toJsonRecursive(this.props); + } + + private toJsonRecursive(obj: any) { + const output: { [index: string]: any } = {}; + for (const key of Object.keys(obj)) { + const current = obj[key]; + const pascalKey = toPascalCase(key); + if (current instanceof Jsonable) { + output[pascalKey] = current.toJson(); + } else if (isObject(current) || isArray(current)) { + output[pascalKey] = this.toJsonRecursive(current); + } else { + output[pascalKey] = current; + } + } + return output; + } + } + export interface Commentable { - Comment?: string + comment?: string | Token } export interface Branch extends Commentable { - States: {[index: string]: State}, - StartAt: string, + states: { [name: string]: State }, + startAt: string | Token } - export interface StateMachine extends Branch { - Version?: string, - TimeoutSeconds?: string + export interface StateMachineProps extends Branch { + version?: string | Token, + timeoutSeconds?: number + } + + export class StateMachine extends Jsonable { + constructor(props: StateMachineProps) { + if (!(props.startAt in props.states)) { + throw new Error(`Specified startAt state '${props.startAt}' does not exist in states map`); + } + if (props.timeoutSeconds !== undefined && !Number.isInteger(props.timeoutSeconds)) { + throw new Error("timeoutSeconds must be an integer"); + } + if (Object.keys(props.states).filter(n => n.length > 128).length > 0) { + throw new Error("State names must be less than 128 characters in length"); + } + super(props); + } + + public definitionString() { + return tokenAwareJsonify(this.toJson()); + } } export enum StateType { @@ -24,25 +84,32 @@ export namespace amazon_states_language { Parallel } - export type State = PassState | TaskState | WaitState | ChoiceState | ParallelState | SucceedState | FailState; + export class State extends Jsonable { + constructor(type: StateType, props: any) { + super({ ...props, ...{type}}); + } + } export interface NextField { - Next: string; + next: string; } export interface EndField { - End: true; + end: true; } - export type NextOrEndField = NextField | EndField; + export interface NextOrEndField { + next?: string, + end?: true + } export interface InputOutputPathFields { - InputPath?: string; - OutputPath?: string; + inputPath?: string; + outputPath?: string; } export interface ResultPathField { - ResultPath?: string; + resultPath?: string; } export enum ErrorCode { @@ -56,187 +123,343 @@ export namespace amazon_states_language { } export interface WithErrors { - ErrorEquals: string[]; + errorEquals: string[]; } export interface Retrier extends WithErrors { - IntervalSeconds?: number; - MaxAttempts?: number; - BackoffRate?: number; + intervalseconds?: number; + maxAttempts?: number; + backoffRate?: number; } - export interface Catcher extends WithErrors { - Next: string; - ResultPath?: string; - } + export type Catcher = WithErrors | ResultPathField | NextField; export interface RetryCatchFields { - Retry?: Retrier[]; - Catch?: Catcher[]; - } - - export interface BasePassState extends Commentable, InputOutputPathFields, ResultPathField { - Type: StateType.Pass, - Result?: any; - } - - export type PassState = BasePassState & NextOrEndField; - - export interface BaseTaskState extends Commentable, InputOutputPathFields, ResultPathField, RetryCatchFields { - Type: StateType.Task; - Resource: string; - TimeoutSeconds?: number; - HeartbeatSeconds?: number; - } - - export type TaskState = BaseTaskState & NextOrEndField; - - export interface ChoiceState extends Commentable, InputOutputPathFields { - Type: StateType.Choice; - Choices: Array>; - Default?: string; - } - - export interface BaseWaitState extends Commentable, InputOutputPathFields { - Type: StateType.Wait - } - - export interface WaitSeconds extends BaseWaitState { - Seconds: number - } - - export interface WaitSecondsPath extends BaseWaitState { - SecondsPath: string - } - - export interface WaitTimestamp extends BaseWaitState { - Timestamp: string - } - - export interface WaitTimestampPath extends BaseWaitState { - TimestampPath: string - } - - export type WaitState = (WaitSeconds | WaitSecondsPath | WaitTimestamp | WaitTimestampPath) & NextOrEndField; - - export interface SucceedState extends Commentable, InputOutputPathFields { - Type: StateType.Succeed; - } - - export interface FailState extends Commentable { - Type: StateType.Fail; - Error: string | ErrorCode, - Cause: string - } - - export interface BaseParallelState extends Commentable, InputOutputPathFields, ResultPathField, RetryCatchFields { - Type: StateType.Parallel; - Branches: Branch[] - } - - export type ParallelState = BaseParallelState & NextOrEndField; - - export interface VariableComparisonOperation { - Variable: string; - } - - export interface StringEqualsComparisonOperation extends VariableComparisonOperation { - StringEquals: string; - } - - export interface StringLessThanComparisonOperation extends VariableComparisonOperation { - StringLessThan: string; - } - - export interface StringGreaterThanComparisonOperation extends VariableComparisonOperation { - StringGreaterThan: string; - } - - export interface StringLessThanEqualsComparisonOperation extends VariableComparisonOperation { - StringLessThanEquals: string; - } - - export interface StringGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { - StringGreaterThanEquals: string; - } - - export interface NumericEqualsComparisonOperation extends VariableComparisonOperation { - NumericEquals: number; - } - - export interface NumericLessThanComparisonOperation extends VariableComparisonOperation { - NumericLessThan: number; - } - - export interface NumericGreaterThanComparisonOperation extends VariableComparisonOperation { - NumericGreaterThan: number; + retry?: Retrier[]; + catch?: Catcher[]; } - export interface NumericLessThanEqualsComparisonOperation extends VariableComparisonOperation { - NumericLessThanEquals: number; + export interface PassStateProps extends Commentable, InputOutputPathFields, ResultPathField, NextOrEndField { + result?: any; } - export interface NumericGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { - NumericGreaterThanEquals: number; + export class PassState extends State { + constructor(props: PassStateProps) { + requireNextOrEnd(props); + super(StateType.Pass, props); + } + } + + export interface TaskStateProps extends Commentable, InputOutputPathFields, ResultPathField, RetryCatchFields, NextOrEndField { + resource: string; + timeoutSeconds?: number; + heartbeatSeconds?: number; + } + + export class TaskState extends State { + constructor(props: TaskStateProps) { + if (props.timeoutSeconds !== undefined && !Number.isInteger(props.timeoutSeconds)) { + throw new Error(`timeoutSeconds must be an integer, not '${props.timeoutSeconds}'`); + } + if (props.heartbeatSeconds !== undefined && !Number.isInteger(props.heartbeatSeconds)) { + throw new Error(`heartbeatSeconds must be an integer, not '${props.heartbeatSeconds}'`); + } + if (props.timeoutSeconds !== undefined && props.heartbeatSeconds !== undefined && props.heartbeatSeconds >= props.timeoutSeconds) { + throw new Error("heartbeatSeconds must be smaller than timeoutSeconds"); + } + requireNextOrEnd(props); + super(StateType.Task, props); + } + } + + export interface WaitStateProps extends Commentable, InputOutputPathFields { + seconds?: number + secondsPath?: string, + timestamp?: string, + timestampPath?: string + } + + export class WaitState extends State { + constructor(props: WaitStateProps) { + requireOneOf(props, ['seconds', 'secondsPath', 'timestamp', 'timestampPath']); + super(StateType.Wait, props); + } + } + + export interface SucceedStateProps extends Commentable, InputOutputPathFields { + } + + export class SucceedState extends State { + constructor(props: SucceedStateProps) { + super(StateType.Succeed, props); + } + } + + export interface FailStateProps extends State, Commentable { + error: string | ErrorCode, + cause: string + } + + export class FailState extends State { + constructor(props: FailStateProps) { + super(StateType.Fail, props); + } + } + + // export class ChoiceRules extends Jsonable { + // private readonly choices: ChoiceRule[]; + + // constructor(...choices: ChoiceRule[]) { + // super(null); + // this.choices = choices; + // } + + // public toJson() { + // return this.choices; + // // return this.choices.map(choiceRule => choiceRule.toJson()); + // } + // } + + // export interface ChoiceStateProps extends Commentable, InputOutputPathFields { + // choices: ChoiceRules; + // default?: string; + // } + + // export class ChoiceState extends State { + // constructor(props: ChoiceStateProps) { + // super(StateType.Choice, props); + // } + // } + + // export enum ComparisonOperator { + // StringEquals, + // StringLessThan, + // StringGreaterThan, + // StringLessThanEquals, + // StringGreaterThanEquals, + // NumericEquals, + // NumericLessThan, + // NumericGreaterThan, + // NumericLessThanEquals, + // NumericGreaterThanEquals, + // BooleanEquals, + // TimestampEquals, + // TimestampLessThan, + // TimestampGreaterThan, + // TimestampLessThanEquals, + // TimestampGreaterThanEquals, + // And, + // Or, + // Not + // } + + // export interface ComparisonOperationProps { + // comparisonOperator: ComparisonOperator, + // value: string | number | boolean | ComparisonOperator[] + // } + + // export abstract class ComparisonOperation extends Jsonable { + + // } + + // export interface BaseVariableComparisonOperationProps { + // comparisonOperator: ComparisonOperator, + // value: string | number | boolean, + // variable: string + // } + + // export interface VariableComparisonOperationProps { + // value: T, + // variable: string + // } + + // export abstract class VariableComparisonOperation extends ComparisonOperation { + // protected readonly props: BaseVariableComparisonOperationProps; + + // constructor(props: BaseVariableComparisonOperationProps) { + // super(null); + // this.props = props; + // } + + // public toJson(): any { + // return { + // [this.props.comparisonOperator]: this.props.value, + // variable: this.props.variable + // }; + // } + // } + + // export class StringEqualsComparisonOperation extends VariableComparisonOperation { + // constructor(props: VariableComparisonOperationProps) { + // super({...props, ...{comparisonOperator: ComparisonOperator.StringEquals}}); + // } + // } + + // export class StringLessThanComparisonOperation extends VariableComparisonOperation { + // constructor(props: VariableComparisonOperationProps) { + // super({...props, ...{comparisonOperator: ComparisonOperator.StringLessThan}}); + // } + // } + + // export class StringGreaterThanComparisonOperation extends VariableComparisonOperation { + // constructor(props: VariableComparisonOperationProps) { + // super({...props, ...{comparisonOperator: ComparisonOperator.StringGreaterThan}}); + // } + // } + + // export class StringLessThanEqualsComparisonOperation extends VariableComparisonOperation { + // constructor(props: VariableComparisonOperationProps) { + // super({...props, ...{comparisonOperator: ComparisonOperator.StringLessThanEquals}}); + // } + // } + + // export class StringGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { + // constructor(props: VariableComparisonOperationProps) { + // super({...props, ...{comparisonOperator: ComparisonOperator.StringGreaterThanEquals}}); + // } + // } + + // export class NumericEqualsComparisonOperation extends VariableComparisonOperation { + // constructor(props: VariableComparisonOperationProps) { + // super({...props, ...{comparisonOperator: ComparisonOperator.NumericEquals}}); + // } + // } + + // export class NumericLessThanComparisonOperation extends VariableComparisonOperation { + // constructor(props: VariableComparisonOperationProps) { + // super({...props, ...{comparisonOperator: ComparisonOperator.NumericLessThan}}); + // } + // } + + // export class NumericGreaterThanComparisonOperation extends VariableComparisonOperation { + // constructor(props: VariableComparisonOperationProps) { + // super({...props, ...{comparisonOperator: ComparisonOperator.NumericGreaterThan}}); + // } + // } + + // export class NumericLessThanEqualsComparisonOperation extends VariableComparisonOperation { + // constructor(props: VariableComparisonOperationProps) { + // super({...props, ...{comparisonOperator: ComparisonOperator.NumericLessThanEquals}}); + // } + // } + + // export class NumericGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { + // constructor(props: VariableComparisonOperationProps) { + // super({...props, ...{comparisonOperator: ComparisonOperator.NumericGreaterThanEquals}}); + // } + // } + + // export class BooleanEqualsComparisonOperation extends VariableComparisonOperation { + // constructor(props: VariableComparisonOperationProps) { + // super({...props, ...{comparisonOperator: ComparisonOperator.BooleanEquals}}); + // } + // } + + // export class TimestampEqualsComparisonOperation extends VariableComparisonOperation { + // constructor(props: VariableComparisonOperationProps) { + // super({...props, ...{comparisonOperator: ComparisonOperator.TimestampEquals}}); + // } + // } + + // export class TimestampLessThanComparisonOperation extends VariableComparisonOperation { + // constructor(props: VariableComparisonOperationProps) { + // super({...props, ...{comparisonOperator: ComparisonOperator.TimestampLessThan}}); + // } + // } + + // export class TimestampGreaterThanComparisonOperation extends VariableComparisonOperation { + // constructor(props: VariableComparisonOperationProps) { + // super({...props, ...{comparisonOperator: ComparisonOperator.TimestampGreaterThan}}); + // } + // } + + // export class TimestampLessThanEqualsComparisonOperation extends VariableComparisonOperation { + // constructor(props: VariableComparisonOperationProps) { + // super({...props, ...{comparisonOperator: ComparisonOperator.TimestampLessThanEquals}}); + // } + // } + + // export class TimestampGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { + // constructor(props: VariableComparisonOperationProps) { + // super({...props, ...{comparisonOperator: ComparisonOperator.TimestampGreaterThanEquals}}); + // } + // } + + // export interface ArrayComparisonOperationProps { + // comparisonOperator: ComparisonOperator, + // comparisonOperations: ComparisonOperation[] + // } + + // // export abstract class ArrayComparisonOperation extends ComparisonOperation { + // // protected readonly props: ArrayComparisonOperationProps; + + // // constructor(props: ArrayComparisonOperationProps) { + // // super(null); + // // this.props = props; + // // } + + // // public toJson() { + // // return { + // // // [this.props.comparisonOperator]: this.props.comparisonOperations.map(comparisonOperation => comparisonOperation.toJson()) + // // }; + // // } + // // } + + // // export class AndComparisonOperation extends ArrayComparisonOperation { + // // constructor(...comparisonOperations: ComparisonOperation[]) { + // // super({ comparisonOperator: ComparisonOperator.And, comparisonOperations}); + // // } + // // } + + // // export class OrComparisonOperation extends ArrayComparisonOperation { + // // constructor(...comparisonOperations: ComparisonOperation[]) { + // // super({ comparisonOperator: ComparisonOperator.Or, comparisonOperations}); + // // } + // // } + + // // export class NotComparisonOperation extends ComparisonOperation { + // // protected readonly comparisonOperation: ComparisonOperation; + + // // constructor(comparisonOperation: ComparisonOperation) { + // // super(null); + // // this.comparisonOperation = comparisonOperation; + // // } + + // // public toJson() { + // // return { + // // [ComparisonOperator.Not]: this.comparisonOperation.toJson() + // // }; + // // } + // // } + + // export interface ChoiceRuleProps extends NextField { + // comparisonOperation: ComparisonOperation; + // } + + // export class ChoiceRule extends Jsonable { + // protected readonly props: ChoiceRuleProps; + + // constructor(props: ChoiceRuleProps) { + // super(null); + // this.props = props; + // } + + // public toJson() { + // return { + // // ...this.props.comparisonOperation.toJson(), + // // ...{next: this.props.next} + // }; + // } + // } + + export interface ParallelStateProps extends Commentable, InputOutputPathFields, ResultPathField, RetryCatchFields, NextOrEndField { + branches: Branch[] + } + + export class ParallelState extends State { + constructor(props: ParallelStateProps) { + requireNextOrEnd(props); + super(StateType.Parallel, props); + } } - - export interface BooleanEqualsComparisonOperation extends VariableComparisonOperation { - BooleanEquals: boolean; - } - - export interface TimestampEqualsComparisonOperation extends VariableComparisonOperation { - TimestampEquals: string; - } - - export interface TimestampLessThanComparisonOperation extends VariableComparisonOperation { - TimestampLessThan: string; - } - - export interface TimestampGreaterThanComparisonOperation extends VariableComparisonOperation { - TimestampGreaterThan: string; - } - - export interface TimestampLessThanEqualsComparisonOperation extends VariableComparisonOperation { - TimestampLessThanEquals: string; - } - - export interface TimestampGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { - TimestampGreaterThanEquals: string; - } - - export interface AndComparisonOperation { - And: ComparisonOperation[] - } - - export interface OrComparisonOperation { - Or: ComparisonOperation[] - } - - export interface NotComparisonOperation { - Not: ComparisonOperation - } - - export type ComparisonOperation = VariableComparisonOperation | AndComparisonOperation | OrComparisonOperation | NotComparisonOperation; - - export type ChoiceRule = NextField & T; - export type StringEqualsChoiceRule = ChoiceRule; - export type StringLessThanChoiceRule = ChoiceRule; - export type StringGreaterThanChoiceRule = ChoiceRule; - export type StringLessThanEqualsChoiceRule = ChoiceRule; - export type StringGreaterThanEqualsChoiceRule = ChoiceRule; - export type NumericEqualsChoiceRule = ChoiceRule; - export type NumericLessThanChoiceRule = ChoiceRule; - export type NumericGreaterThanChoiceRule = ChoiceRule; - export type NumericLessThanEqualsChoiceRule = ChoiceRule; - export type NumericGreaterThanEqualsChoiceRule = ChoiceRule; - export type BooleanEqualsChoiceRule = ChoiceRule; - export type TimestampEqualsChoiceRule = ChoiceRule; - export type TimestampLessThanChoiceRule = ChoiceRule; - export type TimestampGreaterThanChoiceRule = ChoiceRule; - export type TimestampLessThanEqualsChoiceRule = ChoiceRule; - export type TimestampGreaterThanEqualsChoiceRule = ChoiceRule; - export type AndChoiceRule = ChoiceRule; - export type OrChoiceRule = ChoiceRule; - export type NotChoiceRule = ChoiceRule; - // tslint:enable } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/util.ts b/packages/@aws-cdk/aws-stepfunctions/lib/util.ts new file mode 100644 index 0000000000000..b42d9865fd7d2 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/util.ts @@ -0,0 +1,11 @@ +export function requireOneOf(props: { [name: string]: any }, names: string[]) { + if (names.map(name => name in props).filter(x => x === true).length !== 1) { + throw new Error(`${props} must specify exactly one of: ${names}`); + } +} + +export function requireAll(props: { [name: string]: any }, names: string[]) { + if (names.map(name => name in props).filter(x => x === false).length > 0) { + throw new Error(`${props} must specify exactly all of: ${names}`); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts index f95c6bdb19b27..40d8a7710c8da 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts @@ -1,28 +1,25 @@ import { Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; -import { cloudformation, amazon_states_language as asl } from '../lib'; +import { amazon_states_language as asl, cloudformation } from '../lib'; export = { 'Hello World'(test: Test) { const stack = new Stack(); - new cloudformation.StateMachineResource(stack, "", { + new cloudformation.StateMachineResource(stack, "StateMachine", { roleArn: "", - definitionString: JSON.stringify( - { - Comment: "A simple minimal example of the States language", - StartAt: "Hello World", - States: { - "Hello World": { - Type: asl.StateType.Task, - Resource: "arn:aws:lambda:us-east-1:123456789012:function:HelloWorld", - End: true - } - } + definitionString: new asl.StateMachine({ + comment: "A simple minimal example of the States language", + startAt: "Hello World", + states: { + "Hello World": new asl.TaskState({ + resource: "arn:aws:lambda:us-east-1:123456789012:function:HelloWorld", + end: true + }) } - ) - }) + }).definitionString() + }); test.done(); } From 61f310b779f1551f57c6487274dfe93642fe7786 Mon Sep 17 00:00:00 2001 From: Andrew DiLosa Date: Mon, 16 Jul 2018 01:14:05 -0700 Subject: [PATCH 03/29] Added ChoiceState, validations, and basic serialization tests --- .../lib/amazon-states-language.ts | 735 ++++++++++-------- .../@aws-cdk/aws-stepfunctions/lib/util.ts | 6 + .../test/test.amazon-states-language.ts | 208 ++++- 3 files changed, 619 insertions(+), 330 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/amazon-states-language.ts b/packages/@aws-cdk/aws-stepfunctions/lib/amazon-states-language.ts index d9a2b58b5b405..4e8c4d38c3961 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/amazon-states-language.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/amazon-states-language.ts @@ -1,92 +1,113 @@ import { Token, tokenAwareJsonify } from "@aws-cdk/core"; -import { isArray, isObject } from "util"; +import { isString } from "util"; import { requireOneOf } from "./util"; -// tslint:disable:variable-name export namespace amazon_states_language { function requireNextOrEnd(props: any) { requireOneOf(props, ['next', 'end']); } - // tslint:disable-next-line:no-shadowed-variable - function toPascalCase(x: string) { - return x[0].toUpperCase() + x.substring(1); - } - export class Jsonable { - protected readonly props: any; + public readonly props: any; constructor(props: any) { this.props = props; } - public toJson() { - return this.toJsonRecursive(this.props); + public toJSON() { + return this.toPascalCase(this.props); } - private toJsonRecursive(obj: any) { - const output: { [index: string]: any } = {}; - for (const key of Object.keys(obj)) { - const current = obj[key]; - const pascalKey = toPascalCase(key); - if (current instanceof Jsonable) { - output[pascalKey] = current.toJson(); - } else if (isObject(current) || isArray(current)) { - output[pascalKey] = this.toJsonRecursive(current); - } else { - output[pascalKey] = current; + private toPascalCase(o: any) { + const out: { [index: string]: any } = {}; + for (const k in o) { + if (o.hasOwnProperty(k)) { + out[k[0].toUpperCase() + k.substring(1)] = o[k]; } } - return output; + return out; } } - export interface Commentable { - comment?: string | Token - } - - export interface Branch extends Commentable { - states: { [name: string]: State }, - startAt: string | Token - } - - export interface StateMachineProps extends Branch { + export interface StateMachineProps extends BranchProps { version?: string | Token, timeoutSeconds?: number } export class StateMachine extends Jsonable { constructor(props: StateMachineProps) { - if (!(props.startAt in props.states)) { - throw new Error(`Specified startAt state '${props.startAt}' does not exist in states map`); - } if (props.timeoutSeconds !== undefined && !Number.isInteger(props.timeoutSeconds)) { throw new Error("timeoutSeconds must be an integer"); } - if (Object.keys(props.states).filter(n => n.length > 128).length > 0) { - throw new Error("State names must be less than 128 characters in length"); + const allStates = props.states.stateNames(); + if (new Set(allStates).size !== allStates.length) { + throw new Error('State names are not unique within the whole state machine'); } super(props); } public definitionString() { - return tokenAwareJsonify(this.toJson()); + return tokenAwareJsonify(this.toJSON()); } } - export enum StateType { - Pass, - Task, - Choice, - Wait, - Succeed, - Fail, - Parallel + export interface Commentable { + comment?: string | Token } - export class State extends Jsonable { - constructor(type: StateType, props: any) { - super({ ...props, ...{type}}); + export interface BranchProps extends Commentable { + states: States, + startAt: string | Token + } + + export class Branch extends Jsonable { + constructor(props: BranchProps) { + if (isString(props.startAt) && !props.states.hasState(props.startAt)) { + throw new Error(`Specified startAt state '${props.startAt}' does not exist in states map`); + } + super(props); + } + + public stateNames(): string[] { + return this.props.states.stateNames(); + } + } + + export class States extends Jsonable { + constructor(states: { [name: string]: State }) { + const longNames = Object.keys(states).filter(n => n.length > 128); + if (longNames.length > 0) { + throw new Error(`State names ${JSON.stringify(longNames)} exceed 128 characters in length`); + } + Object.keys(states).forEach(stateName => { + const state = states[stateName]; + const next = state.next(); + if (!state.isTerminal() && next.length === 0 && !(state instanceof ChoiceState)) { + throw new Error(`Non-terminal and non-ChoiceState state '${state}' does not have a 'next' field`); + } + next.forEach(referencedState => { + if (!(referencedState in states)) { + throw new Error(`State '${stateName}' references unknown Next state '${referencedState}'`); + } + }); + }); + super(states); + } + + public toJSON() { + return this.props; + } + + public hasState(name: string) { + return this.props.hasOwnProperty(name); + } + + public stateNames(): string[] { + const names = Object.keys(this.props); + Object.values(this.props).map( + state => (state instanceof ParallelState) ? state.stateNames() : [] + ).forEach(branchNames => branchNames.forEach(name => names.push(name))); + return names; } } @@ -122,28 +143,124 @@ export namespace amazon_states_language { NoChoiceMatched = "States.NoChoiceMatched" } - export interface WithErrors { + export interface ErrorEquals { errorEquals: string[]; } - export interface Retrier extends WithErrors { - intervalseconds?: number; + export interface RetrierProps extends ErrorEquals { + intervalSeconds?: number; maxAttempts?: number; backoffRate?: number; } - export type Catcher = WithErrors | ResultPathField | NextField; + function isErrorName(error: string) { + return Object.values(ErrorCode).includes(error) || !error.startsWith("States."); + } + + function validateErrorEquals(errors: string[]) { + if (errors.length === 0) { + throw new Error('ErrorEquals is empty. Must be a non-empty array of Error Names'); + } + if (errors.length > 1 && errors.includes(ErrorCode.ALL)) { + throw new Error(`Error name '${ErrorCode.ALL}' is specified along with other Error Names. '${ErrorCode.ALL}' must appear alone.`); + } + errors.forEach(error => { + if (!isErrorName(error)) { + throw new Error(`'${error}' is not a valid Error Name`); + } + }); + } + + export class Retrier extends Jsonable { + constructor(props: RetrierProps) { + validateErrorEquals(props.errorEquals); + if (props.intervalSeconds && (!Number.isInteger(props.intervalSeconds) || props.intervalSeconds < 1)) { + throw new Error(`intervalSeconds '${props.intervalSeconds}' is not a positive integer`); + } + if (props.maxAttempts && (!Number.isInteger(props.maxAttempts) || props.maxAttempts < 0)) { + throw new Error(`maxAttempts '${props.maxAttempts}' is not a non-negative integer`); + } + if (props.backoffRate && props.backoffRate < 1.0) { + throw new Error(`backoffRate '${props.backoffRate}' is not >= 1.0`); + } + super(props); + } + } + + export interface CatcherProps extends ErrorEquals, ResultPathField, NextField { + + } + + export class Catcher extends Jsonable { + constructor(props: CatcherProps) { + validateErrorEquals(props.errorEquals); + super(props); + } + } export interface RetryCatchFields { - retry?: Retrier[]; - catch?: Catcher[]; + retry?: Retriers; + catch?: Catchers; + } + + function validateErrorAllAppearsLast(props: ErrorEquals[]) { + props.slice(0, -1).forEach(prop => { + if (prop.errorEquals.includes(ErrorCode.ALL)) { + throw new Error( + `Error code '${ErrorCode.ALL}' found before last error handler. '${ErrorCode.ALL}' must appear in the last error handler.` + ); + } + }); + } + + export class Retriers extends Jsonable { + constructor(retriers: Retrier[]) { + validateErrorAllAppearsLast(retriers.map(retrier => retrier.props)); + super(retriers); + } + } + + export class Catchers extends Jsonable { + constructor(catchers: Catcher[]) { + validateErrorAllAppearsLast(catchers.map(catcher => catcher.props)); + super(catchers); + } + } + + export enum StateType { + Pass, + Task, + Choice, + Wait, + Succeed, + Fail, + Parallel + } + + export interface State { + isTerminal(): boolean + next(): string[] + } + + export abstract class BaseState extends Jsonable implements State { + constructor(type: StateType, props: any) { + super({ ...props, ...{type: StateType[type]}}); + } + + public isTerminal() { + return this.props.hasOwnProperty('end') && this.props.end === true; + } + + public next() { + return (this.props.next) ? [this.props.next] : []; + } } export interface PassStateProps extends Commentable, InputOutputPathFields, ResultPathField, NextOrEndField { result?: any; } - export class PassState extends State { + export class PassState extends BaseState { constructor(props: PassStateProps) { requireNextOrEnd(props); super(StateType.Pass, props); @@ -156,32 +273,254 @@ export namespace amazon_states_language { heartbeatSeconds?: number; } - export class TaskState extends State { + export class TaskState extends BaseState { constructor(props: TaskStateProps) { if (props.timeoutSeconds !== undefined && !Number.isInteger(props.timeoutSeconds)) { - throw new Error(`timeoutSeconds must be an integer, not '${props.timeoutSeconds}'`); + throw new Error(`timeoutSeconds '${props.timeoutSeconds}' is not an integer`); } if (props.heartbeatSeconds !== undefined && !Number.isInteger(props.heartbeatSeconds)) { - throw new Error(`heartbeatSeconds must be an integer, not '${props.heartbeatSeconds}'`); + throw new Error(`heartbeatSeconds '${props.heartbeatSeconds}' is not an integer`); } if (props.timeoutSeconds !== undefined && props.heartbeatSeconds !== undefined && props.heartbeatSeconds >= props.timeoutSeconds) { - throw new Error("heartbeatSeconds must be smaller than timeoutSeconds"); + throw new Error("heartbeatSeconds is larger than timeoutSeconds"); } requireNextOrEnd(props); super(StateType.Task, props); } } - export interface WaitStateProps extends Commentable, InputOutputPathFields { + export enum ComparisonOperator { + StringEquals, + StringLessThan, + StringGreaterThan, + StringLessThanEquals, + StringGreaterThanEquals, + NumericEquals, + NumericLessThan, + NumericGreaterThan, + NumericLessThanEquals, + NumericGreaterThanEquals, + BooleanEquals, + TimestampEquals, + TimestampLessThan, + TimestampGreaterThan, + TimestampLessThanEquals, + TimestampGreaterThanEquals, + And, + Or, + Not + } + + export abstract class ComparisonOperation extends Jsonable { + } + + export interface BaseVariableComparisonOperationProps { + comparisonOperator: ComparisonOperator, + value: any, + variable: string + } + + export interface VariableComparisonOperationProps { + value: T, + variable: string + } + + export abstract class VariableComparisonOperation extends ComparisonOperation { + constructor(props: BaseVariableComparisonOperationProps) { + super(props); + } + + public toJSON(): any { + return { + Variable: this.props.variable, + [ComparisonOperator[this.props.comparisonOperator]]: this.props.value + }; + } + } + + export class StringEqualsComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({...props, ...{comparisonOperator: ComparisonOperator.StringEquals}}); + } + } + + export class StringLessThanComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({...props, ...{comparisonOperator: ComparisonOperator.StringLessThan}}); + } + } + + export class StringGreaterThanComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({...props, ...{comparisonOperator: ComparisonOperator.StringGreaterThan}}); + } + } + + export class StringLessThanEqualsComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({...props, ...{comparisonOperator: ComparisonOperator.StringLessThanEquals}}); + } + } + + export class StringGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({...props, ...{comparisonOperator: ComparisonOperator.StringGreaterThanEquals}}); + } + } + + export class NumericEqualsComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({...props, ...{comparisonOperator: ComparisonOperator.NumericEquals}}); + } + } + + export class NumericLessThanComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({...props, ...{comparisonOperator: ComparisonOperator.NumericLessThan}}); + } + } + + export class NumericGreaterThanComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({...props, ...{comparisonOperator: ComparisonOperator.NumericGreaterThan}}); + } + } + + export class NumericLessThanEqualsComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({...props, ...{comparisonOperator: ComparisonOperator.NumericLessThanEquals}}); + } + } + + export class NumericGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({...props, ...{comparisonOperator: ComparisonOperator.NumericGreaterThanEquals}}); + } + } + + export class BooleanEqualsComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({...props, ...{comparisonOperator: ComparisonOperator.BooleanEquals}}); + } + } + + export class TimestampEqualsComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({...props, ...{comparisonOperator: ComparisonOperator.TimestampEquals}}); + } + } + + export class TimestampLessThanComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({...props, ...{comparisonOperator: ComparisonOperator.TimestampLessThan}}); + } + } + + export class TimestampGreaterThanComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({...props, ...{comparisonOperator: ComparisonOperator.TimestampGreaterThan}}); + } + } + + export class TimestampLessThanEqualsComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({...props, ...{comparisonOperator: ComparisonOperator.TimestampLessThanEquals}}); + } + } + + export class TimestampGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({...props, ...{comparisonOperator: ComparisonOperator.TimestampGreaterThanEquals}}); + } + } + + export interface ArrayComparisonOperationProps { + comparisonOperator: ComparisonOperator, + comparisonOperations: ComparisonOperation[] + } + + export abstract class ArrayComparisonOperation extends ComparisonOperation { + constructor(props: ArrayComparisonOperationProps) { + if (props.comparisonOperations.length === 0) { + throw new Error('\'comparisonOperations\' is empty. Must be non-empty array of ChoiceRules'); + } + super({ + [ComparisonOperator[props.comparisonOperator]]: props.comparisonOperations + }); + } + } + + export class AndComparisonOperation extends ArrayComparisonOperation { + constructor(...comparisonOperations: ComparisonOperation[]) { + super({ comparisonOperator: ComparisonOperator.And, comparisonOperations}); + } + } + + export class OrComparisonOperation extends ArrayComparisonOperation { + constructor(...comparisonOperations: ComparisonOperation[]) { + super({ comparisonOperator: ComparisonOperator.Or, comparisonOperations}); + } + } + + export class NotComparisonOperation extends ComparisonOperation { + constructor(comparisonOperation: ComparisonOperation) { + super({ [ComparisonOperator[ComparisonOperator.Not]]: comparisonOperation}); + } + } + + export interface ChoiceRuleProps extends NextField { + comparisonOperation: ComparisonOperation; + } + + export class ChoiceRule extends Jsonable { + constructor(props: ChoiceRuleProps) { + super({...props.comparisonOperation.props, next: props.next}); + } + } + + export class ChoiceRules extends Jsonable { + constructor(...choices: ChoiceRule[]) { + super(choices); + } + + public get length(): number { + return this.props.length; + } + + public get nextStates(): string[] { + return this.props.map((choiceRule: ChoiceRule) => choiceRule.props.next); + } + } + + export interface ChoiceStateProps extends Commentable, InputOutputPathFields { + choices: ChoiceRules; + default?: string; + } + + export class ChoiceState extends BaseState { + constructor(props: ChoiceStateProps) { + if (props.choices.length === 0) { + throw new Error('\'choices\' is empty. Must be non-empty array of ChoiceRules'); + } + super(StateType.Choice, props); + } + + public next() { + return this.props.choices.nextStates().concat([this.props.default]); + } + } + + export interface WaitStateProps extends Commentable, InputOutputPathFields, NextOrEndField { seconds?: number secondsPath?: string, timestamp?: string, timestampPath?: string } - export class WaitState extends State { + export class WaitState extends BaseState { constructor(props: WaitStateProps) { requireOneOf(props, ['seconds', 'secondsPath', 'timestamp', 'timestampPath']); + requireNextOrEnd(props); super(StateType.Wait, props); } } @@ -189,277 +528,45 @@ export namespace amazon_states_language { export interface SucceedStateProps extends Commentable, InputOutputPathFields { } - export class SucceedState extends State { - constructor(props: SucceedStateProps) { + export class SucceedState extends BaseState { + constructor(props: SucceedStateProps = {}) { super(StateType.Succeed, props); } + + public isTerminal() { + return true; + } } - export interface FailStateProps extends State, Commentable { - error: string | ErrorCode, + export interface FailStateProps extends BaseState, Commentable { + error: string, cause: string } - export class FailState extends State { + export class FailState extends BaseState { constructor(props: FailStateProps) { super(StateType.Fail, props); } - } - // export class ChoiceRules extends Jsonable { - // private readonly choices: ChoiceRule[]; - - // constructor(...choices: ChoiceRule[]) { - // super(null); - // this.choices = choices; - // } - - // public toJson() { - // return this.choices; - // // return this.choices.map(choiceRule => choiceRule.toJson()); - // } - // } - - // export interface ChoiceStateProps extends Commentable, InputOutputPathFields { - // choices: ChoiceRules; - // default?: string; - // } - - // export class ChoiceState extends State { - // constructor(props: ChoiceStateProps) { - // super(StateType.Choice, props); - // } - // } - - // export enum ComparisonOperator { - // StringEquals, - // StringLessThan, - // StringGreaterThan, - // StringLessThanEquals, - // StringGreaterThanEquals, - // NumericEquals, - // NumericLessThan, - // NumericGreaterThan, - // NumericLessThanEquals, - // NumericGreaterThanEquals, - // BooleanEquals, - // TimestampEquals, - // TimestampLessThan, - // TimestampGreaterThan, - // TimestampLessThanEquals, - // TimestampGreaterThanEquals, - // And, - // Or, - // Not - // } - - // export interface ComparisonOperationProps { - // comparisonOperator: ComparisonOperator, - // value: string | number | boolean | ComparisonOperator[] - // } - - // export abstract class ComparisonOperation extends Jsonable { - - // } - - // export interface BaseVariableComparisonOperationProps { - // comparisonOperator: ComparisonOperator, - // value: string | number | boolean, - // variable: string - // } - - // export interface VariableComparisonOperationProps { - // value: T, - // variable: string - // } - - // export abstract class VariableComparisonOperation extends ComparisonOperation { - // protected readonly props: BaseVariableComparisonOperationProps; - - // constructor(props: BaseVariableComparisonOperationProps) { - // super(null); - // this.props = props; - // } - - // public toJson(): any { - // return { - // [this.props.comparisonOperator]: this.props.value, - // variable: this.props.variable - // }; - // } - // } - - // export class StringEqualsComparisonOperation extends VariableComparisonOperation { - // constructor(props: VariableComparisonOperationProps) { - // super({...props, ...{comparisonOperator: ComparisonOperator.StringEquals}}); - // } - // } - - // export class StringLessThanComparisonOperation extends VariableComparisonOperation { - // constructor(props: VariableComparisonOperationProps) { - // super({...props, ...{comparisonOperator: ComparisonOperator.StringLessThan}}); - // } - // } - - // export class StringGreaterThanComparisonOperation extends VariableComparisonOperation { - // constructor(props: VariableComparisonOperationProps) { - // super({...props, ...{comparisonOperator: ComparisonOperator.StringGreaterThan}}); - // } - // } - - // export class StringLessThanEqualsComparisonOperation extends VariableComparisonOperation { - // constructor(props: VariableComparisonOperationProps) { - // super({...props, ...{comparisonOperator: ComparisonOperator.StringLessThanEquals}}); - // } - // } - - // export class StringGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { - // constructor(props: VariableComparisonOperationProps) { - // super({...props, ...{comparisonOperator: ComparisonOperator.StringGreaterThanEquals}}); - // } - // } - - // export class NumericEqualsComparisonOperation extends VariableComparisonOperation { - // constructor(props: VariableComparisonOperationProps) { - // super({...props, ...{comparisonOperator: ComparisonOperator.NumericEquals}}); - // } - // } - - // export class NumericLessThanComparisonOperation extends VariableComparisonOperation { - // constructor(props: VariableComparisonOperationProps) { - // super({...props, ...{comparisonOperator: ComparisonOperator.NumericLessThan}}); - // } - // } - - // export class NumericGreaterThanComparisonOperation extends VariableComparisonOperation { - // constructor(props: VariableComparisonOperationProps) { - // super({...props, ...{comparisonOperator: ComparisonOperator.NumericGreaterThan}}); - // } - // } - - // export class NumericLessThanEqualsComparisonOperation extends VariableComparisonOperation { - // constructor(props: VariableComparisonOperationProps) { - // super({...props, ...{comparisonOperator: ComparisonOperator.NumericLessThanEquals}}); - // } - // } - - // export class NumericGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { - // constructor(props: VariableComparisonOperationProps) { - // super({...props, ...{comparisonOperator: ComparisonOperator.NumericGreaterThanEquals}}); - // } - // } - - // export class BooleanEqualsComparisonOperation extends VariableComparisonOperation { - // constructor(props: VariableComparisonOperationProps) { - // super({...props, ...{comparisonOperator: ComparisonOperator.BooleanEquals}}); - // } - // } - - // export class TimestampEqualsComparisonOperation extends VariableComparisonOperation { - // constructor(props: VariableComparisonOperationProps) { - // super({...props, ...{comparisonOperator: ComparisonOperator.TimestampEquals}}); - // } - // } - - // export class TimestampLessThanComparisonOperation extends VariableComparisonOperation { - // constructor(props: VariableComparisonOperationProps) { - // super({...props, ...{comparisonOperator: ComparisonOperator.TimestampLessThan}}); - // } - // } - - // export class TimestampGreaterThanComparisonOperation extends VariableComparisonOperation { - // constructor(props: VariableComparisonOperationProps) { - // super({...props, ...{comparisonOperator: ComparisonOperator.TimestampGreaterThan}}); - // } - // } - - // export class TimestampLessThanEqualsComparisonOperation extends VariableComparisonOperation { - // constructor(props: VariableComparisonOperationProps) { - // super({...props, ...{comparisonOperator: ComparisonOperator.TimestampLessThanEquals}}); - // } - // } - - // export class TimestampGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { - // constructor(props: VariableComparisonOperationProps) { - // super({...props, ...{comparisonOperator: ComparisonOperator.TimestampGreaterThanEquals}}); - // } - // } - - // export interface ArrayComparisonOperationProps { - // comparisonOperator: ComparisonOperator, - // comparisonOperations: ComparisonOperation[] - // } - - // // export abstract class ArrayComparisonOperation extends ComparisonOperation { - // // protected readonly props: ArrayComparisonOperationProps; - - // // constructor(props: ArrayComparisonOperationProps) { - // // super(null); - // // this.props = props; - // // } - - // // public toJson() { - // // return { - // // // [this.props.comparisonOperator]: this.props.comparisonOperations.map(comparisonOperation => comparisonOperation.toJson()) - // // }; - // // } - // // } - - // // export class AndComparisonOperation extends ArrayComparisonOperation { - // // constructor(...comparisonOperations: ComparisonOperation[]) { - // // super({ comparisonOperator: ComparisonOperator.And, comparisonOperations}); - // // } - // // } - - // // export class OrComparisonOperation extends ArrayComparisonOperation { - // // constructor(...comparisonOperations: ComparisonOperation[]) { - // // super({ comparisonOperator: ComparisonOperator.Or, comparisonOperations}); - // // } - // // } - - // // export class NotComparisonOperation extends ComparisonOperation { - // // protected readonly comparisonOperation: ComparisonOperation; - - // // constructor(comparisonOperation: ComparisonOperation) { - // // super(null); - // // this.comparisonOperation = comparisonOperation; - // // } - - // // public toJson() { - // // return { - // // [ComparisonOperator.Not]: this.comparisonOperation.toJson() - // // }; - // // } - // // } - - // export interface ChoiceRuleProps extends NextField { - // comparisonOperation: ComparisonOperation; - // } - - // export class ChoiceRule extends Jsonable { - // protected readonly props: ChoiceRuleProps; - - // constructor(props: ChoiceRuleProps) { - // super(null); - // this.props = props; - // } - - // public toJson() { - // return { - // // ...this.props.comparisonOperation.toJson(), - // // ...{next: this.props.next} - // }; - // } - // } + public isTerminal() { + return true; + } + } export interface ParallelStateProps extends Commentable, InputOutputPathFields, ResultPathField, RetryCatchFields, NextOrEndField { branches: Branch[] } - export class ParallelState extends State { + export class ParallelState extends BaseState { constructor(props: ParallelStateProps) { requireNextOrEnd(props); super(StateType.Parallel, props); } + + public stateNames(): string[] { + const names: string[] = []; + this.props.branches.forEach((branch: Branch) => branch.stateNames().forEach(name => names.push(name))); + return names; + } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/util.ts b/packages/@aws-cdk/aws-stepfunctions/lib/util.ts index b42d9865fd7d2..98d40d13f517e 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/util.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/util.ts @@ -8,4 +8,10 @@ export function requireAll(props: { [name: string]: any }, names: string[]) { if (names.map(name => name in props).filter(x => x === false).length > 0) { throw new Error(`${props} must specify exactly all of: ${names}`); } +} + +export function requirePositiveInteger(props: { [name: string]: any }, name: string) { + if (!Number.isInteger(props[name]) || props[name] < 0) { + throw new Error(`${name} must be a postive integer`); + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts index 40d8a7710c8da..4cf11a6654b75 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts @@ -1,26 +1,202 @@ -import { Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; -import { amazon_states_language as asl, cloudformation } from '../lib'; +import { amazon_states_language as asl } from '../lib'; export = { 'Hello World'(test: Test) { - const stack = new Stack(); - - new cloudformation.StateMachineResource(stack, "StateMachine", { - roleArn: "", - definitionString: new asl.StateMachine({ - comment: "A simple minimal example of the States language", - startAt: "Hello World", - states: { - "Hello World": new asl.TaskState({ - resource: "arn:aws:lambda:us-east-1:123456789012:function:HelloWorld", - end: true + test.deepEqual( + JSON.parse(JSON.stringify( + new asl.StateMachine({ + comment: "A simple minimal example of the States language", + startAt: "Hello World", + states: new asl.States({ + "Hello World": new asl.TaskState({ + resource: "arn:aws:lambda:us-east-1:123456789012:function:HelloWorld", + end: true + }) }) + }) + )), + { + Comment: "A simple minimal example of the States language", + StartAt: "Hello World", + States: { + "Hello World": { + Type: "Task", + Resource: "arn:aws:lambda:us-east-1:123456789012:function:HelloWorld", + End: true + } } - }).definitionString() - }); - + } + ); + test.done(); + }, + 'Complex retry scenarios'(test: Test) { + test.deepEqual( + JSON.parse(JSON.stringify( + new asl.TaskState({ + resource: "arn:aws:swf:us-east-1:123456789012:task:X", + next: "Y", + retry: new asl.Retriers([ + new asl.Retrier({ + errorEquals: ["ErrorA", "ErrorB"], + intervalSeconds: 1, + backoffRate: 2, + maxAttempts: 2 + }), + new asl.Retrier({ + errorEquals: ["ErrorC"], + intervalSeconds: 5 + }) + ]), + catch: new asl.Catchers([ + new asl.Catcher({ + errorEquals: [ asl.ErrorCode.ALL ], + next: "Z" + }) + ]) + }) + )), + { + Type: "Task", + Resource: "arn:aws:swf:us-east-1:123456789012:task:X", + Next: "Y", + Retry: [ + { + ErrorEquals: [ "ErrorA", "ErrorB" ], + IntervalSeconds: 1, + BackoffRate: 2, + MaxAttempts: 2 + }, + { + ErrorEquals: [ "ErrorC" ], + IntervalSeconds: 5 + } + ], + Catch: [ + { + ErrorEquals: [ "States.ALL" ], + Next: "Z" + } + ] + } + ); + test.done(); + }, + 'Choice state'(test: Test) { + test.deepEqual( + JSON.parse(JSON.stringify( + new asl.ChoiceState({ + choices: new asl.ChoiceRules( + new asl.ChoiceRule({ + comparisonOperation: new asl.NotComparisonOperation( + new asl.StringEqualsComparisonOperation({ + variable: "$.type", + value: "Private" + }) + ), + next: "Public" + }), + new asl.ChoiceRule({ + comparisonOperation: new asl.AndComparisonOperation( + new asl.NumericGreaterThanEqualsComparisonOperation({ + variable: "$.value", + value: 20 + }), + new asl.NumericLessThanComparisonOperation({ + variable: "$.value", + value: 30 + }) + ), + next: "ValueInTwenties" + }) + ), + default: "DefaultState" + }) + )), + { + Type : "Choice", + Choices: [ + { + Not: { + Variable: "$.type", + StringEquals: "Private" + }, + Next: "Public" + }, + { + And: [ + { + Variable: "$.value", + NumericGreaterThanEquals: 20 + }, + { + Variable: "$.value", + NumericLessThan: 30 + } + ], + Next: "ValueInTwenties" + } + ], + Default: "DefaultState" + } + ); + test.done(); + }, + 'Parallel state'(test: Test) { + test.deepEqual( + JSON.parse(JSON.stringify( + new asl.ParallelState({ + branches: [ + new asl.Branch({ + startAt: "LookupAddress", + states: new asl.States({ + LookupAddress: new asl.TaskState({ + resource: "arn:aws:lambda:us-east-1:123456789012:function:AddressFinder", + end: true + }) + }) + }), + new asl.Branch({ + startAt: "LookupPhone", + states: new asl.States({ + LookupPhone: new asl.TaskState({ + resource: "arn:aws:lambda:us-east-1:123456789012:function:PhoneFinder", + end: true + }) + }) + }) + ], + next: "NextState" + }) + )), + { + Type: "Parallel", + Branches: [ + { + StartAt: "LookupAddress", + States: { + LookupAddress: { + Type: "Task", + Resource: "arn:aws:lambda:us-east-1:123456789012:function:AddressFinder", + End: true + } + } + }, + { + StartAt: "LookupPhone", + States: { + LookupPhone: { + Type: "Task", + Resource: "arn:aws:lambda:us-east-1:123456789012:function:PhoneFinder", + End: true + } + } + } + ], + Next: "NextState" + } + ); test.done(); } }; From 1fbdee313fba7794a09e4aa94af5f11b4e8a42aa Mon Sep 17 00:00:00 2001 From: Andrew DiLosa Date: Thu, 26 Jul 2018 11:37:05 -0700 Subject: [PATCH 04/29] Add method and class documentation --- .../lib/amazon-states-language.ts | 527 +++++++++++++++--- .../test/test.amazon-states-language.ts | 4 +- packages/@aws-cdk/cdk/lib/core/tokens.ts | 2 +- 3 files changed, 446 insertions(+), 87 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/amazon-states-language.ts b/packages/@aws-cdk/aws-stepfunctions/lib/amazon-states-language.ts index 4e8c4d38c3961..fabdd53a929a4 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/amazon-states-language.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/amazon-states-language.ts @@ -1,13 +1,21 @@ -import { Token, tokenAwareJsonify } from "@aws-cdk/core"; +import { Token, tokenAwareJsonify, istoken } from "@aws-cdk/core"; import { isString } from "util"; import { requireOneOf } from "./util"; +/** + * Models the Amazon States Language + * + * {@link https://states-language.net/spec.html} + */ export namespace amazon_states_language { function requireNextOrEnd(props: any) { requireOneOf(props, ['next', 'end']); } - export class Jsonable { + /** + * Converts all keys to PascalCase when serializing to JSON. + */ + export class PascalCaseJson { public readonly props: any; constructor(props: any) { @@ -30,13 +38,33 @@ export namespace amazon_states_language { } export interface StateMachineProps extends BranchProps { + /** + * The version of the States language. + * + * @default "1.0" + * + * {@link https://states-language.net/spec.html#toplevelfields} + */ version?: string | Token, - timeoutSeconds?: number - } - export class StateMachine extends Jsonable { + /** + * The maximum number of seconds the machine is allowed to run. + * + * If the machine runs longer than the specified time, then the + * interpreter fails the machine with a {@link ErrorCode#Timeout} + * Error Name. + * + * {@link https://states-language.net/spec.html#toplevelfields} + */ + timeoutSeconds?: number | Token + } + + /** + * A State Machine which can serialize to a JSON object + */ + export class StateMachine extends PascalCaseJson { constructor(props: StateMachineProps) { - if (props.timeoutSeconds !== undefined && !Number.isInteger(props.timeoutSeconds)) { + if (props.timeoutSeconds !== undefined && !istoken(props.timeoutSeconds) && !Number.isInteger(props.timeoutSeconds)) { throw new Error("timeoutSeconds must be an integer"); } const allStates = props.states.stateNames(); @@ -46,21 +74,44 @@ export namespace amazon_states_language { super(props); } + // tslint:disable:max-line-length + /** + * Returns a JSON representation for use with CloudFormation. + * + * @link http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html#cfn-stepfunctions-statemachine-definitionstring + */ + // tslint:enable:max-line-length public definitionString() { return tokenAwareJsonify(this.toJSON()); } } export interface Commentable { + /** + * A comment provided as human-readable description + */ comment?: string | Token } export interface BranchProps extends Commentable { + /** + * Represents the states in this State Machine. + * + * {@link https://states-language.net/spec.html#toplevelfields} + */ states: States, + + /** + * Name of the state the interpreter starts running the machine at. + * + * Must exactly match one of the names of the {@link states} field. + * + * {@link https://states-language.net/spec.html#toplevelfields} + */ startAt: string | Token } - export class Branch extends Jsonable { + export class Branch extends PascalCaseJson { constructor(props: BranchProps) { if (isString(props.startAt) && !props.states.hasState(props.startAt)) { throw new Error(`Specified startAt state '${props.startAt}' does not exist in states map`); @@ -68,12 +119,20 @@ export namespace amazon_states_language { super(props); } - public stateNames(): string[] { + public stateNames(): Array { return this.props.states.stateNames(); } } - export class States extends Jsonable { + /** + * The States of the State Machine. + * + * State names must have length of less than or equal to 128 characters. + * State names must be unique within the scope of the whole state machine. + * + * {@link https://states-language.net/spec.html#states-fields} + */ + export class States extends PascalCaseJson { constructor(states: { [name: string]: State }) { const longNames = Object.keys(states).filter(n => n.length > 128); if (longNames.length > 0) { @@ -112,72 +171,209 @@ export namespace amazon_states_language { } export interface NextField { - next: string; + /** + * The name of the next state to execute. + * + * Must exactly match the name of another state in the state machine. + */ + next: string | Token; } export interface EndField { - end: true; + /** + * Marks the state as an End State. + * + * After the interpreter executes an End State, the state machine will + * terminate and return a result. + */ + end: true | Token; } export interface NextOrEndField { - next?: string, + /** + * The name of the next state to execute. + * + * Must exactly match the name of another state in the state machine. + */ + next?: string | Token, + + /** + * Marks the state as an End State. + * + * After the interpreter executes an End State, the state machine will + * terminate and return a result. + */ end?: true } export interface InputOutputPathFields { - inputPath?: string; - outputPath?: string; + /** + * A {@link https://states-language.net/spec.html#path Path} applied to + * a State's raw input to select some or all of it. + * + * The selection is used as the input to the State. + * + * If `null` the raw input is discarded, and teh effective input for + * the state is an empty JSON object. + * + * @default "$", which selects the whole raw input + * + * {@link https://states-language.net/spec.html#filters} + */ + inputPath?: string | Token; + + /** + * A {@link https://states-language.net/spec.html#path Path} applied to + * a State's output after the application of {@link ResultPathField#resultPath ResultPath} + * leading in the generation of the raw input for the next state. + * + * If `null` the input and result are discarded, and the effective + * output for the state is an empty JSON object. + * + * @default "$", which is effectively the result of processing {@link ResultPathField#resultPath ResultPath} + * + * {@link https://states-language.net/spec.html#filters} + */ + outputPath?: string | Token; } export interface ResultPathField { - resultPath?: string; - } - + /** + * A {@link https://states-language.net/spec.html#ref-paths Reference Path} + * which specifies the where to place the result, relative to the raw input. + * + * If the input has a field which matches the ResultPath value, then in + * the * output, that field is discarded and overwritten. Otherwise, a + * new field is created. + * + * If `null` the state's own raw output is discarded and its raw input + * becomes its result. + * + * @default "$", the state's result overwrites and replaces the raw input + * + * {@link https://states-language.net/spec.html#filters} + */ + resultPath?: string | Token; + } + + /** + * Predefined Error Codes + * + * {@link https://states-language.net/spec.html#appendix-a} + */ export enum ErrorCode { + /** + * A wild-card which matches any Error Name. + */ ALL = "States.ALL", + + /** + * A {@link TaskState Task State} either ran longer than the {@link TaskStateProps#timeoutSeconds TimeoutSeconds} value, + * or failed to heartbeat for a time longer than the {@link TaskStateProps#heartbeatSeconds HeartbeatSeconds} value. + */ Timeout = "States.Timeout", + + /** + * A {@link TaskState Task State} failed during the execution. + */ TaskFailed = "States.TaskFailed", + + /** + * A {@link TaskState Task State} failed because it had insufficient privileges to + * execute the specified code. + */ Permissions = "States.Permissions", + + /** + * A {@link TaskState Task State} failed because it had insufficient privileges to + * execute the specified code. + */ ResultPathMatchFailure = "States.ResultPathMatchFailure", + + /** + * A branch of a {@link ParallelState Parallel state} failed. + */ BranchFailed = "States.BranchFailed", + + /** + * A {@link ChoiceState Choice state} failed to find a match for the condition field + * extracted from its input. + */ NoChoiceMatched = "States.NoChoiceMatched" } export interface ErrorEquals { - errorEquals: string[]; + /** + * A non-empty array of {@link https://states-language.net/spec.html#error-names Error Names} + * this rule should match. + * + * Can use {@link ErrorCode} values for pre-defined Error Codes. + * + * The reserved error name {@link ErrorCode#ALL} is a wild-card and + * matches any Error Name. It must appear alone in the array and must + * appear in the last {@link Retrier}/{@link Catcher} in the + * {@link RetryCatchFields#retry}/{@link RetryCatchFields#catch} arrays + * + * {@link https://states-language.net/spec.html#errors} + */ + errorEquals: Array } export interface RetrierProps extends ErrorEquals { - intervalSeconds?: number; - maxAttempts?: number; - backoffRate?: number; - } - - function isErrorName(error: string) { - return Object.values(ErrorCode).includes(error) || !error.startsWith("States."); - } - - function validateErrorEquals(errors: string[]) { + /** + * Number of seconds before first retry attempt. + * + * Must be a positive integer. + * + * @default 1 + */ + intervalSeconds?: number | Token + + /** + * Maximum number of retry attempts. + * + * Must be a non-negative integer. May be set to 0 to specify no retries + * + * @default 3 + */ + maxAttempts?: number | Token + + /** + * Multiplier tha tincreases the retry interval on each attempt. + * + * Must be greater than or equal to 1.0. + * + * @default 2.0 + */ + backoffRate?: number | Token + } + + function validateErrorEquals(errors: Array) { if (errors.length === 0) { throw new Error('ErrorEquals is empty. Must be a non-empty array of Error Names'); } if (errors.length > 1 && errors.includes(ErrorCode.ALL)) { throw new Error(`Error name '${ErrorCode.ALL}' is specified along with other Error Names. '${ErrorCode.ALL}' must appear alone.`); } - errors.forEach(error => { - if (!isErrorName(error)) { - throw new Error(`'${error}' is not a valid Error Name`); + errors.forEach(name => { + if (isString(name) && !(Object.values(ErrorCode).includes(name) || !name.startsWith("States."))) { + throw new Error(`'${name}' is not a valid Error Name`); } }); } - export class Retrier extends Jsonable { + /** + * A retry policy for the specified errors. + * + * {@link https://states-language.net/spec.html#errors} + */ + export class Retrier extends PascalCaseJson { constructor(props: RetrierProps) { validateErrorEquals(props.errorEquals); - if (props.intervalSeconds && (!Number.isInteger(props.intervalSeconds) || props.intervalSeconds < 1)) { + if (props.intervalSeconds && !istoken(props.intervalSeconds) && (!Number.isInteger(props.intervalSeconds) || props.intervalSeconds < 1)) { throw new Error(`intervalSeconds '${props.intervalSeconds}' is not a positive integer`); } - if (props.maxAttempts && (!Number.isInteger(props.maxAttempts) || props.maxAttempts < 0)) { + if (props.maxAttempts && !istoken(props.maxAttempts) && (!Number.isInteger(props.maxAttempts) || props.maxAttempts < 0)) { throw new Error(`maxAttempts '${props.maxAttempts}' is not a non-negative integer`); } if (props.backoffRate && props.backoffRate < 1.0) { @@ -191,7 +387,12 @@ export namespace amazon_states_language { } - export class Catcher extends Jsonable { + /** + * A fallback state for the specified errors. + * + * {@link https://states-language.net/spec.html#errors} + */ + export class Catcher extends PascalCaseJson { constructor(props: CatcherProps) { validateErrorEquals(props.errorEquals); super(props); @@ -199,7 +400,21 @@ export namespace amazon_states_language { } export interface RetryCatchFields { + /** + * Ordered array of {@link Retrier} the interpreter scans through on + * error. + * + * {@link https://states-language.net/spec.html#errors} + */ retry?: Retriers; + + /** + * Ordered array of {@link Catcher} the interpeter scans through to + * handle errors when there is no {@link retry} or retries have been + * exhausted. + * + * {@link https://states-language.net/spec.html#errors} + */ catch?: Catchers; } @@ -213,20 +428,25 @@ export namespace amazon_states_language { }); } - export class Retriers extends Jsonable { + export class Retriers extends PascalCaseJson { constructor(retriers: Retrier[]) { validateErrorAllAppearsLast(retriers.map(retrier => retrier.props)); super(retriers); } } - export class Catchers extends Jsonable { + export class Catchers extends PascalCaseJson { constructor(catchers: Catcher[]) { validateErrorAllAppearsLast(catchers.map(catcher => catcher.props)); super(catchers); } } + /** + * Values for the "Type" field which is required for every State object. + * + * {@link https://states-language.net/spec.html#statetypes} + */ export enum StateType { Pass, Task, @@ -237,12 +457,17 @@ export namespace amazon_states_language { Parallel } + /** + * A State in a State Machine. + * + * {@link https://states-language.net/spec.html#statetypes} + */ export interface State { isTerminal(): boolean - next(): string[] + next(): Array; } - export abstract class BaseState extends Jsonable implements State { + export abstract class BaseState extends PascalCaseJson implements State { constructor(type: StateType, props: any) { super({ ...props, ...{type: StateType[type]}}); } @@ -257,9 +482,21 @@ export namespace amazon_states_language { } export interface PassStateProps extends Commentable, InputOutputPathFields, ResultPathField, NextOrEndField { + /** + * Treated as the output of a virtual task, placed as prescribed by the {@link resultPath} field. + * + * @default By default, the output is the input. + */ result?: any; } + /** + * Passes its input to its output, performing no work. + * + * Can also be used to inject fixed data into the state machine. + * + * {@link https://states-language.net/spec.html#pass-state} + */ export class PassState extends BaseState { constructor(props: PassStateProps) { requireNextOrEnd(props); @@ -268,17 +505,43 @@ export namespace amazon_states_language { } export interface TaskStateProps extends Commentable, InputOutputPathFields, ResultPathField, RetryCatchFields, NextOrEndField { - resource: string; - timeoutSeconds?: number; - heartbeatSeconds?: number; - } - + /** + * A URI identifying the task to execute. + * + * The States language does not constrain the URI. However, the AWS + * Step Functions intepreter only supports the ARN of an Activity or + * Lambda function. {@link https://docs.aws.amazon.com/step-functions/latest/dg/concepts-tasks.html} + */ + resource: string | Token; + + /** + * The maxium number of seconds the state will run before the interpeter + * fails it with a {@link ErrorCode.Timeout} error. + * + * Must be a positive integer, and must be smaller than {@link heartbeatSeconds} if provided. + * + * @default 60 + */ + timeoutSeconds?: number | Token; + + /** + * The number of seconds between heartbeats before the intepreter + * fails the state with a {@link ErrorCode.Timeout} error. + */ + heartbeatSeconds?: number | Token; + } + + /** + * Executes the work identified by the {@link TaskStateProps#resource} field. + * + * {@link https://states-language.net/spec.html#task-state} + */ export class TaskState extends BaseState { constructor(props: TaskStateProps) { - if (props.timeoutSeconds !== undefined && !Number.isInteger(props.timeoutSeconds)) { + if (props.timeoutSeconds !== undefined && !istoken(props.timeoutSeconds) && !Number.isInteger(props.timeoutSeconds)) { throw new Error(`timeoutSeconds '${props.timeoutSeconds}' is not an integer`); } - if (props.heartbeatSeconds !== undefined && !Number.isInteger(props.heartbeatSeconds)) { + if (props.heartbeatSeconds !== undefined && !istoken(props.timeoutSeconds) && !Number.isInteger(props.heartbeatSeconds)) { throw new Error(`heartbeatSeconds '${props.heartbeatSeconds}' is not an integer`); } if (props.timeoutSeconds !== undefined && props.heartbeatSeconds !== undefined && props.heartbeatSeconds >= props.timeoutSeconds) { @@ -311,18 +574,25 @@ export namespace amazon_states_language { Not } - export abstract class ComparisonOperation extends Jsonable { + export abstract class ComparisonOperation extends PascalCaseJson { } export interface BaseVariableComparisonOperationProps { comparisonOperator: ComparisonOperator, value: any, - variable: string + variable: string | Token } export interface VariableComparisonOperationProps { + /** + * The value to be compared against. + */ value: T, - variable: string + + /** + * A Path to the value to be compared. + */ + variable: string | Token } export abstract class VariableComparisonOperation extends ComparisonOperation { @@ -339,97 +609,97 @@ export namespace amazon_states_language { } export class StringEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { + constructor(props: VariableComparisonOperationProps) { super({...props, ...{comparisonOperator: ComparisonOperator.StringEquals}}); } } export class StringLessThanComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { + constructor(props: VariableComparisonOperationProps) { super({...props, ...{comparisonOperator: ComparisonOperator.StringLessThan}}); } } export class StringGreaterThanComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { + constructor(props: VariableComparisonOperationProps) { super({...props, ...{comparisonOperator: ComparisonOperator.StringGreaterThan}}); } } export class StringLessThanEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { + constructor(props: VariableComparisonOperationProps) { super({...props, ...{comparisonOperator: ComparisonOperator.StringLessThanEquals}}); } } export class StringGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { + constructor(props: VariableComparisonOperationProps) { super({...props, ...{comparisonOperator: ComparisonOperator.StringGreaterThanEquals}}); } } export class NumericEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { + constructor(props: VariableComparisonOperationProps) { super({...props, ...{comparisonOperator: ComparisonOperator.NumericEquals}}); } } export class NumericLessThanComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { + constructor(props: VariableComparisonOperationProps) { super({...props, ...{comparisonOperator: ComparisonOperator.NumericLessThan}}); } } export class NumericGreaterThanComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { + constructor(props: VariableComparisonOperationProps) { super({...props, ...{comparisonOperator: ComparisonOperator.NumericGreaterThan}}); } } export class NumericLessThanEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { + constructor(props: VariableComparisonOperationProps) { super({...props, ...{comparisonOperator: ComparisonOperator.NumericLessThanEquals}}); } } export class NumericGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { + constructor(props: VariableComparisonOperationProps) { super({...props, ...{comparisonOperator: ComparisonOperator.NumericGreaterThanEquals}}); } } export class BooleanEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { + constructor(props: VariableComparisonOperationProps) { super({...props, ...{comparisonOperator: ComparisonOperator.BooleanEquals}}); } } export class TimestampEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { + constructor(props: VariableComparisonOperationProps) { super({...props, ...{comparisonOperator: ComparisonOperator.TimestampEquals}}); } } export class TimestampLessThanComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { + constructor(props: VariableComparisonOperationProps) { super({...props, ...{comparisonOperator: ComparisonOperator.TimestampLessThan}}); } } export class TimestampGreaterThanComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { + constructor(props: VariableComparisonOperationProps) { super({...props, ...{comparisonOperator: ComparisonOperator.TimestampGreaterThan}}); } } export class TimestampLessThanEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { + constructor(props: VariableComparisonOperationProps) { super({...props, ...{comparisonOperator: ComparisonOperator.TimestampLessThanEquals}}); } } export class TimestampGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { + constructor(props: VariableComparisonOperationProps) { super({...props, ...{comparisonOperator: ComparisonOperator.TimestampGreaterThanEquals}}); } } @@ -472,14 +742,24 @@ export namespace amazon_states_language { comparisonOperation: ComparisonOperation; } - export class ChoiceRule extends Jsonable { + /** + * A rule desribing a conditional state transition. + */ + export class ChoiceRule extends PascalCaseJson { constructor(props: ChoiceRuleProps) { super({...props.comparisonOperation.props, next: props.next}); } } - export class ChoiceRules extends Jsonable { + /** + * An non-empty ordered array of ChoiceRule the interpreter will scan through + * in order to make a state transition choice. + */ + export class ChoiceRules extends PascalCaseJson { constructor(...choices: ChoiceRule[]) { + if (choices.length === 0) { + throw new Error("'Choices' array is empty. Must specify non-empty array of ChoiceRule."); + } super(choices); } @@ -493,10 +773,27 @@ export namespace amazon_states_language { } export interface ChoiceStateProps extends Commentable, InputOutputPathFields { + /** + * Ordered array of {@link ChoiceRule}s the interpreter will scan through. + */ choices: ChoiceRules; - default?: string; - } + /** + * Name of a state to transition to if none of the ChoiceRules in + * {@link choices} match. + * + * @default The interpreter will raise a {@link ErrorCode.NoChoiceMatched} + * error if no {@link ChoiceRule} matches and there is no {@link default} + * specified. + */ + default?: string | Token; + } + + /** + * Adds branching logic to a state machine. + * + * {@link https://states-language.net/spec.html#choice-state} + */ export class ChoiceState extends BaseState { constructor(props: ChoiceStateProps) { if (props.choices.length === 0) { @@ -511,12 +808,38 @@ export namespace amazon_states_language { } export interface WaitStateProps extends Commentable, InputOutputPathFields, NextOrEndField { - seconds?: number - secondsPath?: string, - timestamp?: string, - timestampPath?: string - } - + /** + * Number of seconds to wait. + */ + seconds?: number | Token + + /** + * {@link https://states-language.net/spec.html#ref-paths Reference Path} to a value for + * the number of seconds to wait. + */ + secondsPath?: string | Token, + + /** + * Wait until specified absolute time. + * + * Must be an ISO-8601 extended offsete date-time formatted string. + */ + timestamp?: string | Token, + + /** + * {@link https://states-language.net/spec.html#ref-paths Reference Path} to a value for + * an absolute expiry time. + * + * Value must be an ISO-8601 extended offsete date-time formatted string. + */ + timestampPath?: string | Token + } + + /** + * Delay the state machine for a specified time. + * + * {@link https://states-language.net/spec.html#wait-state} + */ export class WaitState extends BaseState { constructor(props: WaitStateProps) { requireOneOf(props, ['seconds', 'secondsPath', 'timestamp', 'timestampPath']); @@ -528,6 +851,11 @@ export namespace amazon_states_language { export interface SucceedStateProps extends Commentable, InputOutputPathFields { } + /** + * Terminate the state machine successfully. + * + * {@link https://states-language.net/spec.html#succeed-state} + */ export class SucceedState extends BaseState { constructor(props: SucceedStateProps = {}) { super(StateType.Succeed, props); @@ -539,10 +867,23 @@ export namespace amazon_states_language { } export interface FailStateProps extends BaseState, Commentable { - error: string, - cause: string - } - + /** + * An Error Name used for error handling in a {@link Retrier} or {@link Catcher}, + * or for operational/diagnostic purposes. + */ + error: string | Token, + + /** + * A human-readable message describing the error. + */ + cause: string | Token + } + + /** + * Terminate the state machine and mark the execution as a failure. + * + * {@link https://states-language.net/spec.html#fail-state} + */ export class FailState extends BaseState { constructor(props: FailStateProps) { super(StateType.Fail, props); @@ -554,19 +895,37 @@ export namespace amazon_states_language { } export interface ParallelStateProps extends Commentable, InputOutputPathFields, ResultPathField, RetryCatchFields, NextOrEndField { - branches: Branch[] + /** + * An array of branches to execute in parallel. + */ + branches: Branches } + export class Branches extends PascalCaseJson { + constructor(...branches: Branch[]) { + super(branches); + } + + public stateNames(): Array { + const names: Array = []; + this.props.branches.forEach((branch: Branch) => branch.stateNames().forEach(name => names.push(name))); + return names; + } + } + + /** + * Executes branches in parallel. + * + * {@link https://states-language.net/spec.html#parallel-state} + */ export class ParallelState extends BaseState { constructor(props: ParallelStateProps) { requireNextOrEnd(props); super(StateType.Parallel, props); } - public stateNames(): string[] { - const names: string[] = []; - this.props.branches.forEach((branch: Branch) => branch.stateNames().forEach(name => names.push(name))); - return names; + public stateNames(): Array { + return this.props.branches.stateNames(); } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts index 4cf11a6654b75..2314c934728e9 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts @@ -147,7 +147,7 @@ export = { test.deepEqual( JSON.parse(JSON.stringify( new asl.ParallelState({ - branches: [ + branches: new asl.Branches( new asl.Branch({ startAt: "LookupAddress", states: new asl.States({ @@ -166,7 +166,7 @@ export = { }) }) }) - ], + ), next: "NextState" }) )), diff --git a/packages/@aws-cdk/cdk/lib/core/tokens.ts b/packages/@aws-cdk/cdk/lib/core/tokens.ts index b0f953ee2544b..60efaa8e9c6ca 100644 --- a/packages/@aws-cdk/cdk/lib/core/tokens.ts +++ b/packages/@aws-cdk/cdk/lib/core/tokens.ts @@ -35,7 +35,7 @@ export class Token { * Returns true if obj is a token (i.e. has the resolve() method) * @param obj The object to test. */ -export function istoken(obj: any) { +export function istoken(obj: any): obj is Token { return typeof(obj[RESOLVE_METHOD]) === 'function'; } From b4bad6b0b196422ab25da7f896ae1bfcb291e470 Mon Sep 17 00:00:00 2001 From: Andrew DiLosa Date: Thu, 26 Jul 2018 17:53:08 -0700 Subject: [PATCH 05/29] Add unit tests --- .../lib/amazon-states-language.ts | 114 +-- .../@aws-cdk/aws-stepfunctions/lib/index.ts | 2 +- .../aws-stepfunctions/lib/state-machine.ts.bk | 732 ------------------ .../@aws-cdk/aws-stepfunctions/lib/util.ts | 14 +- .../aws-stepfunctions/package-lock.json | 108 --- .../test/test.amazon-states-language.ts | 329 ++++++-- .../test/test.stepfunctions.ts | 8 - packages/aws-cdk/package-lock.json | 9 + 8 files changed, 331 insertions(+), 985 deletions(-) delete mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts.bk delete mode 100644 packages/@aws-cdk/aws-stepfunctions/package-lock.json delete mode 100644 packages/@aws-cdk/aws-stepfunctions/test/test.stepfunctions.ts diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/amazon-states-language.ts b/packages/@aws-cdk/aws-stepfunctions/lib/amazon-states-language.ts index fabdd53a929a4..ee877dcc27d88 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/amazon-states-language.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/amazon-states-language.ts @@ -1,17 +1,13 @@ -import { Token, tokenAwareJsonify, istoken } from "@aws-cdk/core"; +import { istoken, Token, tokenAwareJsonify } from "@aws-cdk/cdk"; import { isString } from "util"; -import { requireOneOf } from "./util"; +import { requireNextOrEnd, requireOneOf } from "./util"; /** * Models the Amazon States Language * * {@link https://states-language.net/spec.html} */ -export namespace amazon_states_language { - function requireNextOrEnd(props: any) { - requireOneOf(props, ['next', 'end']); - } - +export namespace AmazonStatesLanguage { /** * Converts all keys to PascalCase when serializing to JSON. */ @@ -67,8 +63,11 @@ export namespace amazon_states_language { if (props.timeoutSeconds !== undefined && !istoken(props.timeoutSeconds) && !Number.isInteger(props.timeoutSeconds)) { throw new Error("timeoutSeconds must be an integer"); } + if (isString(props.startAt) && !props.states.hasState(props.startAt)) { + throw new Error(`Specified startAt state '${props.startAt}' does not exist in states map`); + } const allStates = props.states.stateNames(); - if (new Set(allStates).size !== allStates.length) { + if (!allStates.some(istoken) && new Set(allStates).size !== allStates.length) { throw new Error('State names are not unique within the whole state machine'); } super(props); @@ -112,15 +111,18 @@ export namespace amazon_states_language { } export class Branch extends PascalCaseJson { + private readonly states: States; + constructor(props: BranchProps) { if (isString(props.startAt) && !props.states.hasState(props.startAt)) { throw new Error(`Specified startAt state '${props.startAt}' does not exist in states map`); } super(props); + this.states = props.states; } public stateNames(): Array { - return this.props.states.stateNames(); + return this.states.stateNames(); } } @@ -133,23 +135,22 @@ export namespace amazon_states_language { * {@link https://states-language.net/spec.html#states-fields} */ export class States extends PascalCaseJson { - constructor(states: { [name: string]: State }) { + constructor(states: { [key: string]: State }) { const longNames = Object.keys(states).filter(n => n.length > 128); if (longNames.length > 0) { throw new Error(`State names ${JSON.stringify(longNames)} exceed 128 characters in length`); } - Object.keys(states).forEach(stateName => { - const state = states[stateName]; + for (const [stateName, state] of Object.entries(states)) { const next = state.next(); if (!state.isTerminal() && next.length === 0 && !(state instanceof ChoiceState)) { throw new Error(`Non-terminal and non-ChoiceState state '${state}' does not have a 'next' field`); } next.forEach(referencedState => { - if (!(referencedState in states)) { + if (!istoken(referencedState) && !(referencedState in states)) { throw new Error(`State '${stateName}' references unknown Next state '${referencedState}'`); } }); - }); + } super(states); } @@ -161,8 +162,8 @@ export namespace amazon_states_language { return this.props.hasOwnProperty(name); } - public stateNames(): string[] { - const names = Object.keys(this.props); + public stateNames(): Array { + const names: Array = Object.keys(this.props); Object.values(this.props).map( state => (state instanceof ParallelState) ? state.stateNames() : [] ).forEach(branchNames => branchNames.forEach(name => names.push(name))); @@ -186,7 +187,7 @@ export namespace amazon_states_language { * After the interpreter executes an End State, the state machine will * terminate and return a result. */ - end: true | Token; + end: true; } export interface NextOrEndField { @@ -418,7 +419,7 @@ export namespace amazon_states_language { catch?: Catchers; } - function validateErrorAllAppearsLast(props: ErrorEquals[]) { + function validateErrorAllNotBeforeLast(props: ErrorEquals[]) { props.slice(0, -1).forEach(prop => { if (prop.errorEquals.includes(ErrorCode.ALL)) { throw new Error( @@ -430,14 +431,14 @@ export namespace amazon_states_language { export class Retriers extends PascalCaseJson { constructor(retriers: Retrier[]) { - validateErrorAllAppearsLast(retriers.map(retrier => retrier.props)); + validateErrorAllNotBeforeLast(retriers.map(retrier => retrier.props)); super(retriers); } } export class Catchers extends PascalCaseJson { constructor(catchers: Catcher[]) { - validateErrorAllAppearsLast(catchers.map(catcher => catcher.props)); + validateErrorAllNotBeforeLast(catchers.map(catcher => catcher.props)); super(catchers); } } @@ -469,7 +470,7 @@ export namespace amazon_states_language { export abstract class BaseState extends PascalCaseJson implements State { constructor(type: StateType, props: any) { - super({ ...props, ...{type: StateType[type]}}); + super({ ...props, ...{ type: StateType[type] } }); } public isTerminal() { @@ -541,7 +542,7 @@ export namespace amazon_states_language { if (props.timeoutSeconds !== undefined && !istoken(props.timeoutSeconds) && !Number.isInteger(props.timeoutSeconds)) { throw new Error(`timeoutSeconds '${props.timeoutSeconds}' is not an integer`); } - if (props.heartbeatSeconds !== undefined && !istoken(props.timeoutSeconds) && !Number.isInteger(props.heartbeatSeconds)) { + if (props.heartbeatSeconds !== undefined && !istoken(props.heartbeatSeconds) && !Number.isInteger(props.heartbeatSeconds)) { throw new Error(`heartbeatSeconds '${props.heartbeatSeconds}' is not an integer`); } if (props.timeoutSeconds !== undefined && props.heartbeatSeconds !== undefined && props.heartbeatSeconds >= props.timeoutSeconds) { @@ -600,7 +601,7 @@ export namespace amazon_states_language { super(props); } - public toJSON(): any { + public toJSON(): { [key: string]: any } { return { Variable: this.props.variable, [ComparisonOperator[this.props.comparisonOperator]]: this.props.value @@ -610,97 +611,97 @@ export namespace amazon_states_language { export class StringEqualsComparisonOperation extends VariableComparisonOperation { constructor(props: VariableComparisonOperationProps) { - super({...props, ...{comparisonOperator: ComparisonOperator.StringEquals}}); + super({ ...props, ...{ comparisonOperator: ComparisonOperator.StringEquals } }); } } export class StringLessThanComparisonOperation extends VariableComparisonOperation { constructor(props: VariableComparisonOperationProps) { - super({...props, ...{comparisonOperator: ComparisonOperator.StringLessThan}}); + super({ ...props, ...{ comparisonOperator: ComparisonOperator.StringLessThan } }); } } export class StringGreaterThanComparisonOperation extends VariableComparisonOperation { constructor(props: VariableComparisonOperationProps) { - super({...props, ...{comparisonOperator: ComparisonOperator.StringGreaterThan}}); + super({ ...props, ...{ comparisonOperator: ComparisonOperator.StringGreaterThan } }); } } export class StringLessThanEqualsComparisonOperation extends VariableComparisonOperation { constructor(props: VariableComparisonOperationProps) { - super({...props, ...{comparisonOperator: ComparisonOperator.StringLessThanEquals}}); + super({ ...props, ...{ comparisonOperator: ComparisonOperator.StringLessThanEquals } }); } } export class StringGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { constructor(props: VariableComparisonOperationProps) { - super({...props, ...{comparisonOperator: ComparisonOperator.StringGreaterThanEquals}}); + super({ ...props, ...{ comparisonOperator: ComparisonOperator.StringGreaterThanEquals } }); } } export class NumericEqualsComparisonOperation extends VariableComparisonOperation { constructor(props: VariableComparisonOperationProps) { - super({...props, ...{comparisonOperator: ComparisonOperator.NumericEquals}}); + super({ ...props, ...{ comparisonOperator: ComparisonOperator.NumericEquals } }); } } export class NumericLessThanComparisonOperation extends VariableComparisonOperation { constructor(props: VariableComparisonOperationProps) { - super({...props, ...{comparisonOperator: ComparisonOperator.NumericLessThan}}); + super({ ...props, ...{ comparisonOperator: ComparisonOperator.NumericLessThan } }); } } export class NumericGreaterThanComparisonOperation extends VariableComparisonOperation { constructor(props: VariableComparisonOperationProps) { - super({...props, ...{comparisonOperator: ComparisonOperator.NumericGreaterThan}}); + super({ ...props, ...{ comparisonOperator: ComparisonOperator.NumericGreaterThan } }); } } export class NumericLessThanEqualsComparisonOperation extends VariableComparisonOperation { constructor(props: VariableComparisonOperationProps) { - super({...props, ...{comparisonOperator: ComparisonOperator.NumericLessThanEquals}}); + super({ ...props, ...{ comparisonOperator: ComparisonOperator.NumericLessThanEquals } }); } } export class NumericGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { constructor(props: VariableComparisonOperationProps) { - super({...props, ...{comparisonOperator: ComparisonOperator.NumericGreaterThanEquals}}); + super({ ...props, ...{ comparisonOperator: ComparisonOperator.NumericGreaterThanEquals } }); } } export class BooleanEqualsComparisonOperation extends VariableComparisonOperation { constructor(props: VariableComparisonOperationProps) { - super({...props, ...{comparisonOperator: ComparisonOperator.BooleanEquals}}); + super({ ...props, ...{ comparisonOperator: ComparisonOperator.BooleanEquals } }); } } export class TimestampEqualsComparisonOperation extends VariableComparisonOperation { constructor(props: VariableComparisonOperationProps) { - super({...props, ...{comparisonOperator: ComparisonOperator.TimestampEquals}}); + super({ ...props, ...{ comparisonOperator: ComparisonOperator.TimestampEquals } }); } } export class TimestampLessThanComparisonOperation extends VariableComparisonOperation { constructor(props: VariableComparisonOperationProps) { - super({...props, ...{comparisonOperator: ComparisonOperator.TimestampLessThan}}); + super({ ...props, ...{ comparisonOperator: ComparisonOperator.TimestampLessThan } }); } } export class TimestampGreaterThanComparisonOperation extends VariableComparisonOperation { constructor(props: VariableComparisonOperationProps) { - super({...props, ...{comparisonOperator: ComparisonOperator.TimestampGreaterThan}}); + super({ ...props, ...{ comparisonOperator: ComparisonOperator.TimestampGreaterThan } }); } } export class TimestampLessThanEqualsComparisonOperation extends VariableComparisonOperation { constructor(props: VariableComparisonOperationProps) { - super({...props, ...{comparisonOperator: ComparisonOperator.TimestampLessThanEquals}}); + super({ ...props, ...{ comparisonOperator: ComparisonOperator.TimestampLessThanEquals } }); } } export class TimestampGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { constructor(props: VariableComparisonOperationProps) { - super({...props, ...{comparisonOperator: ComparisonOperator.TimestampGreaterThanEquals}}); + super({ ...props, ...{ comparisonOperator: ComparisonOperator.TimestampGreaterThanEquals } }); } } @@ -714,27 +715,25 @@ export namespace amazon_states_language { if (props.comparisonOperations.length === 0) { throw new Error('\'comparisonOperations\' is empty. Must be non-empty array of ChoiceRules'); } - super({ - [ComparisonOperator[props.comparisonOperator]]: props.comparisonOperations - }); + super({ [ComparisonOperator[props.comparisonOperator]]: props.comparisonOperations }); } } export class AndComparisonOperation extends ArrayComparisonOperation { constructor(...comparisonOperations: ComparisonOperation[]) { - super({ comparisonOperator: ComparisonOperator.And, comparisonOperations}); + super({ comparisonOperator: ComparisonOperator.And, comparisonOperations }); } } export class OrComparisonOperation extends ArrayComparisonOperation { constructor(...comparisonOperations: ComparisonOperation[]) { - super({ comparisonOperator: ComparisonOperator.Or, comparisonOperations}); + super({ comparisonOperator: ComparisonOperator.Or, comparisonOperations }); } } export class NotComparisonOperation extends ComparisonOperation { constructor(comparisonOperation: ComparisonOperation) { - super({ [ComparisonOperator[ComparisonOperator.Not]]: comparisonOperation}); + super({ [ComparisonOperator[ComparisonOperator.Not]]: comparisonOperation }); } } @@ -747,7 +746,7 @@ export namespace amazon_states_language { */ export class ChoiceRule extends PascalCaseJson { constructor(props: ChoiceRuleProps) { - super({...props.comparisonOperation.props, next: props.next}); + super({ ...props.comparisonOperation.props, next: props.next }); } } @@ -803,7 +802,7 @@ export namespace amazon_states_language { } public next() { - return this.props.choices.nextStates().concat([this.props.default]); + return this.props.choices.nextStates.concat([this.props.default]); } } @@ -848,8 +847,7 @@ export namespace amazon_states_language { } } - export interface SucceedStateProps extends Commentable, InputOutputPathFields { - } + export interface SucceedStateProps extends Commentable, InputOutputPathFields { } /** * Terminate the state machine successfully. @@ -866,7 +864,7 @@ export namespace amazon_states_language { } } - export interface FailStateProps extends BaseState, Commentable { + export interface FailStateProps extends Commentable { /** * An Error Name used for error handling in a {@link Retrier} or {@link Catcher}, * or for operational/diagnostic purposes. @@ -902,13 +900,16 @@ export namespace amazon_states_language { } export class Branches extends PascalCaseJson { - constructor(...branches: Branch[]) { + private readonly branches: Branch[]; + + constructor(branches: Branch[]) { super(branches); + this.branches = branches; } public stateNames(): Array { const names: Array = []; - this.props.branches.forEach((branch: Branch) => branch.stateNames().forEach(name => names.push(name))); + this.branches.forEach(branch => branch.stateNames().forEach(name => names.push(name))); return names; } } @@ -919,13 +920,18 @@ export namespace amazon_states_language { * {@link https://states-language.net/spec.html#parallel-state} */ export class ParallelState extends BaseState { + private readonly branches: Branches; + constructor(props: ParallelStateProps) { requireNextOrEnd(props); super(StateType.Parallel, props); + this.branches = props.branches; } public stateNames(): Array { - return this.props.branches.stateNames(); + return this.branches.stateNames(); } } -} \ No newline at end of file +} + +export import asl = AmazonStatesLanguage; diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions/lib/index.ts index cac43fa1099d2..da7ea91994b63 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/index.ts @@ -1,4 +1,4 @@ export * from './amazon-states-language'; // AWS::StepFunctions CloudFormation Resources: -export * from './stepfunctions.generated'; \ No newline at end of file +export * from './stepfunctions.generated'; diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts.bk b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts.bk deleted file mode 100644 index ee08aa1339797..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts.bk +++ /dev/null @@ -1,732 +0,0 @@ -// import { stepfunctions } from '@aws-cdk/resources'; -// import { Construct, Token, tokenAwareJsonify, Arn, Stack } from '@aws-cdk/core'; -// import { isUndefined } from 'util'; - - -// export interface BranchProps extends Commentable { -// } - -// export interface StateMachineProps extends Commentable { -// roleArn: Arn, -// version?: string, -// timeoutSeconds?: number, -// stateMachineName?: string -// } - -// export interface IStateMachine { -// startAt(state: IState): IStateMachine -// next(state: IState): IStateMachine -// } - -// export class Branch implements Jsonable, IStateMachine { -// private startState?: IState; -// private head?: IState; -// private states: IState[] = []; -// private readonly props: BranchProps; - -// constructor(props: BranchProps = {}) { -// this.props = props; -// } - -// startAt(state: IState) { -// this.startState = this.head = state; -// return this; -// } - -// next(state: IState) { -// if (isUndefined(this.head)) { -// throw new Error("The first state must be added with startAt()"); -// } -// this.head.next = state; -// this.head = state; -// this.states.push(state); -// return this; -// } - -// toJson() { -// return { -// Comment: this.props && this.props.comment, -// States: Array.from(this.states).reduce( -// (map: { [index: string] : IState }, state) => { -// map[state.name] = state; -// return map; -// }, {} -// ), -// StartAt: (this.startState || this.states[0]).name -// } -// } -// } - -// export class StateMachine extends Construct implements IStateMachine { -// private readonly branch: Branch; -// constructor(parent: Construct, name: string, props: StateMachineProps) { -// super(parent, name); -// this.branch = new Branch({comment: props.comment}) - -// new stepfunctions.StateMachineResource(this, 'Resource', { -// definitionString: new Token(() => { -// return tokenAwareJsonify({ -// ...{ -// Version: props && props.version, -// TimeoutSeconds: props && props.timeoutSeconds, -// }, -// ...this.branch.toJson() -// }) -// }), -// roleArn: props.roleArn, -// stateMachineName: props.stateMachineName -// }) -// } - -// validate(): string[] { -// return [] -// } - -// startAt(state: IState) { -// this.branch.startAt(state); -// return this; -// } - -// next(state: IState) { -// this.branch.next(state); -// return this; -// } -// } - -// export enum StateType { -// Pass, -// Task, -// Choice, -// Wait, -// Succeed, -// Fail, -// Parallel -// } - -// export interface IState { -// readonly type: StateType; -// readonly name: string -// readonly comment?: string; - -// next?: IState; - -// toJson(): any -// } - -// export abstract class State implements IState { -// public readonly type: StateType -// public readonly name: string - -// constructor(type: StateType, name: string) { -// this.type = type -// this.name = name -// } - -// public abstract toJson(): any -// } - -// export interface Commentable { -// comment?: string -// } - -// export interface StateProps extends Commentable { -// name: string; -// } - -// export interface NextStateProps { -// next?: State; -// } - -// export interface InputOutputPathStateProps { -// inputPath?: string; -// outputPath?: string; -// } - -// export interface ResultPathStateProps { -// resultPath?: string; -// } - -// export enum ErrorCode { -// ALL = "States.ALL", -// Timeout = "States.Timeout", -// TaskFailed = "States.TaskFailed", -// Permissions = "States.Permissions", -// ResultPathMatchFailure = "States.ResultPathMatchFailure", -// BranchFailed = "States.BranchFailed", -// NoChoiceMatched = "States.NoChoiceMatched" -// } - -// export interface WithErrors { -// errorEquals: (string | ErrorCode)[]; -// } - -// export interface Retrier extends WithErrors { -// intervalSeconds?: number; -// maxAttempts?: number; -// backoffRate?: number; -// } - -// export interface Catcher extends WithErrors { -// next: IState; -// resultPath?: string; -// } - -// export interface RetryCatchStateProps { -// retry?: Retrier[]; -// catch?: Catcher[]; -// } - -// export interface PassStateProps extends StateProps, NextStateProps, InputOutputPathStateProps, ResultPathStateProps { -// result?: any; -// } - -// export class PassState extends State { -// private readonly props: PassStateProps; - -// constructor(props: PassStateProps) { -// super(StateType.Pass, props.name) -// this.props = props; -// } - -// public toJson(): any { -// return { -// ...stateJson(this.props, this.type), -// ...nextJson(this.props), -// ...inputOutputJson(this.props), -// ...resultPathJson(this.props), -// ...{ "Result": this.props.result } -// } -// } -// } - -// function stateJson(props: StateProps, type: StateType): any { -// return { -// "Type": type, -// "Name": props.name, -// "Comment": props.comment -// } -// } - -// function nextJson(props: NextStateProps): any { -// return (!isUndefined(props.next)) ? { "Next": props.next.name } : { "End": true } -// } - -// function inputOutputJson(props: InputOutputPathStateProps): any { -// return { -// "InputPath": props.inputPath, -// "OutputPath": props.outputPath -// } -// } - -// function resultPathJson(props: ResultPathStateProps): any { -// return { -// "ResultPath": props.resultPath -// } -// } - -// function retryJson(props: RetryCatchStateProps): any { -// var out : { [index: string] : any} = {} -// if (props.retry) { -// out["Retry"] = props.retry.map(retrierJson) -// } -// if (props.catch) { -// out["Catch"] = props.catch.map(catcherJson) -// } -// return out; -// } - -// function retrierJson(retrier: Retrier): any { -// return { -// "ErrorEquals": retrier.errorEquals, -// "IntervalSeconds": retrier.intervalSeconds, -// "MaxAttempts": retrier.maxAttempts, -// "BackoffRate": retrier.backoffRate -// } -// } - -// function catcherJson(catcher: Catcher): any { -// return { -// "ErrorEquals": catcher.errorEquals, -// "Next": catcher.next.name, -// "ResultPath": catcher.resultPath -// } -// } - -// export interface TaskStateProps extends StateProps, InputOutputPathStateProps, ResultPathStateProps, NextStateProps, RetryCatchStateProps { -// resource: string; -// timeoutSeconds?: number; -// heartbeatSeconds?: number; -// } - -// export class TaskState extends State { -// private readonly props: TaskStateProps; - -// constructor(props: TaskStateProps) { -// super(StateType.Task, props.name) -// this.props = props; -// } - -// public toJson(): any { -// return { -// ...stateJson(this.props, this.type), -// ...nextJson(this.props), -// ...inputOutputJson(this.props), -// ...resultPathJson(this.props), -// ...retryJson(this.props), -// ...{ -// "Resource": this.props.resource, -// "TimeoutSeconds": this.props.timeoutSeconds, -// "HeartbeatSeconds": this.props.heartbeatSeconds -// } -// } -// } -// } - -// export class ChoiceRules implements Jsonable { -// public readonly choiceRules: ChoiceRule[]; - -// constructor(...choiceRules: ChoiceRule[]) { -// this.choiceRules = choiceRules; -// } - -// toJson() { -// return this.choiceRules.map(choiceRule => choiceRule.toJson()) -// } -// } - -// export interface ChoiceRuleProps { -// comparisonOperation: IComparisonOperation; -// next: IState; -// } - -// export class ChoiceRule { -// public readonly comparisonOperation: IComparisonOperation; -// public readonly next: IState; - -// constructor(props: ChoiceRuleProps) { -// this.comparisonOperation = props.comparisonOperation; -// this.next = props.next; -// } - -// toJson() { -// return { -// ...this.comparisonOperation.toJson(), -// ...{"Next": this.next.name} -// } -// } -// } - -// export interface Jsonable { -// toJson(): any -// } - -// export interface IComparisonOperation extends Jsonable { -// operation: string; -// value: any; -// } - -// export class ComparisonOperations implements Jsonable { -// public readonly comparisons: IComparisonOperation[]; - -// constructor(...comparisons: IComparisonOperation[]) { -// this.comparisons = comparisons -// } - -// toJson() { -// return this.comparisons.map(comparison => comparison.toJson()) -// } -// } - -// export abstract class ComparisonOperation implements IComparisonOperation { -// public readonly operation: string; -// public readonly value: any; -// public readonly variable?: string - -// constructor(operation: string, value: any, variable?: string) { -// this.operation = operation; -// this.value = value -// this.variable = variable -// } - -// public toJson() { -// return { -// [this.operation]: (typeof this.value['toJson'] === 'function') ? this.value.toJson() : this.value, -// "Variable": this.variable -// } -// } -// } - -// export class StringEqualsOperation extends ComparisonOperation { -// constructor(variable: string, value: string) { -// super("StringEquals", value, variable); -// } -// } - -// export class StringLessThanOperation extends ComparisonOperation { -// constructor(variable: string, value: string) { -// super("StringLessThan", value, variable); -// } -// } - -// export class StringGreaterThanOperation extends ComparisonOperation { -// constructor(variable: string, value: string) { -// super("StringGreaterThan", value, variable); -// } -// } - -// export class StringLessThanEqualsOperation extends ComparisonOperation { -// constructor(variable: string, value: string) { -// super("StringLessThanEquals", value, variable); -// } -// } - -// export class StringGreaterThanEqualsOperation extends ComparisonOperation { -// constructor(variable: string, value: string) { -// super("StringGreaterThanEquals", value, variable); -// } -// } - -// export class NumericEqualsOperation extends ComparisonOperation { -// constructor(variable: string, value: number) { -// super("NumericEquals", value, variable); -// } -// } - -// export class NumericLessThanOperation extends ComparisonOperation { -// constructor(variable: string, value: number) { -// super("NumericLessThan", value, variable); -// } -// } - -// export class NumericGreaterThanOperation extends ComparisonOperation { -// constructor(variable: string, value: number) { -// super("NumericGreaterThan", value, variable); -// } -// } - -// export class NumericLessThanEqualsOperation extends ComparisonOperation { -// constructor(variable: string, value: number) { -// super("NumericLessThanEquals", value, variable); -// } -// } - -// export class NumericGreaterThanEqualsOperation extends ComparisonOperation { -// constructor(variable: string, value: number) { -// super("NumericGreaterThanEquals", value, variable); -// } -// } - -// export class BooleanEqualsOperation extends ComparisonOperation { -// constructor(variable: string, value: boolean) { -// super("BooleanEquals", value, variable); -// } -// } - -// export class TimestampEqualsOperation extends ComparisonOperation { -// constructor(variable: string, value: string) { -// super("TimestampEquals", value, variable); -// } -// } - -// export class TimestampLessThanOperation extends ComparisonOperation { -// constructor(variable: string, value: string) { -// super("TimestampLessThan", value, variable); -// } -// } - -// export class TimestampGreaterThanOperation extends ComparisonOperation { -// constructor(variable: string, value: string) { -// super("TimestampGreaterThan", value, variable); -// } -// } - -// export class TimestampLessThanEqualsOperation extends ComparisonOperation { -// constructor(variable: string, value: string) { -// super("TimestampLessThanEquals", value, variable); -// } -// } - -// export class TimestampGreaterThanEqualsOperation extends ComparisonOperation { -// constructor(variable: string, value: string) { -// super("TimestampGreaterThanEquals", value, variable); -// } -// } - -// export class AndOperation extends ComparisonOperation { -// constructor(...comparisons: IComparisonOperation[]) { -// super("And", new ComparisonOperations(...comparisons)); -// } -// } - -// export class OrOperation extends ComparisonOperation { -// constructor(...comparisons: IComparisonOperation[]) { -// super("Or", new ComparisonOperations(...comparisons)); -// } -// } - -// export class NotOperation extends ComparisonOperation { -// constructor(comparison: IComparisonOperation) { -// super("Not", comparison); -// } -// } - -// export interface ChoiceStateProps extends StateProps, InputOutputPathStateProps { -// choices: ChoiceRules; -// default?: State; -// } - -// export class ChoiceState extends State { -// private readonly props: ChoiceStateProps; - -// constructor(props: ChoiceStateProps) { -// super(StateType.Choice, props.name) -// this.props = props; -// } - -// public toJson(): any { -// return { -// ...stateJson(this.props, this.type), -// ...inputOutputJson(this.props), -// ...{ "Choices": this.props.choices.toJson() } -// } -// } -// } - -// export interface WaitStateProps extends StateProps, InputOutputPathStateProps, NextStateProps { -// seconds?: number; -// secondsPath?: string; -// timestamp?: string; -// timestampPath?: string; -// } - -// export class WaitState extends State { -// private readonly props: WaitStateProps; - -// constructor(sm: StateMachine, props: WaitStateProps) { -// super(sm, StateType.Wait, props.name); -// this.props = props; -// } - -// toJson() { -// return { -// ...stateJson(this.props, this.type), -// ...inputOutputJson(this.props), -// ...nextJson(this.props), -// ...{ -// "Seconds": this.props.seconds, -// "SecondsPath": this.props.secondsPath, -// "Timestamp": this.props.timestamp, -// "TimestampPath": this.props.timestampPath -// } -// } -// } -// } - -// export interface SucceedStateProps extends StateProps, InputOutputPathStateProps { -// } - -// export class SucceedState extends State { -// constructor(sm: StateMachine, props: SucceedStateProps) { -// super(sm, StateType.Succeed, props.name); -// } -// } - -// export interface FailStateProps extends StateProps { -// error: string; -// cause: string; -// } - -// export class FailState extends State { -// constructor(props: FailStateProps) { -// super(StateType.Fail, props.name); -// } -// } - -// export interface ParallelStateProps extends StateProps, InputOutputPathStateProps, ResultPathStateProps, NextStateProps, RetryCatchStateProps { -// branches: Branch[] -// } - -// export class ParallelState extends State { -// private readonly props: ParallelStateProps; - -// constructor(props: ParallelStateProps) { -// super(StateType.Parallel, props.name); -// this.props = props; -// } - -// toJson() { -// return ""; -// } -// } - -// // var n = new PassState(); -// // new ChoiceState( -// // [ -// // { -// // comparisonOperation: new NotOperation( -// // { -// // comparisonOperation: new StringEqualsOperation("$.type", "Private") -// // } -// // ), -// // next: new TaskState("arn:aws:lambda:us-east-1:123456789012:function:Foo", {next: n}) -// // }, -// // { -// // comparisonOperation: new AndOperation( -// // [ -// // { -// // comparisonOperation: new NumericGreaterThanEqualsOperation("$.value", 20) -// // }, -// // { -// // comparisonOperation: new NumericLessThanOperation("$.value", 30) -// // } -// // ] -// // ), -// // next: new TaskState("arn:aws:lambda:us-east-1:123456789012:function:Foo", {next: n}) -// // } -// // ], -// // { -// // default: new FailState({ -// // error: "ErrorA", -// // cause: "Kaiju Attack" -// // }) -// // } -// // ) - - -// var stack = new Stack() - -// var sm = new StateMachine(stack, "StateMachine", {roleArn: Arn.fromComponents(Arn.parse("arn::foo"))}) - -// new TaskState(sm, { -// name: "Hello World", -// resource: "arn:aws:lambda:us-east-1:123456789012:function:HelloWorld", -// }) -// new TaskState(sm, { name: "X", -// resource: "arn:aws:swf:us-east-1:123456789012:task:X", -// next: new PassState(sm, {name: "Y"}), -// retry: [ -// { -// errorEquals: [ "ErrorA", "ErrorB" ], -// intervalSeconds: 1, -// backoffRate: 2, -// maxAttempts: 2 -// }, -// { -// errorEquals: [ "ErrorC" ], -// intervalSeconds: 5 -// } -// ], -// catch: [ -// { -// errorEquals: ErrorCode.ALL, -// next: new PassState(sm, { name: "Z" }) -// } -// ] -// }) - -// var sm = new StateMachine(stack, "StateMachine", {roleArn: Arn.fromComponents(Arn.parse("arn::foo"))}) -// let nextstate = new PassState(sm, { name: "NextState" }) -// sm.startAt( -// new ChoiceState(sm, { -// name: "ChoiceStateX", -// choices: new ChoiceRules( -// new ChoiceRule({ -// comparisonOperation: new NotOperation( -// new StringEqualsOperation("$.type", "Private") -// ), -// next: new TaskState({ -// name: "Public", -// resource: "arn:aws:lambda:us-east-1:123456789012:function:Foo", -// next: nextstate -// }) -// }), -// new ChoiceRule({ -// comparisonOperation: new AndOperation( -// new NumericGreaterThanEqualsOperation("$.value", 20), -// new NumericLessThanOperation("$.value", 30) -// ), -// next: new TaskState({ -// name: "ValueInTwenties", -// resource: "arn:aws:lambda:us-east-1:123456789012:function:Bar", -// next: nextstate -// }) -// }) -// ), -// default: new FailState({ -// name: "DefaultState", -// error: "Error", -// cause: "No Matches!" -// }) -// }) -// ) -// // new TaskState({ -// // resource: "some-lambda-arn", -// // next: new PassState({ -// // result: "foo", -// // resultPath: "$.var" -// // }) -// // }) - -// // let lookupAddress = new TaskState("arn:aws:lambda:us-east-1:123456789012:function:AddressFinder") -// // let lookupPhone = new TaskState("arn:aws:lambda:us-east-1:123456789012:function:PhoneFinder") -// // new ParallelState(sm, { -// // name: "LookupCustomerInfo", -// // branches: [ -// // new Branch().startAt(lookupPhone), -// // new Branch().startAt(lookupAddress).addState(lookupPhone) -// // ] -// // }) - -// new StateMachine(stack, "", {roleArn: Arn.fromComponents({resource: "", service: ""})}) -// .startAt( -// new ParallelState({ -// name: "FunWithMath", -// branches: [ -// new Branch().startAt( -// new TaskState({ -// name: "Add", -// resource: "foo" -// }) -// ), -// new Branch().startAt( -// new TaskState({ -// name: "Subtract", -// resource: "bar" -// }) -// ) -// ] -// }) -// ); - -// let iteratorTask = new TaskState({ -// name: "Iterator", -// resource: "foo", -// resultPath: "$.iterator" -// }) - -// new StateMachine(stack, "", {roleArn: Arn.fromComponents({resource: "foo", service:" bar"})}) -// .startAt( -// new PassState({ -// name: "ConfigureCount", -// result: { "count": 10, "index": 0, "step": 1 }, -// resultPath: "$.iterator" -// }) -// ) -// .next(iteratorTask) -// .next( -// new ChoiceState({ -// name: "IsCountReached", -// choices: new ChoiceRules( -// new ChoiceRule({ -// comparisonOperation: new BooleanEqualsOperation("$.iterator.continue", true), -// next: new PassState({ -// name: "ExampleWork", -// comment: "Your application logic, to run a specific number of times", -// result: { "success": true }, -// resultPath: "$.result", -// next: iteratorTask -// }) -// }) -// ) -// }) -// ) \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/util.ts b/packages/@aws-cdk/aws-stepfunctions/lib/util.ts index 98d40d13f517e..6a46b09e5f7de 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/util.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/util.ts @@ -1,17 +1,9 @@ export function requireOneOf(props: { [name: string]: any }, names: string[]) { if (names.map(name => name in props).filter(x => x === true).length !== 1) { - throw new Error(`${props} must specify exactly one of: ${names}`); + throw new Error(`${JSON.stringify(props)} must specify exactly one of: ${names}`); } } -export function requireAll(props: { [name: string]: any }, names: string[]) { - if (names.map(name => name in props).filter(x => x === false).length > 0) { - throw new Error(`${props} must specify exactly all of: ${names}`); - } +export function requireNextOrEnd(props: any) { + requireOneOf(props, ['next', 'end']); } - -export function requirePositiveInteger(props: { [name: string]: any }, name: string) { - if (!Number.isInteger(props[name]) || props[name] < 0) { - throw new Error(`${name} must be a postive integer`); - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/package-lock.json b/packages/@aws-cdk/aws-stepfunctions/package-lock.json deleted file mode 100644 index 1cd38e72da675..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions/package-lock.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "requires": true, - "lockfileVersion": 1, - "dependencies": { - "aws-sdk": { - "version": "2.266.1", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.266.1.tgz", - "integrity": "sha512-b8lisloCETh0Fx0il540i+Hbgf3hyegQ6ezoJFggfc1HIbqzvIjVJYJhOsYl1fL1o+iMUaVU4ZH8cSyoMFR2Tw==", - "requires": { - "buffer": "4.9.1", - "events": "1.1.1", - "ieee754": "1.1.8", - "jmespath": "0.15.0", - "querystring": "0.2.0", - "sax": "1.2.1", - "url": "0.10.3", - "uuid": "3.1.0", - "xml2js": "0.4.17" - } - }, - "base64-js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", - "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" - }, - "buffer": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", - "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" - }, - "ieee754": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", - "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "jmespath": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", - "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" - }, - "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" - }, - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" - }, - "sax": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", - "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" - }, - "url": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", - "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - } - }, - "uuid": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" - }, - "xml2js": { - "version": "0.4.17", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.17.tgz", - "integrity": "sha1-F76T6q4/O3eTWceVtBlwWogX6Gg=", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "^4.1.0" - } - }, - "xmlbuilder": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.2.1.tgz", - "integrity": "sha1-qlijBBoGb5DqoWwvU4n/GfP0YaU=", - "requires": { - "lodash": "^4.0.0" - } - } - } -} diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts index 2314c934728e9..7c56ca4098b47 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts @@ -1,11 +1,14 @@ import { Test } from 'nodeunit'; +import { asl } from './../lib'; -import { amazon_states_language as asl } from '../lib'; +function roundtrip(obj: any) { + return JSON.parse(JSON.stringify(obj)); +} export = { - 'Hello World'(test: Test) { + 'Hello World example state machine'(test: Test) { test.deepEqual( - JSON.parse(JSON.stringify( + JSON.parse( new asl.StateMachine({ comment: "A simple minimal example of the States language", startAt: "Hello World", @@ -15,8 +18,8 @@ export = { end: true }) }) - }) - )), + }).definitionString() + ), { Comment: "A simple minimal example of the States language", StartAt: "Hello World", @@ -31,9 +34,9 @@ export = { ); test.done(); }, - 'Complex retry scenarios'(test: Test) { + 'Models Complex retry scenarios example'(test: Test) { test.deepEqual( - JSON.parse(JSON.stringify( + roundtrip( new asl.TaskState({ resource: "arn:aws:swf:us-east-1:123456789012:task:X", next: "Y", @@ -51,41 +54,87 @@ export = { ]), catch: new asl.Catchers([ new asl.Catcher({ - errorEquals: [ asl.ErrorCode.ALL ], + errorEquals: [asl.ErrorCode.ALL], next: "Z" }) ]) }) - )), + ), { Type: "Task", Resource: "arn:aws:swf:us-east-1:123456789012:task:X", Next: "Y", Retry: [ - { - ErrorEquals: [ "ErrorA", "ErrorB" ], - IntervalSeconds: 1, - BackoffRate: 2, - MaxAttempts: 2 - }, - { - ErrorEquals: [ "ErrorC" ], - IntervalSeconds: 5 - } + { + ErrorEquals: ["ErrorA", "ErrorB"], + IntervalSeconds: 1, + BackoffRate: 2, + MaxAttempts: 2 + }, + { + ErrorEquals: ["ErrorC"], + IntervalSeconds: 5 + } ], Catch: [ - { - ErrorEquals: [ "States.ALL" ], - Next: "Z" - } + { + ErrorEquals: ["States.ALL"], + Next: "Z" + } ] } ); test.done(); }, - 'Choice state'(test: Test) { + 'Pass state example'(test: Test) { + test.deepEqual( + roundtrip( + new asl.PassState({ + result: { + "x-datum": 0.381018, + "y-datum": 622.2269926397355 + }, + resultPath: "$.coords", + next: "End" + }) + ), + { + Type: "Pass", + Result: { + "x-datum": 0.381018, + "y-datum": 622.2269926397355 + }, + ResultPath: "$.coords", + Next: "End" + } + ); + test.done(); + }, + 'Task state example'(test: Test) { + test.deepEqual( + roundtrip( + new asl.TaskState({ + comment: "Task State example", + resource: "arn:aws:swf:us-east-1:123456789012:task:HelloWorld", + next: "NextState", + timeoutSeconds: 300, + heartbeatSeconds: 60 + }) + ), + { + Comment: "Task State example", + Type: "Task", + Resource: "arn:aws:swf:us-east-1:123456789012:task:HelloWorld", + Next: "NextState", + TimeoutSeconds: 300, + HeartbeatSeconds: 60 + } + ); + test.done(); + }, + 'Choice state example'(test: Test) { test.deepEqual( - JSON.parse(JSON.stringify( + roundtrip( new asl.ChoiceState({ choices: new asl.ChoiceRules( new asl.ChoiceRule({ @@ -113,41 +162,88 @@ export = { ), default: "DefaultState" }) - )), + ), { - Type : "Choice", + Type: "Choice", Choices: [ - { - Not: { - Variable: "$.type", - StringEquals: "Private" - }, - Next: "Public" - }, - { - And: [ - { - Variable: "$.value", - NumericGreaterThanEquals: 20 - }, - { - Variable: "$.value", - NumericLessThan: 30 - } - ], - Next: "ValueInTwenties" - } + { + Not: { + Variable: "$.type", + StringEquals: "Private" + }, + Next: "Public" + }, + { + And: [ + { + Variable: "$.value", + NumericGreaterThanEquals: 20 + }, + { + Variable: "$.value", + NumericLessThan: 30 + } + ], + Next: "ValueInTwenties" + } ], Default: "DefaultState" } ); test.done(); }, - 'Parallel state'(test: Test) { + 'Wait state examples'(test: Test) { + test.deepEqual( + roundtrip(new asl.WaitState({ seconds: 10, next: "NextState" })), + { + Type: "Wait", + Seconds: 10, + Next: "NextState" + } + ); + test.deepEqual( + roundtrip(new asl.WaitState({ timestamp: "2016-03-14T01:59:00Z", next: "NextState" })), + { + Type: "Wait", + Timestamp: "2016-03-14T01:59:00Z", + Next: "NextState" + } + ); test.deepEqual( - JSON.parse(JSON.stringify( + roundtrip(new asl.WaitState({ timestampPath: "$.expirydate", next: "NextState" })), + { + Type: "Wait", + TimestampPath: "$.expirydate", + Next: "NextState" + } + ); + test.done(); + }, + 'Succeed state example'(test: Test) { + test.deepEqual(roundtrip(new asl.SucceedState()), { Type: "Succeed" }); + test.done(); + }, + 'Fail state example'(test: Test) { + test.deepEqual( + roundtrip( + new asl.FailState({ + error: "ErrorA", + cause: "Kaiju attack" + }) + ), + { + Type: "Fail", + Error: "ErrorA", + Cause: "Kaiju attack" + } + ); + test.done(); + }, + 'Parallel state example'(test: Test) { + test.deepEqual( + roundtrip( new asl.ParallelState({ - branches: new asl.Branches( + branches: new asl.Branches([ new asl.Branch({ startAt: "LookupAddress", states: new asl.States({ @@ -166,37 +262,128 @@ export = { }) }) }) - ), + ]), next: "NextState" }) - )), + ), { Type: "Parallel", Branches: [ - { - StartAt: "LookupAddress", - States: { - LookupAddress: { - Type: "Task", - Resource: "arn:aws:lambda:us-east-1:123456789012:function:AddressFinder", - End: true - } + { + StartAt: "LookupAddress", + States: { + LookupAddress: { + Type: "Task", + Resource: "arn:aws:lambda:us-east-1:123456789012:function:AddressFinder", + End: true + } + } + }, + { + StartAt: "LookupPhone", + States: { + LookupPhone: { + Type: "Task", + Resource: "arn:aws:lambda:us-east-1:123456789012:function:PhoneFinder", + End: true + } + } } - }, - { - StartAt: "LookupPhone", - States: { - LookupPhone: { - Type: "Task", - Resource: "arn:aws:lambda:us-east-1:123456789012:function:PhoneFinder", - End: true - } - } - } ], Next: "NextState" } ); test.done(); + }, + 'Validates state names are unique'(test: Test) { + test.throws(() => { + new asl.StateMachine({ + startAt: "foo", + states: new asl.States({ + foo: new asl.PassState({ next: "bar" }), + bar: new asl.PassState({ next: "bat" }), + bat: new asl.ParallelState({ + branches: new asl.Branches([ + new asl.Branch({ + startAt: "foo", + states: new asl.States({ + foo: new asl.PassState({ end: true }) + }) + }) + ]), + end: true + }) + }) + }); + }); + test.done(); + }, + 'Validates startAt is in states'(test: Test) { + test.throws(() => { + new asl.StateMachine({ + startAt: "notFoo", + states: new asl.States({ + foo: new asl.SucceedState() + }) + }); + }); + test.throws(() => { + new asl.Branch({ + startAt: "notFoo", + states: new asl.States({ + foo: new asl.SucceedState() + }) + }); + }); + test.done(); + }, + 'Validates state names aren\'t too long'(test: Test) { + test.throws(() => { + new asl.States({ + [new Array(200).join('x')]: new asl.SucceedState() + }); + }); + test.done(); + }, + 'Validates next states are known'(test: Test) { + test.throws(() => { + new asl.States({ + foo: new asl.PassState({ next: "unknown" }) + }); + }); + test.done(); + }, + 'Validates Error.ALL appears alone'(test: Test) { + test.throws(() => { + new asl.Retrier({ + errorEquals: ["a", "b", asl.ErrorCode.ALL, "c"] + }); + }); + test.done(); + }, + 'Validates error names'(test: Test) { + test.throws(() => { + new asl.Retrier({ + errorEquals: ["States.MY_ERROR"] + }); + }); + test.done(); + }, + 'Valdiate Error.ALL must appear last'(test: Test) { + test.throws(() => { + new asl.Retriers([ + new asl.Retrier({ errorEquals: ["SomeOtherError", "BeforeERROR.ALL"] }), + new asl.Retrier({ errorEquals: [asl.ErrorCode.ALL] }), + new asl.Retrier({ errorEquals: ["SomeOtherError", "AfterERROR.ALL"] }) + ]); + }); + test.throws(() => { + new asl.Catchers([ + new asl.Retrier({ errorEquals: ["SomeOtherError", "BeforeERROR.ALL"] }), + new asl.Catcher({ errorEquals: [asl.ErrorCode.ALL], next: "" }), + new asl.Catcher({ errorEquals: ["SomeOtherError", "AfterERROR.ALL"], next: "" }) + ]); + }); + test.done(); } }; diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.stepfunctions.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.stepfunctions.ts deleted file mode 100644 index db4c843199541..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.stepfunctions.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Test, testCase } from 'nodeunit'; - -exports = testCase({ - notTested(test: Test) { - test.ok(true, 'No tests are specified for this package.'); - test.done(); - } -}); diff --git a/packages/aws-cdk/package-lock.json b/packages/aws-cdk/package-lock.json index 39ea89bf30562..419d3693aec45 100644 --- a/packages/aws-cdk/package-lock.json +++ b/packages/aws-cdk/package-lock.json @@ -1281,6 +1281,15 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", "requires": { + "camelcase": "^4.1.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + } + } }, "zip-stream": { "version": "1.2.0", From a7deb6929f15d83926e419f8f1503b0b0ca81ec9 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 28 Aug 2018 16:34:37 +0200 Subject: [PATCH 06/29] WIP --- .../aws-stepfunctions/lib/activity.ts | 16 ++++++ .../lib/{amazon-states-language.ts => asl.ts} | 23 +++------ .../@aws-cdk/aws-stepfunctions/lib/index.ts | 4 +- .../aws-stepfunctions/lib/state-machine.ts | 50 +++++++++++++++++++ .../@aws-cdk/aws-stepfunctions/package.json | 3 +- 5 files changed, 78 insertions(+), 18 deletions(-) create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/activity.ts rename packages/@aws-cdk/aws-stepfunctions/lib/{amazon-states-language.ts => asl.ts} (98%) create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts b/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts new file mode 100644 index 0000000000000..efe14fc5b1b14 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts @@ -0,0 +1,16 @@ +import cdk = require('@aws-cdk/cdk'); + +import { cloudformation } from './stepfunctions.generated'; + +export interface ActivityProps { + /** + * The name for this activity. + * + * The name is required. + */ + activityName: string; +} + +export class Activity extends cdk.Construct { + +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/amazon-states-language.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl.ts similarity index 98% rename from packages/@aws-cdk/aws-stepfunctions/lib/amazon-states-language.ts rename to packages/@aws-cdk/aws-stepfunctions/lib/asl.ts index ee877dcc27d88..4568443c063e6 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/amazon-states-language.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/asl.ts @@ -1,13 +1,9 @@ -import { istoken, Token, tokenAwareJsonify } from "@aws-cdk/cdk"; +import { Token, CloudFormationJSON } from "@aws-cdk/cdk"; import { isString } from "util"; import { requireNextOrEnd, requireOneOf } from "./util"; -/** - * Models the Amazon States Language - * - * {@link https://states-language.net/spec.html} - */ -export namespace AmazonStatesLanguage { +export namespace asl { + /** * Converts all keys to PascalCase when serializing to JSON. */ @@ -41,7 +37,7 @@ export namespace AmazonStatesLanguage { * * {@link https://states-language.net/spec.html#toplevelfields} */ - version?: string | Token, + version?: string, /** * The maximum number of seconds the machine is allowed to run. @@ -52,7 +48,7 @@ export namespace AmazonStatesLanguage { * * {@link https://states-language.net/spec.html#toplevelfields} */ - timeoutSeconds?: number | Token + timeoutSeconds?: number, } /** @@ -60,9 +56,6 @@ export namespace AmazonStatesLanguage { */ export class StateMachine extends PascalCaseJson { constructor(props: StateMachineProps) { - if (props.timeoutSeconds !== undefined && !istoken(props.timeoutSeconds) && !Number.isInteger(props.timeoutSeconds)) { - throw new Error("timeoutSeconds must be an integer"); - } if (isString(props.startAt) && !props.states.hasState(props.startAt)) { throw new Error(`Specified startAt state '${props.startAt}' does not exist in states map`); } @@ -81,7 +74,7 @@ export namespace AmazonStatesLanguage { */ // tslint:enable:max-line-length public definitionString() { - return tokenAwareJsonify(this.toJSON()); + return CloudFormationJSON.stringify(this.toJSON()); } } @@ -932,6 +925,4 @@ export namespace AmazonStatesLanguage { return this.branches.stateNames(); } } -} - -export import asl = AmazonStatesLanguage; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions/lib/index.ts index da7ea91994b63..dd0414620f109 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/index.ts @@ -1,4 +1,6 @@ -export * from './amazon-states-language'; +export * from './activity'; +export * from './asl'; +export * from './state-machine'; // AWS::StepFunctions CloudFormation Resources: export * from './stepfunctions.generated'; diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts new file mode 100644 index 0000000000000..c76eec3e48e73 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts @@ -0,0 +1,50 @@ +import iam = require('@aws-cdk/aws-iam'); +import cdk = require('@aws-cdk/cdk'); + +import { asl } from './asl'; +import { cloudformation, StateMachineName } from './stepfunctions.generated'; + +export interface StateMachineProps { + /** + * A name for the state machine + * + * @default A name is automatically generated + */ + stateMachineName?: string; + + /** + * The definition of this state machine + */ + definition: asl.StateMachine; + + /** + * The execution role for the state machine service + * + * @default A role is automatically created + */ + role?: iam.Role; +} + +/** + * Define a StepFunctions State Machine + */ +export class StateMachine extends cdk.Construct { + public readonly role: iam.Role; + public readonly stateMachineName: StateMachineName; + + constructor(parent: cdk.Construct, id: string, props: StateMachineProps) { + super(parent, id); + + this.role = props.role || new iam.Role(this, 'Role', { + assumedBy: new cdk.ServicePrincipal('stepfunctions.amazonaws.com'), + }); + + const resource = new cloudformation.StateMachineResource(this, 'Resource', { + stateMachineName: props.stateMachineName, + roleArn: this.role.roleArn, + definitionString: props.definition.definitionString() + }); + + this.stateMachineName = resource.stateMachineName; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/package.json b/packages/@aws-cdk/aws-stepfunctions/package.json index c56a228d11f07..9a24127c14b35 100644 --- a/packages/@aws-cdk/aws-stepfunctions/package.json +++ b/packages/@aws-cdk/aws-stepfunctions/package.json @@ -52,7 +52,8 @@ "pkglint": "^0.8.2" }, "dependencies": { - "@aws-cdk/cdk": "^0.8.2" + "@aws-cdk/cdk": "^0.8.2", + "@aws-cdk/aws-iam": "^0.8.2" }, "homepage": "https://github.com/awslabs/aws-cdk" } From 813f4a4c4a391b14cb7ee65d50d6bc28e5677ee3 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 29 Aug 2018 18:39:19 +0200 Subject: [PATCH 07/29] First iteration of object model and builder --- examples/cdk-examples-typescript/package.json | 1 + .../stepfunctions-poller/cdk.json | 3 + .../stepfunctions-poller/index.ts | 52 +++ .../@aws-cdk/aws-lambda/lib/lambda-ref.ts | 15 +- packages/@aws-cdk/aws-lambda/package.json | 1 + .../aws-stepfunctions/lib/activity.ts | 18 +- .../aws-stepfunctions/lib/asl-builder.ts | 116 ++++++ .../aws-stepfunctions/lib/asl-condition.ts | 196 +++++++++ .../aws-stepfunctions/lib/asl-states.ts | 382 ++++++++++++++++++ .../@aws-cdk/aws-stepfunctions/lib/asl.ts | 49 +-- .../@aws-cdk/aws-stepfunctions/lib/index.ts | 4 + .../aws-stepfunctions/lib/state-machine.ts | 25 +- .../aws-stepfunctions/lib/task-resource.ts | 16 + .../test/test.amazon-states-language.ts | 3 + 14 files changed, 830 insertions(+), 51 deletions(-) create mode 100644 examples/cdk-examples-typescript/stepfunctions-poller/cdk.json create mode 100644 examples/cdk-examples-typescript/stepfunctions-poller/index.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/asl-builder.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/asl-condition.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/asl-states.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/task-resource.ts diff --git a/examples/cdk-examples-typescript/package.json b/examples/cdk-examples-typescript/package.json index 3a2ef91dcbf36..17d84517d47f7 100644 --- a/examples/cdk-examples-typescript/package.json +++ b/examples/cdk-examples-typescript/package.json @@ -35,6 +35,7 @@ "@aws-cdk/aws-s3": "^0.8.2", "@aws-cdk/aws-sns": "^0.8.2", "@aws-cdk/aws-sqs": "^0.8.2", + "@aws-cdk/aws-stepfunctions": "^0.8.2", "@aws-cdk/cdk": "^0.8.2", "@aws-cdk/runtime-values": "^0.8.2" }, diff --git a/examples/cdk-examples-typescript/stepfunctions-poller/cdk.json b/examples/cdk-examples-typescript/stepfunctions-poller/cdk.json new file mode 100644 index 0000000000000..071a16c8242fe --- /dev/null +++ b/examples/cdk-examples-typescript/stepfunctions-poller/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "node index" +} diff --git a/examples/cdk-examples-typescript/stepfunctions-poller/index.ts b/examples/cdk-examples-typescript/stepfunctions-poller/index.ts new file mode 100644 index 0000000000000..3fc630d5af378 --- /dev/null +++ b/examples/cdk-examples-typescript/stepfunctions-poller/index.ts @@ -0,0 +1,52 @@ +import stepfunctions = require('@aws-cdk/aws-stepfunctions'); +import cdk = require('@aws-cdk/cdk'); + +class Poller extends stepfunctions.StateMachine { + constructor(parent: cdk.Construct, id: string) { + super(parent, id); + + const submitJob = new stepfunctions.Pass(this, 'Submit Job'); + const waitJob = new stepfunctions.Wait(this, 'Wait X Seconds', { seconds: 60 }); + const getJobStatus = new stepfunctions.Pass(this, 'Get Job Status'); + const jobComplete = new stepfunctions.Choice(this, 'Job Complete?'); + const jobFailed = new stepfunctions.Fail(this, 'Job Failed', { cause: 'Job failed', error: 'JobFailure' }); + const getFinalStatus = new stepfunctions.Pass(this, 'Get Final Job Status'); + + submitJob.next(waitJob); + waitJob.next(getJobStatus); + getJobStatus.next(jobComplete); + jobComplete.on(new stepfunctions.StringEqualsComparisonOperation({ variable: '$.status', value: "SUCCEEDED" }), getFinalStatus); + jobComplete.on(new stepfunctions.StringEqualsComparisonOperation({ variable: '$.status', value: "FAILED" }), jobFailed); + jobComplete.otherwise(waitJob); + + new stepfunctions.Branch(this) + .pass(submitJob) + .label('loop') + .wait(waitJob) + .wait_('Wait Some More', { seconds: 10 }) + .pass(getJobStatus) + .choice(jobComplete) + .on(new stepfunctions.StringEqualsComparisonOperation({ variable: '$.status', value: "SUCCEEDED" })) + .pass(getFinalStatus) + .end() + .on(new stepfunctions.StringEqualsComparisonOperation({ variable: '$.status', value: "FAILED" })) + .fail(jobFailed) + .end() + .otherwise().goto('loop'); + + // States referenceable inside container are different from states + // rendered! + } +} + +class StepFunctionsDemoStack extends cdk.Stack { + constructor(parent: cdk.App, name: string) { + super(parent, name); + + new Poller(this, 'SM'); + } +} + +const app = new cdk.App(process.argv); +new StepFunctionsDemoStack(app, 'StepFunctionsDemo'); +process.stdout.write(app.run()); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts index 089270bbaaa93..467ff04927877 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts @@ -3,6 +3,7 @@ import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import logs = require('@aws-cdk/aws-logs'); import s3n = require('@aws-cdk/aws-s3-notifications'); +import stepfunctions = require('@aws-cdk/aws-stepfunctions'); import cdk = require('@aws-cdk/cdk'); import { cloudformation, FunctionArn, FunctionName } from './lambda.generated'; import { Permission } from './permission'; @@ -25,8 +26,8 @@ export interface FunctionRefProps { } export abstract class FunctionRef extends cdk.Construct - implements events.IEventRuleTarget, logs.ILogSubscriptionDestination, s3n.IBucketNotificationDestination { - + implements events.IEventRuleTarget, logs.ILogSubscriptionDestination, s3n.IBucketNotificationDestination, + stepfunctions.IStepFunctionsTaskResource { /** * Creates a Lambda function object which represents a function not defined * within this stack. @@ -190,6 +191,16 @@ export abstract class FunctionRef extends cdk.Construct }; } + public asStepFunctionsTaskResource(callingTask: stepfunctions.Task): stepfunctions.StepFunctionsTaskResourceProps { + callingTask.addToRolePolicy(new cdk.PolicyStatement() + .addResource(this.functionArn) + .addActions("lambda:InvokeFunction")); + + return { + resourceArn: this.functionArn + }; + } + /** * Return the given named metric for this Lambda */ diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index a4cee9cbd8c36..398986f766c2d 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -64,6 +64,7 @@ "@aws-cdk/aws-logs": "^0.8.2", "@aws-cdk/aws-s3": "^0.8.2", "@aws-cdk/aws-s3-notifications": "^0.8.2", + "@aws-cdk/aws-stepfunctions": "^0.8.2", "@aws-cdk/cdk": "^0.8.2", "@aws-cdk/cx-api": "^0.8.2" }, diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts b/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts index efe14fc5b1b14..22f6e934127c3 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts @@ -1,6 +1,5 @@ import cdk = require('@aws-cdk/cdk'); - -import { cloudformation } from './stepfunctions.generated'; +import { ActivityArn, ActivityName, cloudformation } from './stepfunctions.generated'; export interface ActivityProps { /** @@ -11,6 +10,21 @@ export interface ActivityProps { activityName: string; } +/** + * Define a new StepFunctions activity + */ export class Activity extends cdk.Construct { + public readonly activityArn: ActivityArn; + public readonly activityName: ActivityName; + + constructor(parent: cdk.Construct, id: string, props: ActivityProps) { + super(parent, id); + + const resource = new cloudformation.ActivityResource(this, 'Resource', { + activityName: props.activityName + }); + this.activityArn = resource.ref; + this.activityName = resource.activityName; + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl-builder.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl-builder.ts new file mode 100644 index 0000000000000..3387b3b9151cb --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/asl-builder.ts @@ -0,0 +1,116 @@ +import { Condition } from "./asl-condition"; +import { Choice, ChoiceProps, Fail, INextable, Pass, State, StateMachineDefinition, Task, Wait, WaitProps } from "./asl-states"; + +export class Branch { + protected first?: State; + private current?: INextable; + + constructor(private readonly definition: StateMachineDefinition, private readonly choices?: Choices) { + Array.isArray(this.definition); // FORCE USAGE + } + + public pass(task: Pass) { + this.add(task); + this.current = task; + return this; + } + + public wait(task: Wait) { + this.add(task); + this.current = task; + return this; + } + + public wait_(id: string, props: WaitProps) { + return this.wait(new Wait(this.definition, id, props)); + } + + public task(task: Task) { + this.add(task); + this.current = task; + return this; + } + + public fail(task: Fail) { + this.add(task); + return this; + } + + public choice(choice: Choice): Choices { + this.add(choice); + return new Choices(this.definition, this, choice); + } + + public choice_(id: string, props: ChoiceProps): Choices { + return this.choice(new Choice(this.definition, id, props)); + } + + public label(_label: string): Branch { + return this; + } + + public goto(_label: string) { + return this.end(); + } + + public end(): Choices { + if (!this.choices) { + throw new Error('No need to .end() a top-level branch!'); + } + + this.onEnd(); + return this.choices; + } + + protected onEnd(): void { + // Nothing + } + + private add(state: State) { + // FIXME: check task against children of definition + if (!this.first) { + this.first = state; + } + if (this.current) { + this.current.next(state); + } + } +} + +export class Choices { + constructor(private readonly definition: StateMachineDefinition, private readonly parent: Branch, private readonly choice: Choice) { + } + + public on(condition: Condition): ChoiceBranchBuilder { + return new ChoiceBranchBuilder(this.definition, this, condition); + } + + public otherwise(): Branch { + return new Branch(this.definition, this); + } + + public end(): Branch { + return this.parent; + } + + public _addChoice(condition: Condition, nextState: State) { + this.choice.on(condition, nextState); + } +} + +/** + * You'd say this needs to inherit from Branch but it can't because the return type needs to be ChoiceBranchBuilder + */ +export class ChoiceBranchBuilder extends Branch { + constructor(definition: StateMachineDefinition, private readonly parent: Choices, private readonly condition: Condition) { + super(definition, parent); + } + + protected onEnd() { + if (!this.first) { + throw new Error('Branch has no states. Did you mean to add .success()?'); + } + + this.parent._addChoice(this.condition, this.first); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl-condition.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl-condition.ts new file mode 100644 index 0000000000000..bb339a499e135 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/asl-condition.ts @@ -0,0 +1,196 @@ +export enum ComparisonOperator { + StringEquals, + StringLessThan, + StringGreaterThan, + StringLessThanEquals, + StringGreaterThanEquals, + NumericEquals, + NumericLessThan, + NumericGreaterThan, + NumericLessThanEquals, + NumericGreaterThanEquals, + BooleanEquals, + TimestampEquals, + TimestampLessThan, + TimestampGreaterThan, + TimestampLessThanEquals, + TimestampGreaterThanEquals, + And, + Or, + Not +} + +export abstract class Condition { + public abstract toCondition(): any; +} + +export interface BaseVariableComparisonOperationProps { + comparisonOperator: ComparisonOperator, + value: any, + variable: string +} + +export interface VariableComparisonOperationProps { + /** + * The value to be compared against. + */ + value: any, + + /** + * A Path to the value to be compared. + */ + variable: string +} + +export abstract class VariableComparisonOperation extends Condition { + constructor(private readonly props: BaseVariableComparisonOperationProps) { + super(); + } + + public toCondition(): any { + return { + Variable: this.props.variable, + [ComparisonOperator[this.props.comparisonOperator]]: this.props.value + }; + } +} + +export class StringEqualsComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({ ...props, ...{ comparisonOperator: ComparisonOperator.StringEquals } }); + } +} + +export class StringLessThanComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({ ...props, ...{ comparisonOperator: ComparisonOperator.StringLessThan } }); + } +} + +export class StringGreaterThanComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({ ...props, ...{ comparisonOperator: ComparisonOperator.StringGreaterThan } }); + } +} + +export class StringLessThanEqualsComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({ ...props, ...{ comparisonOperator: ComparisonOperator.StringLessThanEquals } }); + } +} + +export class StringGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({ ...props, ...{ comparisonOperator: ComparisonOperator.StringGreaterThanEquals } }); + } +} + +export class NumericEqualsComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({ ...props, ...{ comparisonOperator: ComparisonOperator.NumericEquals } }); + } +} + +export class NumericLessThanComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({ ...props, ...{ comparisonOperator: ComparisonOperator.NumericLessThan } }); + } +} + +export class NumericGreaterThanComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({ ...props, ...{ comparisonOperator: ComparisonOperator.NumericGreaterThan } }); + } +} + +export class NumericLessThanEqualsComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({ ...props, ...{ comparisonOperator: ComparisonOperator.NumericLessThanEquals } }); + } +} + +export class NumericGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({ ...props, ...{ comparisonOperator: ComparisonOperator.NumericGreaterThanEquals } }); + } +} + +export class BooleanEqualsComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({ ...props, ...{ comparisonOperator: ComparisonOperator.BooleanEquals } }); + } +} + +export class TimestampEqualsComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({ ...props, ...{ comparisonOperator: ComparisonOperator.TimestampEquals } }); + } +} + +export class TimestampLessThanComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({ ...props, ...{ comparisonOperator: ComparisonOperator.TimestampLessThan } }); + } +} + +export class TimestampGreaterThanComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({ ...props, ...{ comparisonOperator: ComparisonOperator.TimestampGreaterThan } }); + } +} + +export class TimestampLessThanEqualsComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({ ...props, ...{ comparisonOperator: ComparisonOperator.TimestampLessThanEquals } }); + } +} + +export class TimestampGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { + constructor(props: VariableComparisonOperationProps) { + super({ ...props, ...{ comparisonOperator: ComparisonOperator.TimestampGreaterThanEquals } }); + } +} + +export interface ArrayComparisonOperationProps { + comparisonOperator: ComparisonOperator, + comparisonOperations: Condition[] +} + +export abstract class ArrayComparisonOperation extends Condition { + constructor(private readonly props: ArrayComparisonOperationProps) { + super(); + if (props.comparisonOperations.length === 0) { + throw new Error('\'comparisonOperations\' is empty. Must be non-empty array of ChoiceRules'); + } + } + + public toCondition(): any { + return { + [ComparisonOperator[this.props.comparisonOperator]]: this.props.comparisonOperations + }; + } +} + +export class AndComparisonOperation extends ArrayComparisonOperation { + constructor(...comparisonOperations: Condition[]) { + super({ comparisonOperator: ComparisonOperator.And, comparisonOperations }); + } +} + +export class OrComparisonOperation extends ArrayComparisonOperation { + constructor(...comparisonOperations: Condition[]) { + super({ comparisonOperator: ComparisonOperator.Or, comparisonOperations }); + } +} + +export class NotComparisonOperation extends Condition { + constructor(private readonly comparisonOperation: Condition) { + super(); + } + + public toCondition(): any { + return { + [ComparisonOperator[ComparisonOperator.Not]]: this.comparisonOperation + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl-states.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl-states.ts new file mode 100644 index 0000000000000..6b7a1ef562cfc --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/asl-states.ts @@ -0,0 +1,382 @@ +import iam = require('@aws-cdk/aws-iam'); +import cdk = require('@aws-cdk/cdk'); +import { Condition } from './asl-condition'; +import { IStepFunctionsTaskResource, StepFunctionsTaskResourceProps } from './task-resource'; + +export interface StateMachineDefinitionProps { + timeoutSeconds?: number; +} + +export interface INextState { + firstState(): State; +} + +export class StateMachineDefinition extends cdk.Construct { + /** + * Used to find this Construct back in the construct tree + */ + public readonly isStateMachine = true; + + private readonly states: State[] = []; + private startState?: State; + private readonly timeoutSeconds?: number; + private readonly policyStatements = new Array(); + + constructor(parent: cdk.Construct, id: string, props: StateMachineDefinitionProps = {}) { + super(parent, id); + this.timeoutSeconds = props.timeoutSeconds; + } + + public _addState(state: State) { + this.states.push(state); + if (this.startState === undefined) { + this.startState = state; + } + } + + public addToRolePolicy(statement: cdk.PolicyStatement) { + this.policyStatements.push(statement); + } + + public addPolicyStatementsToRole(role: iam.Role) { + for (const s of this.policyStatements) { + role.addToPolicy(s); + } + } + + public toStateMachine(): any { + if (!this.startState) { + throw new Error('No states added to StateMachine'); + } + + const states: any = {}; + for (const s of this.states) { + states[s.stateId] = s.toState(); + } + + // FIXME: Check all states are reachable. + + return { + StartAt: this.startState.stateId, + States: states, + TimeoutSeconds: this.timeoutSeconds, + Version: '1.0', + }; + } +} + +export interface StateProps { + type: StateType; + inputPath?: string; + outputPath?: string; +} + +export interface INextable { + next(state: State): void; +} + +export abstract class State extends cdk.Construct { + private readonly type: StateType; + private readonly inputPath?: string; + private readonly outputPath?: string; + + constructor(parent: StateMachineDefinition, id: string, props: StateProps) { + super(parent, id); + + this.type = props.type; + this.inputPath = props.inputPath; + this.outputPath = props.outputPath; + + parent._addState(this); + } + + /** + * Return the name of this state + */ + public get stateId(): string { + const sm = this.encompassingStateMachine(); + return this.ancestors(sm).map(p => p.id).join('/'); + } + + public toState(): any { + return { + Type: this.type, + InputPath: this.inputPath, + OutputPath: this.outputPath, + }; + } + + /** + * Find the top-level StateMachine we're part of + */ + private encompassingStateMachine(): StateMachineDefinition { + let curr: cdk.Construct | undefined = this; + while (curr && !isStateMachine(curr)) { + curr = curr.parent; + } + if (!curr) { + throw new Error('Could not find encompassing StateMachine'); + } + return curr; + } +} + +function isStateMachine(construct: cdk.Construct): construct is StateMachineDefinition { + return (construct as any).isStateMachine; +} + +export interface TaskProps { + resource: IStepFunctionsTaskResource; + inputPath?: string; + outputPath?: string; + resultPath?: string; + timeoutSeconds?: number; + heartbeatSeconds?: number; +} + +export class Task extends State implements INextable { + private _next?: State; + private readonly resource: IStepFunctionsTaskResource; + private readonly resultPath?: string; + private readonly timeoutSeconds?: number; + private readonly heartbeatSeconds?: number; + private readonly resourceProps: StepFunctionsTaskResourceProps; + + constructor(parent: StateMachineDefinition, id: string, props: TaskProps) { + super(parent, id, { + type: StateType.Task, + inputPath: props.inputPath, + outputPath: props.outputPath + }); + this.resource = props.resource; + this.resultPath = props.resultPath; + this.timeoutSeconds = props.timeoutSeconds; + this.heartbeatSeconds = props.heartbeatSeconds; + + this.resourceProps = this.resource.asStepFunctionsTaskResource(this); + } + + /** + * Add a policy statement to the role that ultimately executes this + */ + public addToRolePolicy(statement: cdk.PolicyStatement) { + (this.parent as StateMachineDefinition).addToRolePolicy(statement); + } + + public next(state: State) { + this._next = state; + } + + public toState(): any { + return { + ...super.toState(), + Resource: this.resourceProps.resourceArn, + ResultPath: this.resultPath, + Next: this._next ? this._next.stateId : undefined, + End: this._next ? undefined : true, + TimeoutSeconds: this.timeoutSeconds, + HeartbeatSeconds: this.heartbeatSeconds, + }; + } +} + +export interface PassProps { + inputPath?: string; + outputPath?: string; +} + +export class Pass extends State implements INextable { + private _next?: State; + + constructor(parent: StateMachineDefinition, id: string, props: PassProps = {}) { + super(parent, id, { + type: StateType.Pass, + inputPath: props.inputPath, + outputPath: props.outputPath + }); + } + + public next(state: State) { + this._next = state; + } + + public toState(): any { + return { + ...super.toState(), + Next: this._next ? this._next.stateId : undefined, + End: this._next ? undefined : true, + }; + } +} + +export interface WaitProps { + seconds?: number; + timestamp?: string; + + secondsPath?: string; + timestampPath?: string; +} + +export class Wait extends State implements INextable { + private _next?: State; + private readonly seconds?: number; + private readonly timestamp?: string; + private readonly secondsPath?: string; + private readonly timestampPath?: string; + + constructor(parent: StateMachineDefinition, id: string, props: WaitProps) { + // FIXME: Validate input + + super(parent, id, { + type: StateType.Wait, + }); + + this.seconds = props.seconds; + this.timestamp = props.timestamp; + this.secondsPath = props.secondsPath; + this.timestampPath = props.timestampPath; + } + + public next(state: State) { + this._next = state; + } + + public toState(): any { + return { + ...super.toState(), + Seconds: this.seconds, + Timestamp: this.timestamp, + SecondsPath: this.secondsPath, + TimestampPath: this.timestampPath, + Next: this._next ? this._next.stateId : undefined, + End: this._next ? undefined : true, + }; + } +} + +export interface ParallelProps { + branches: StateMachineDefinition[] + inputPath?: string; + outputPath?: string; + resultPath?: string; +} + +export class Parallel extends State implements INextable { + private _next?: State; + private readonly branches: StateMachineDefinition[]; + private readonly resultPath?: string; + + constructor(parent: StateMachineDefinition, id: string, props: ParallelProps) { + super(parent, id, { + type: StateType.Parallel, + inputPath: props.inputPath, + outputPath: props.outputPath + }); + this.branches = props.branches; + this.resultPath = props.resultPath; + } + + public next(state: State) { + this._next = state; + } + + public toState(): any { + return { + ...super.toState(), + ResultPath: this.resultPath, + Next: this._next ? this._next.stateId : undefined, + End: this._next ? undefined : true, + Branches: this.branches.map(b => b.toStateMachine()) + }; + } +} + +export interface ChoiceBranch { + condition: Condition; + next: State; +} + +export interface ChoiceProps { + inputPath?: string; + outputPath?: string; +} + +export class Choice extends State { + private readonly choices: ChoiceBranch[] = []; + private default?: State; + + constructor(parent: StateMachineDefinition, id: string, props: ChoiceProps = {}) { + super(parent, id, { + type: StateType.Choice, + inputPath: props.inputPath, + outputPath: props.outputPath + }); + } + + public on(condition: Condition, next: State) { + this.choices.push({ condition, next }); + } + + public otherwise(next: State) { + this.default = next; + } + + public toState(): any { + return { + ...super.toState(), + Choices: this.choices.map(c => this.renderChoice(c)), + Default: this.default ? this.default.stateId : undefined + }; + } + + private renderChoice(c: ChoiceBranch) { + return { + ...c.condition.toCondition(), + Next: c.next.stateId + }; + } +} + +export interface FailProps { + error: string; + cause: string; +} + +export class Fail extends State { + private readonly error: string; + private readonly cause: string; + + constructor(parent: StateMachineDefinition, id: string, props: FailProps) { + super(parent, id, { + type: StateType.Fail + }); + this.error = props.error; + this.cause = props.cause; + } + + public toState(): any { + return { + ...super.toState(), + Error: this.error, + Cause: this.cause + }; + } +} + +export class Succeed extends State { + constructor(parent: StateMachineDefinition, id: string) { + super(parent, id, { + type: StateType.Succeed + }); + } +} + +export enum StateType { + Pass = 'Pass', + Task = 'Task', + Choice = 'Choice', + Wait = 'Wait', + Succeed = 'Succeed', + Fail = 'Fail', + Parallel = 'Parallel' +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl.ts index 4568443c063e6..3711733f077a3 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/asl.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/asl.ts @@ -1,4 +1,4 @@ -import { Token, CloudFormationJSON } from "@aws-cdk/cdk"; +import { CloudFormationJSON, Token } from "@aws-cdk/cdk"; import { isString } from "util"; import { requireNextOrEnd, requireOneOf } from "./util"; @@ -59,10 +59,6 @@ export namespace asl { if (isString(props.startAt) && !props.states.hasState(props.startAt)) { throw new Error(`Specified startAt state '${props.startAt}' does not exist in states map`); } - const allStates = props.states.stateNames(); - if (!allStates.some(istoken) && new Set(allStates).size !== allStates.length) { - throw new Error('State names are not unique within the whole state machine'); - } super(props); } @@ -82,7 +78,7 @@ export namespace asl { /** * A comment provided as human-readable description */ - comment?: string | Token + comment?: string } export interface BranchProps extends Commentable { @@ -100,7 +96,7 @@ export namespace asl { * * {@link https://states-language.net/spec.html#toplevelfields} */ - startAt: string | Token + startAt: string } export class Branch extends PascalCaseJson { @@ -114,7 +110,7 @@ export namespace asl { this.states = props.states; } - public stateNames(): Array { + public stateNames(): string[] { return this.states.stateNames(); } } @@ -133,16 +129,11 @@ export namespace asl { if (longNames.length > 0) { throw new Error(`State names ${JSON.stringify(longNames)} exceed 128 characters in length`); } - for (const [stateName, state] of Object.entries(states)) { + for (const state of Object.values(states)) { const next = state.next(); if (!state.isTerminal() && next.length === 0 && !(state instanceof ChoiceState)) { throw new Error(`Non-terminal and non-ChoiceState state '${state}' does not have a 'next' field`); } - next.forEach(referencedState => { - if (!istoken(referencedState) && !(referencedState in states)) { - throw new Error(`State '${stateName}' references unknown Next state '${referencedState}'`); - } - }); } super(states); } @@ -155,8 +146,8 @@ export namespace asl { return this.props.hasOwnProperty(name); } - public stateNames(): Array { - const names: Array = Object.keys(this.props); + public stateNames(): string[] { + const names: string[] = Object.keys(this.props); Object.values(this.props).map( state => (state instanceof ParallelState) ? state.stateNames() : [] ).forEach(branchNames => branchNames.forEach(name => names.push(name))); @@ -170,7 +161,7 @@ export namespace asl { * * Must exactly match the name of another state in the state machine. */ - next: string | Token; + next: string; } export interface EndField { @@ -189,7 +180,7 @@ export namespace asl { * * Must exactly match the name of another state in the state machine. */ - next?: string | Token, + next?: string, /** * Marks the state as an End State. @@ -214,7 +205,7 @@ export namespace asl { * * {@link https://states-language.net/spec.html#filters} */ - inputPath?: string | Token; + inputPath?: string; /** * A {@link https://states-language.net/spec.html#path Path} applied to @@ -364,12 +355,6 @@ export namespace asl { export class Retrier extends PascalCaseJson { constructor(props: RetrierProps) { validateErrorEquals(props.errorEquals); - if (props.intervalSeconds && !istoken(props.intervalSeconds) && (!Number.isInteger(props.intervalSeconds) || props.intervalSeconds < 1)) { - throw new Error(`intervalSeconds '${props.intervalSeconds}' is not a positive integer`); - } - if (props.maxAttempts && !istoken(props.maxAttempts) && (!Number.isInteger(props.maxAttempts) || props.maxAttempts < 0)) { - throw new Error(`maxAttempts '${props.maxAttempts}' is not a non-negative integer`); - } if (props.backoffRate && props.backoffRate < 1.0) { throw new Error(`backoffRate '${props.backoffRate}' is not >= 1.0`); } @@ -532,12 +517,6 @@ export namespace asl { */ export class TaskState extends BaseState { constructor(props: TaskStateProps) { - if (props.timeoutSeconds !== undefined && !istoken(props.timeoutSeconds) && !Number.isInteger(props.timeoutSeconds)) { - throw new Error(`timeoutSeconds '${props.timeoutSeconds}' is not an integer`); - } - if (props.heartbeatSeconds !== undefined && !istoken(props.heartbeatSeconds) && !Number.isInteger(props.heartbeatSeconds)) { - throw new Error(`heartbeatSeconds '${props.heartbeatSeconds}' is not an integer`); - } if (props.timeoutSeconds !== undefined && props.heartbeatSeconds !== undefined && props.heartbeatSeconds >= props.timeoutSeconds) { throw new Error("heartbeatSeconds is larger than timeoutSeconds"); } @@ -586,7 +565,7 @@ export namespace asl { /** * A Path to the value to be compared. */ - variable: string | Token + variable: string } export abstract class VariableComparisonOperation extends ComparisonOperation { @@ -900,8 +879,8 @@ export namespace asl { this.branches = branches; } - public stateNames(): Array { - const names: Array = []; + public stateNames(): string[] { + const names: string[] = []; this.branches.forEach(branch => branch.stateNames().forEach(name => names.push(name))); return names; } @@ -921,7 +900,7 @@ export namespace asl { this.branches = props.branches; } - public stateNames(): Array { + public stateNames(): string[] { return this.branches.stateNames(); } } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions/lib/index.ts index dd0414620f109..ba483e0750c9b 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/index.ts @@ -1,6 +1,10 @@ export * from './activity'; export * from './asl'; +export * from './asl-states'; +export * from './asl-condition'; export * from './state-machine'; +export * from './task-resource'; +export * from './asl-builder'; // AWS::StepFunctions CloudFormation Resources: export * from './stepfunctions.generated'; diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts index c76eec3e48e73..1eebcc569ce01 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts @@ -1,8 +1,7 @@ import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); - -import { asl } from './asl'; -import { cloudformation, StateMachineName } from './stepfunctions.generated'; +import { StateMachineDefinition } from './asl-states'; +import { cloudformation, StateMachineArn, StateMachineName } from './stepfunctions.generated'; export interface StateMachineProps { /** @@ -12,11 +11,6 @@ export interface StateMachineProps { */ stateMachineName?: string; - /** - * The definition of this state machine - */ - definition: asl.StateMachine; - /** * The execution role for the state machine service * @@ -28,23 +22,30 @@ export interface StateMachineProps { /** * Define a StepFunctions State Machine */ -export class StateMachine extends cdk.Construct { +export class StateMachine extends StateMachineDefinition { public readonly role: iam.Role; public readonly stateMachineName: StateMachineName; + public readonly stateMachineArn: StateMachineArn; - constructor(parent: cdk.Construct, id: string, props: StateMachineProps) { + constructor(parent: cdk.Construct, id: string, props: StateMachineProps = {}) { super(parent, id); this.role = props.role || new iam.Role(this, 'Role', { - assumedBy: new cdk.ServicePrincipal('stepfunctions.amazonaws.com'), + assumedBy: new cdk.ServicePrincipal(new cdk.FnConcat('states.', new cdk.AwsRegion(), '.amazonaws.com').toString()), }); const resource = new cloudformation.StateMachineResource(this, 'Resource', { stateMachineName: props.stateMachineName, roleArn: this.role.roleArn, - definitionString: props.definition.definitionString() + // We may have objects added to us after creation + definitionString: new cdk.Token(() => cdk.CloudFormationJSON.stringify(this.toStateMachine())) }); this.stateMachineName = resource.stateMachineName; + this.stateMachineArn = resource.ref; + } + + public addToRolePolicy(statement: cdk.PolicyStatement) { + this.role.addToPolicy(statement); } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/task-resource.ts b/packages/@aws-cdk/aws-stepfunctions/lib/task-resource.ts new file mode 100644 index 0000000000000..d0752176ef11c --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/task-resource.ts @@ -0,0 +1,16 @@ +import cdk = require('@aws-cdk/cdk'); +import { Task } from './asl-states'; + +/** + * Interface for objects that can be invoked in a Task state + */ +export interface IStepFunctionsTaskResource { + /** + * Return the properties required for using this object as a Task resource + */ + asStepFunctionsTaskResource(callingTask: Task): StepFunctionsTaskResourceProps; +} + +export interface StepFunctionsTaskResourceProps { + resourceArn: cdk.Arn; +} diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts index 7c56ca4098b47..8ffd9f91402f6 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts @@ -6,6 +6,7 @@ function roundtrip(obj: any) { } export = { + /* 'Hello World example state machine'(test: Test) { test.deepEqual( JSON.parse( @@ -34,6 +35,8 @@ export = { ); test.done(); }, + */ + 'Models Complex retry scenarios example'(test: Test) { test.deepEqual( roundtrip( From 385ae1f7359cc5a9de9e568649d8cc33b973a604 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 3 Sep 2018 12:40:01 +0200 Subject: [PATCH 08/29] WIP --- .../stepfunctions-poller/index.ts | 34 +- .../aws-stepfunctions/DESIGN_NOTES.md | 202 ++++++ .../aws-stepfunctions/lib/asl-builder.ts | 116 ---- .../aws-stepfunctions/lib/asl-condition.ts | 14 +- .../aws-stepfunctions/lib/asl-external-api.ts | 80 +++ .../aws-stepfunctions/lib/asl-internal-api.ts | 29 + .../aws-stepfunctions/lib/asl-state-chain.ts | 93 +++ .../aws-stepfunctions/lib/asl-states.ts | 382 ----------- .../@aws-cdk/aws-stepfunctions/lib/index.ts | 15 +- .../aws-stepfunctions/lib/state-machine.ts | 14 +- .../aws-stepfunctions/lib/states/choice.ts | 92 +++ .../aws-stepfunctions/lib/states/fail.ts | 51 ++ .../aws-stepfunctions/lib/states/parallel.ts | 85 +++ .../aws-stepfunctions/lib/states/pass.ts | 62 ++ .../lib/states/state-machine-definition.ts | 74 +++ .../aws-stepfunctions/lib/states/state.ts | 71 ++ .../aws-stepfunctions/lib/states/succeed.ts | 52 ++ .../aws-stepfunctions/lib/states/task.ts | 108 +++ .../aws-stepfunctions/lib/states/util.ts | 22 + .../aws-stepfunctions/lib/states/wait.ts | 61 ++ .../aws-stepfunctions/lib/task-resource.ts | 16 - .../test/test.amazon-states-language.ts | 392 ----------- .../test/test.state-machine-resources.ts | 7 + .../test/test.states-language.ts | 625 ++++++++++++++++++ 24 files changed, 1756 insertions(+), 941 deletions(-) create mode 100644 packages/@aws-cdk/aws-stepfunctions/DESIGN_NOTES.md delete mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/asl-builder.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/asl-external-api.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/asl-internal-api.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/asl-state-chain.ts delete mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/asl-states.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/states/fail.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/states/state-machine-definition.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/states/util.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/states/wait.ts delete mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/task-resource.ts delete mode 100644 packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts diff --git a/examples/cdk-examples-typescript/stepfunctions-poller/index.ts b/examples/cdk-examples-typescript/stepfunctions-poller/index.ts index 3fc630d5af378..9c5d00dac1bef 100644 --- a/examples/cdk-examples-typescript/stepfunctions-poller/index.ts +++ b/examples/cdk-examples-typescript/stepfunctions-poller/index.ts @@ -1,7 +1,7 @@ import stepfunctions = require('@aws-cdk/aws-stepfunctions'); import cdk = require('@aws-cdk/cdk'); -class Poller extends stepfunctions.StateMachine { +class Poller extends stepfunctions.StateMachineDefinition { constructor(parent: cdk.Construct, id: string) { super(parent, id); @@ -12,27 +12,13 @@ class Poller extends stepfunctions.StateMachine { const jobFailed = new stepfunctions.Fail(this, 'Job Failed', { cause: 'Job failed', error: 'JobFailure' }); const getFinalStatus = new stepfunctions.Pass(this, 'Get Final Job Status'); - submitJob.next(waitJob); - waitJob.next(getJobStatus); - getJobStatus.next(jobComplete); - jobComplete.on(new stepfunctions.StringEqualsComparisonOperation({ variable: '$.status', value: "SUCCEEDED" }), getFinalStatus); - jobComplete.on(new stepfunctions.StringEqualsComparisonOperation({ variable: '$.status', value: "FAILED" }), jobFailed); - jobComplete.otherwise(waitJob); - - new stepfunctions.Branch(this) - .pass(submitJob) - .label('loop') - .wait(waitJob) - .wait_('Wait Some More', { seconds: 10 }) - .pass(getJobStatus) - .choice(jobComplete) - .on(new stepfunctions.StringEqualsComparisonOperation({ variable: '$.status', value: "SUCCEEDED" })) - .pass(getFinalStatus) - .end() - .on(new stepfunctions.StringEqualsComparisonOperation({ variable: '$.status', value: "FAILED" })) - .fail(jobFailed) - .end() - .otherwise().goto('loop'); + this.define(submitJob + .then(waitJob) + .then(getJobStatus) + .then(jobComplete + .on(stepfunctions.Condition.stringEquals('$.status', 'SUCCEEDED'), getFinalStatus) + .on(stepfunctions.Condition.stringEquals('$.status', 'FAILED'), jobFailed) + .otherwise(waitJob))); // States referenceable inside container are different from states // rendered! @@ -43,7 +29,9 @@ class StepFunctionsDemoStack extends cdk.Stack { constructor(parent: cdk.App, name: string) { super(parent, name); - new Poller(this, 'SM'); + new stepfunctions.StateMachine(this, 'StateMachine', { + definition: new Poller(this, 'Poller') + }); } } diff --git a/packages/@aws-cdk/aws-stepfunctions/DESIGN_NOTES.md b/packages/@aws-cdk/aws-stepfunctions/DESIGN_NOTES.md new file mode 100644 index 0000000000000..dc106f30b9e99 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/DESIGN_NOTES.md @@ -0,0 +1,202 @@ +- Goal: should be possible to define structures such as IfThenElse() by users. + We'd like the usage of these constructs to look more or less like they would look + in regular programming languages. It should look roughly like this: + + task + .then(new IfThenElse( + new Condition(), + task.then(task).then(task), + task.then(task)) + .then(task) + +- Goal: should be possible to define reusable-recurring pieces with parameters, + and then reuse them. + + new Parallel([ + new DoSomeWork({ quality: 'high' }), + new DoSomeWork({ quality: 'medium' }), + new DoSomeWork({ quality: 'low' }), + ]) + +- Goal: States defined in the same StateMachineDefinition share a scope and cannot refer + to states outside that scope. StateMachineDefinition can be exploded into other + StateMachineDefinitions. + +- Goal: you shouldn't HAVE to define a StateMachineDefinition to use in a Parallel + (even though should render as such when expanding to ASL). The following should also work: + + new Parallel([ + task.then(task).then(task), + task.then(task), + task + ]); + + Regardless of how the states get into the Parallel, it should not be possible for them + to jump outside the Parallel branch. + +- Other kind of syntax: + + task1.then(task2).then(task3).goto(task1); // <--- ends chain + +- Interface we need from State: + + interface IState { + next(chainable): Chainable; + goto(state): void; // Use this for terminators as well? + + first(): State; + allReachable(): State[]; + } + + StateMachineDefinition + - All targeted states must be part of the same StateMachineDefinition + enclosure. + - States can be "lifted" into a separate enclosure by being a target of the + Parallel branch. + - Must be able to enumerate all reachable states, so that it can error on + them. Every task can only be the target of a .next() once, but it can + be the target of multiple goto()s. + +- Contraints: Because of JSII: + + - No overloading (Golang) + - No generics (Golang) + - No return type covariance (C#) + + Lack of overloading means that in order to not have a different function call for + every state type, we'll have to do runtime checks (for example, not all states + can be next()'ed onto, and we cannot use the type system to detect and appropriately + constrain this). + + Bonus: runtime checks are going to help dynamically-typed users. + +- Constraint: Trying to next() onto a Chainable that doens't have a next(), runtime error; + Lack of overloads make it impossible to do otherwise. + +- State machine definition should be frozen (and complete) after being chained to. + +Nextable: Pass, Task, Wait, Parallel +Terminating: Succeed, Fail +Weird: Choice + +class Blah extends StateMachine { + constructor() { + // define statemachine here + } +} + +Use as: + + new StateMachine(..., { + definintion: new Blah() + }); + +But also: + + const def = new StateMachineDefinition(); + const task = new Task(def, ...); + task.then(new Blah())// <-- expand (does that introduce naming? It should!) + .then(new Task(...); // <-- appends transition to every end state! + +And also: + + const p = new Parallel(); + p.parallel(new Blah()); // <-- leave as state machine + +But not: + + const p = new Parallel(); + blah = new Blah(); + p.parallel(blah); + + blah.then(...); // <-- should be immutable!! + +And also: + + const task1 = new Task1(); + const task2 = new Task1(); + + const p = new Parallel(); + p.parallel(task1); // <-- convert to state machine + p.parallel(task2); + +class FrozenStateMachine { + render(); +} + +TO CHECK +- Can SMDef be mutable? + +QUESTION +- Do branches in Parallel allow Timeout/Version? + +PROBLEMS ENCOUNTERED +-------------------- + + task1.catch(handler).then(task2) + +Does this mean: + + (a) task1 -> task2 + | + +---> handler + +Or does this mean: + + (b) task1 -------> task2 + | ^ + +--> handler --+ + +In the case of simply writing this, you probably want (a), but +in the case of a larger composition: + + someStateMachine.then(task2) + +You want behavior (b) in case someStateMachine = task1.catch(handler). + +How to distinguish the two? The problem is in the .then() operator, but the +only way to solve it is with more operators (.close(), or otherwise) which is +going to confuse people 99% of the time. + +If we make everything mutable, we can simply do the narrow .then() definition +(open transitions only include task1), and people can manually add more +transitions after handler if they want to (in an immutable system, there's no +good way to "grab" that handler object and add to the transitions later). +Also, in the mutable representation, goto's are easier to represent (as in: +need no special representation, we can simply use .then()). + +Next complication however: if we do the mutable thing, and we can add to the +state machine in fragments, there's no object that represents the ENTIRE +state machine with all states and transitions. How do we get the full state +machine? Either we enumerate all children of a StateMachineDefinition object, +or we begin at the start state and crawl all accessible states. In the former +case, we can warn/error if not all states are used in the SM. + +Then there is the construct model API. I would like to allow: + +```ts +class SomeConstruct extends cdk.Construct { + constructor(parent: cdk.Construct) { + const task1 = new Task(this, ...); + const task2 = new Task(this, ...); + + // ALLOW THIS + new StateMachine(..., { + definition: task1.then(task2) + }); + + // ALLOW THIS + task1.then(task2); + new StateMachine(..., { + definition: task1 + }); + + new StateMachineDefinition(this, { + }); + } +} +``` + +It's desirable to just have a StateMachineDefinition own and list everything, +but that kind of requires that for Parallel you need a StateMachineDefinition +as well (but probably without appending names). \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl-builder.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl-builder.ts deleted file mode 100644 index 3387b3b9151cb..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions/lib/asl-builder.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { Condition } from "./asl-condition"; -import { Choice, ChoiceProps, Fail, INextable, Pass, State, StateMachineDefinition, Task, Wait, WaitProps } from "./asl-states"; - -export class Branch { - protected first?: State; - private current?: INextable; - - constructor(private readonly definition: StateMachineDefinition, private readonly choices?: Choices) { - Array.isArray(this.definition); // FORCE USAGE - } - - public pass(task: Pass) { - this.add(task); - this.current = task; - return this; - } - - public wait(task: Wait) { - this.add(task); - this.current = task; - return this; - } - - public wait_(id: string, props: WaitProps) { - return this.wait(new Wait(this.definition, id, props)); - } - - public task(task: Task) { - this.add(task); - this.current = task; - return this; - } - - public fail(task: Fail) { - this.add(task); - return this; - } - - public choice(choice: Choice): Choices { - this.add(choice); - return new Choices(this.definition, this, choice); - } - - public choice_(id: string, props: ChoiceProps): Choices { - return this.choice(new Choice(this.definition, id, props)); - } - - public label(_label: string): Branch { - return this; - } - - public goto(_label: string) { - return this.end(); - } - - public end(): Choices { - if (!this.choices) { - throw new Error('No need to .end() a top-level branch!'); - } - - this.onEnd(); - return this.choices; - } - - protected onEnd(): void { - // Nothing - } - - private add(state: State) { - // FIXME: check task against children of definition - if (!this.first) { - this.first = state; - } - if (this.current) { - this.current.next(state); - } - } -} - -export class Choices { - constructor(private readonly definition: StateMachineDefinition, private readonly parent: Branch, private readonly choice: Choice) { - } - - public on(condition: Condition): ChoiceBranchBuilder { - return new ChoiceBranchBuilder(this.definition, this, condition); - } - - public otherwise(): Branch { - return new Branch(this.definition, this); - } - - public end(): Branch { - return this.parent; - } - - public _addChoice(condition: Condition, nextState: State) { - this.choice.on(condition, nextState); - } -} - -/** - * You'd say this needs to inherit from Branch but it can't because the return type needs to be ChoiceBranchBuilder - */ -export class ChoiceBranchBuilder extends Branch { - constructor(definition: StateMachineDefinition, private readonly parent: Choices, private readonly condition: Condition) { - super(definition, parent); - } - - protected onEnd() { - if (!this.first) { - throw new Error('Branch has no states. Did you mean to add .success()?'); - } - - this.parent._addChoice(this.condition, this.first); - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl-condition.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl-condition.ts index bb339a499e135..a7d057deb7970 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/asl-condition.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/asl-condition.ts @@ -21,7 +21,11 @@ export enum ComparisonOperator { } export abstract class Condition { - public abstract toCondition(): any; + public static stringEquals(variable: string, value: string): Condition { + return new StringEqualsComparisonOperation({ variable, value }); + } + + public abstract renderCondition(): any; } export interface BaseVariableComparisonOperationProps { @@ -47,7 +51,7 @@ export abstract class VariableComparisonOperation extends Condition { super(); } - public toCondition(): any { + public renderCondition(): any { return { Variable: this.props.variable, [ComparisonOperator[this.props.comparisonOperator]]: this.props.value @@ -55,7 +59,7 @@ export abstract class VariableComparisonOperation extends Condition { } } -export class StringEqualsComparisonOperation extends VariableComparisonOperation { +class StringEqualsComparisonOperation extends VariableComparisonOperation { constructor(props: VariableComparisonOperationProps) { super({ ...props, ...{ comparisonOperator: ComparisonOperator.StringEquals } }); } @@ -164,7 +168,7 @@ export abstract class ArrayComparisonOperation extends Condition { } } - public toCondition(): any { + public renderCondition(): any { return { [ComparisonOperator[this.props.comparisonOperator]]: this.props.comparisonOperations }; @@ -188,7 +192,7 @@ export class NotComparisonOperation extends Condition { super(); } - public toCondition(): any { + public renderCondition(): any { return { [ComparisonOperator[ComparisonOperator.Not]]: this.comparisonOperation }; diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl-external-api.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl-external-api.ts new file mode 100644 index 0000000000000..69cc46da95149 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/asl-external-api.ts @@ -0,0 +1,80 @@ +import { IInternalState } from "./asl-internal-api"; + +export interface IChainable { + toStateChain(): IStateChain; +} + +export interface IStateChain extends IChainable { + readonly startState: IInternalState; + + then(state: IChainable): IStateChain; + catch(targetState: IChainable, ...errors: string[]): IStateChain; +} + +// tslint:disable-next-line:no-empty-interface +export interface IState extends IChainable { +} + +/** + * Predefined error strings + */ +export class Errors { + /** + * Matches any Error. + */ + public static all = 'States.ALL'; + + /** + * A Task State either ran longer than the “TimeoutSeconds” value, or + * failed to heartbeat for a time longer than the “HeartbeatSeconds” value. + */ + public static timeout = 'States.Timeout'; + + /** + * A Task State failed during the execution. + */ + public static taskFailed = 'States.TaskFailed'; + + /** + * A Task State failed because it had insufficient privileges to execute + * the specified code. + */ + public static permissions = 'States.Permissions'; + + /** + * A Task State’s “ResultPath” field cannot be applied to the input the state received. + */ + public static resultPathMatchFailure = 'States.ResultPathMatchFailure'; + + /** + * A branch of a Parallel state failed. + */ + public static branchFailed = 'States.BranchFailed'; + + /** + * A Choice state failed to find a match for the condition field extracted + * from its input. + */ + public static noChoiceMatched = 'States.NoChoiceMatched'; +} + +export interface RetryProps { + errors?: string[]; + + /** + * @default 1 + */ + intervalSeconds?: number; + + /** + * May be 0 to disable retry in case of multiple entries. + * + * @default 3 + */ + maxAttempts?: number; + + /** + * @default 2 + */ + backoffRate?: number; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl-internal-api.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl-internal-api.ts new file mode 100644 index 0000000000000..513a03f2db595 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/asl-internal-api.ts @@ -0,0 +1,29 @@ +export interface IInternalState { + readonly stateId: string; + readonly stateBehavior: StateBehavior; + + next(targetState: IInternalState): void; + catch(targetState: IInternalState, errors: string[]): void; + renderState(): any; +} + +export interface StateBehavior { + elidable: boolean; + canHaveNext: boolean; + canHaveCatch: boolean; +} + +export interface Transition { + targetState: IInternalState; + annotation: any; +} + +export enum StateType { + Pass = 'Pass', + Task = 'Task', + Choice = 'Choice', + Wait = 'Wait', + Succeed = 'Succeed', + Fail = 'Fail', + Parallel = 'Parallel' +} diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl-state-chain.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl-state-chain.ts new file mode 100644 index 0000000000000..79cf40856ed18 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/asl-state-chain.ts @@ -0,0 +1,93 @@ +import { Errors, IChainable, IStateChain } from './asl-external-api'; +import { IInternalState } from './asl-internal-api'; + +export class StateChain implements IStateChain { + private allStates = new Set(); + private activeStates = new Set(); + private _startState: IInternalState; + + constructor(startState: IInternalState) { + this.allStates.add(startState); + + this.activeStates.add(startState); + this._startState = startState; + } + + public get startState(): IInternalState { + return this._startState; + } + + public then(state: IChainable): IStateChain { + const sm = state.toStateChain(); + + const ret = this.clone(); + + if (this.activeStates.size === 0) { + throw new Error('Cannot chain onto state machine; no end states'); + } + + for (const endState of this.activeStates) { + endState.next(sm.startState); + } + + ret.absorb(sm); + ret.activeStates = new Set(accessMachineInternals(sm).activeStates); + + return ret; + } + + public toStateChain(): IStateChain { + return this; + } + + public catch(handler: IChainable, ...errors: string[]): IStateChain { + if (errors.length === 0) { + errors = [Errors.all]; + } + + const sm = handler.toStateChain(); + + const canApplyDirectly = Array.from(this.allStates).every(s => s.stateBehavior.canHaveCatch); + if (!canApplyDirectly) { + // Can't easily create a Parallel here automatically since we need a + // StateMachineDefinition parent and need to invent a unique name. + throw new Error('Chain contains non-Task, non-Parallel actions. Wrap this in a Parallel to catch errors.'); + } + + const ret = this.clone(); + for (const state of this.allStates) { + state.catch(sm.startState, errors); + } + + // Those states are now part of the state machine, but we don't include + // their active ends. + ret.absorb(sm); + + return ret; + } + + public absorb(other: IStateChain) { + const sdm = accessMachineInternals(other); + for (const state of sdm.allStates) { + this.allStates.add(state); + } + } + + private clone(): StateChain { + const ret = new StateChain(this.startState); + ret.allStates = new Set(this.allStates); + ret.activeStates = new Set(this.activeStates); + return ret; + } +} + +/** + * Access private parts of the state machine definition + * + * Parts that we don't want to show to the consumers because they'll + * only distract, but parts that other states need to achieve their + * work. + */ +export function accessMachineInternals(x: IStateChain): StateChain { + return x as StateChain; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl-states.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl-states.ts deleted file mode 100644 index 6b7a1ef562cfc..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions/lib/asl-states.ts +++ /dev/null @@ -1,382 +0,0 @@ -import iam = require('@aws-cdk/aws-iam'); -import cdk = require('@aws-cdk/cdk'); -import { Condition } from './asl-condition'; -import { IStepFunctionsTaskResource, StepFunctionsTaskResourceProps } from './task-resource'; - -export interface StateMachineDefinitionProps { - timeoutSeconds?: number; -} - -export interface INextState { - firstState(): State; -} - -export class StateMachineDefinition extends cdk.Construct { - /** - * Used to find this Construct back in the construct tree - */ - public readonly isStateMachine = true; - - private readonly states: State[] = []; - private startState?: State; - private readonly timeoutSeconds?: number; - private readonly policyStatements = new Array(); - - constructor(parent: cdk.Construct, id: string, props: StateMachineDefinitionProps = {}) { - super(parent, id); - this.timeoutSeconds = props.timeoutSeconds; - } - - public _addState(state: State) { - this.states.push(state); - if (this.startState === undefined) { - this.startState = state; - } - } - - public addToRolePolicy(statement: cdk.PolicyStatement) { - this.policyStatements.push(statement); - } - - public addPolicyStatementsToRole(role: iam.Role) { - for (const s of this.policyStatements) { - role.addToPolicy(s); - } - } - - public toStateMachine(): any { - if (!this.startState) { - throw new Error('No states added to StateMachine'); - } - - const states: any = {}; - for (const s of this.states) { - states[s.stateId] = s.toState(); - } - - // FIXME: Check all states are reachable. - - return { - StartAt: this.startState.stateId, - States: states, - TimeoutSeconds: this.timeoutSeconds, - Version: '1.0', - }; - } -} - -export interface StateProps { - type: StateType; - inputPath?: string; - outputPath?: string; -} - -export interface INextable { - next(state: State): void; -} - -export abstract class State extends cdk.Construct { - private readonly type: StateType; - private readonly inputPath?: string; - private readonly outputPath?: string; - - constructor(parent: StateMachineDefinition, id: string, props: StateProps) { - super(parent, id); - - this.type = props.type; - this.inputPath = props.inputPath; - this.outputPath = props.outputPath; - - parent._addState(this); - } - - /** - * Return the name of this state - */ - public get stateId(): string { - const sm = this.encompassingStateMachine(); - return this.ancestors(sm).map(p => p.id).join('/'); - } - - public toState(): any { - return { - Type: this.type, - InputPath: this.inputPath, - OutputPath: this.outputPath, - }; - } - - /** - * Find the top-level StateMachine we're part of - */ - private encompassingStateMachine(): StateMachineDefinition { - let curr: cdk.Construct | undefined = this; - while (curr && !isStateMachine(curr)) { - curr = curr.parent; - } - if (!curr) { - throw new Error('Could not find encompassing StateMachine'); - } - return curr; - } -} - -function isStateMachine(construct: cdk.Construct): construct is StateMachineDefinition { - return (construct as any).isStateMachine; -} - -export interface TaskProps { - resource: IStepFunctionsTaskResource; - inputPath?: string; - outputPath?: string; - resultPath?: string; - timeoutSeconds?: number; - heartbeatSeconds?: number; -} - -export class Task extends State implements INextable { - private _next?: State; - private readonly resource: IStepFunctionsTaskResource; - private readonly resultPath?: string; - private readonly timeoutSeconds?: number; - private readonly heartbeatSeconds?: number; - private readonly resourceProps: StepFunctionsTaskResourceProps; - - constructor(parent: StateMachineDefinition, id: string, props: TaskProps) { - super(parent, id, { - type: StateType.Task, - inputPath: props.inputPath, - outputPath: props.outputPath - }); - this.resource = props.resource; - this.resultPath = props.resultPath; - this.timeoutSeconds = props.timeoutSeconds; - this.heartbeatSeconds = props.heartbeatSeconds; - - this.resourceProps = this.resource.asStepFunctionsTaskResource(this); - } - - /** - * Add a policy statement to the role that ultimately executes this - */ - public addToRolePolicy(statement: cdk.PolicyStatement) { - (this.parent as StateMachineDefinition).addToRolePolicy(statement); - } - - public next(state: State) { - this._next = state; - } - - public toState(): any { - return { - ...super.toState(), - Resource: this.resourceProps.resourceArn, - ResultPath: this.resultPath, - Next: this._next ? this._next.stateId : undefined, - End: this._next ? undefined : true, - TimeoutSeconds: this.timeoutSeconds, - HeartbeatSeconds: this.heartbeatSeconds, - }; - } -} - -export interface PassProps { - inputPath?: string; - outputPath?: string; -} - -export class Pass extends State implements INextable { - private _next?: State; - - constructor(parent: StateMachineDefinition, id: string, props: PassProps = {}) { - super(parent, id, { - type: StateType.Pass, - inputPath: props.inputPath, - outputPath: props.outputPath - }); - } - - public next(state: State) { - this._next = state; - } - - public toState(): any { - return { - ...super.toState(), - Next: this._next ? this._next.stateId : undefined, - End: this._next ? undefined : true, - }; - } -} - -export interface WaitProps { - seconds?: number; - timestamp?: string; - - secondsPath?: string; - timestampPath?: string; -} - -export class Wait extends State implements INextable { - private _next?: State; - private readonly seconds?: number; - private readonly timestamp?: string; - private readonly secondsPath?: string; - private readonly timestampPath?: string; - - constructor(parent: StateMachineDefinition, id: string, props: WaitProps) { - // FIXME: Validate input - - super(parent, id, { - type: StateType.Wait, - }); - - this.seconds = props.seconds; - this.timestamp = props.timestamp; - this.secondsPath = props.secondsPath; - this.timestampPath = props.timestampPath; - } - - public next(state: State) { - this._next = state; - } - - public toState(): any { - return { - ...super.toState(), - Seconds: this.seconds, - Timestamp: this.timestamp, - SecondsPath: this.secondsPath, - TimestampPath: this.timestampPath, - Next: this._next ? this._next.stateId : undefined, - End: this._next ? undefined : true, - }; - } -} - -export interface ParallelProps { - branches: StateMachineDefinition[] - inputPath?: string; - outputPath?: string; - resultPath?: string; -} - -export class Parallel extends State implements INextable { - private _next?: State; - private readonly branches: StateMachineDefinition[]; - private readonly resultPath?: string; - - constructor(parent: StateMachineDefinition, id: string, props: ParallelProps) { - super(parent, id, { - type: StateType.Parallel, - inputPath: props.inputPath, - outputPath: props.outputPath - }); - this.branches = props.branches; - this.resultPath = props.resultPath; - } - - public next(state: State) { - this._next = state; - } - - public toState(): any { - return { - ...super.toState(), - ResultPath: this.resultPath, - Next: this._next ? this._next.stateId : undefined, - End: this._next ? undefined : true, - Branches: this.branches.map(b => b.toStateMachine()) - }; - } -} - -export interface ChoiceBranch { - condition: Condition; - next: State; -} - -export interface ChoiceProps { - inputPath?: string; - outputPath?: string; -} - -export class Choice extends State { - private readonly choices: ChoiceBranch[] = []; - private default?: State; - - constructor(parent: StateMachineDefinition, id: string, props: ChoiceProps = {}) { - super(parent, id, { - type: StateType.Choice, - inputPath: props.inputPath, - outputPath: props.outputPath - }); - } - - public on(condition: Condition, next: State) { - this.choices.push({ condition, next }); - } - - public otherwise(next: State) { - this.default = next; - } - - public toState(): any { - return { - ...super.toState(), - Choices: this.choices.map(c => this.renderChoice(c)), - Default: this.default ? this.default.stateId : undefined - }; - } - - private renderChoice(c: ChoiceBranch) { - return { - ...c.condition.toCondition(), - Next: c.next.stateId - }; - } -} - -export interface FailProps { - error: string; - cause: string; -} - -export class Fail extends State { - private readonly error: string; - private readonly cause: string; - - constructor(parent: StateMachineDefinition, id: string, props: FailProps) { - super(parent, id, { - type: StateType.Fail - }); - this.error = props.error; - this.cause = props.cause; - } - - public toState(): any { - return { - ...super.toState(), - Error: this.error, - Cause: this.cause - }; - } -} - -export class Succeed extends State { - constructor(parent: StateMachineDefinition, id: string) { - super(parent, id, { - type: StateType.Succeed - }); - } -} - -export enum StateType { - Pass = 'Pass', - Task = 'Task', - Choice = 'Choice', - Wait = 'Wait', - Succeed = 'Succeed', - Fail = 'Fail', - Parallel = 'Parallel' -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions/lib/index.ts index ba483e0750c9b..33ce4fa49c8d8 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/index.ts @@ -1,10 +1,19 @@ export * from './activity'; export * from './asl'; -export * from './asl-states'; +export * from './asl-external-api'; +export * from './asl-internal-api'; export * from './asl-condition'; export * from './state-machine'; -export * from './task-resource'; -export * from './asl-builder'; + +export * from './states/choice'; +export * from './states/fail'; +export * from './states/parallel'; +export * from './states/pass'; +export * from './states/state-machine-definition'; +export * from './states/state'; +export * from './states/succeed'; +export * from './states/task'; +export * from './states/wait'; // AWS::StepFunctions CloudFormation Resources: export * from './stepfunctions.generated'; diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts index 1eebcc569ce01..10c5252e96c03 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts @@ -11,6 +11,11 @@ export interface StateMachineProps { */ stateMachineName?: string; + /** + * Definition for this state machine + */ + definition: StateMachineDefinition; + /** * The execution role for the state machine service * @@ -22,12 +27,12 @@ export interface StateMachineProps { /** * Define a StepFunctions State Machine */ -export class StateMachine extends StateMachineDefinition { +export class StateMachine extends cdk.Construct { public readonly role: iam.Role; public readonly stateMachineName: StateMachineName; public readonly stateMachineArn: StateMachineArn; - constructor(parent: cdk.Construct, id: string, props: StateMachineProps = {}) { + constructor(parent: cdk.Construct, id: string, props: StateMachineProps) { super(parent, id); this.role = props.role || new iam.Role(this, 'Role', { @@ -37,8 +42,9 @@ export class StateMachine extends StateMachineDefinition { const resource = new cloudformation.StateMachineResource(this, 'Resource', { stateMachineName: props.stateMachineName, roleArn: this.role.roleArn, - // We may have objects added to us after creation - definitionString: new cdk.Token(() => cdk.CloudFormationJSON.stringify(this.toStateMachine())) + // Depending on usage, definition may change after our instantiation + // (because we're organized like a mutable object tree) + definitionString: new cdk.Token(() => cdk.CloudFormationJSON.stringify(props.definition.toStateMachine())) }); this.stateMachineName = resource.stateMachineName; diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts new file mode 100644 index 0000000000000..36ad70f4507bc --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts @@ -0,0 +1,92 @@ +import { Condition } from '../asl-condition'; +import { IChainable, IStateChain } from '../asl-external-api'; +import { IInternalState, StateBehavior, StateType } from '../asl-internal-api'; +import { StateChain } from '../asl-state-chain'; +import { State } from './state'; +import { StateMachineDefinition } from './state-machine-definition'; + +interface ChoiceBranch { + condition: Condition; + next: IChainable; +} + +export interface ChoiceProps { + inputPath?: string; + outputPath?: string; +} + +export class Choice extends State { + private static Internals = class implements IInternalState { + public readonly stateBehavior: StateBehavior = { + canHaveCatch: false, + canHaveNext: false, + elidable: false + }; + + constructor(private readonly choice: Choice) { + } + + public get stateId(): string { + return this.choice.stateId; + } + + public renderState() { + const defaultTransitions = this.choice.transitions.filter(t => t.annotation === undefined); + if (defaultTransitions.length > 1) { + throw new Error('Can only have one default transition'); + } + const choices = this.choice.transitions.filter(t => t.annotation !== undefined); + + return { + ...this.choice.renderBaseState(), + Choices: choices.map(c => ({ + ...c.annotation, + Next: c.targetState.stateId + })), + Default: defaultTransitions.length > 0 ? defaultTransitions[0].targetState.stateId : undefined + }; + } + + public next(_targetState: IInternalState): void { + throw new Error("Cannot chain onto a Choice state. Use the state's .on() or .otherwise() instead."); + } + + public catch(_targetState: IInternalState, _errors: string[]): void { + throw new Error("Cannot catch errors on a Choice."); + } + }; + public readonly stateBehavior: StateBehavior = { + canHaveCatch: false, + canHaveNext: false, + elidable: false + }; + + private readonly choices: ChoiceBranch[] = []; + private hasDefault = false; + + constructor(parent: StateMachineDefinition, id: string, props: ChoiceProps = {}) { + super(parent, id, { + Type: StateType.Choice, + InputPath: props.inputPath, + OutputPath: props.outputPath, + }); + } + + public on(condition: Condition, next: IChainable): Choice { + this.choices.push({ condition, next }); + return this; + } + + public otherwise(next: IChainable): Choice { + if (this.hasDefault) { + throw new Error('Can only have one default transition'); + } + this.hasDefault = true; + this.addTransition(next.toStateChain().startState, undefined); + return this; + } + + public toStateChain(): IStateChain { + return new StateChain(new Choice.Internals(this)); + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/fail.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/fail.ts new file mode 100644 index 0000000000000..69ade00340cd1 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/fail.ts @@ -0,0 +1,51 @@ +import { IStateChain } from '../asl-external-api'; +import { IInternalState, StateBehavior, StateType } from '../asl-internal-api'; +import { StateChain } from '../asl-state-machine'; +import { State } from './state'; +import { StateMachineDefinition } from './state-machine-definition'; + +export interface FailProps { + error: string; + cause: string; +} + +export class Fail extends State { + private static Internals = class implements IInternalState { + public readonly stateBehavior: StateBehavior = { + canHaveCatch: false, + canHaveNext: false, + elidable: false + }; + + constructor(private readonly fail: Fail) { + } + + public get stateId(): string { + return this.fail.stateId; + } + + public renderState() { + return this.fail.renderBaseState(); + } + + public next(_targetState: IInternalState): void { + throw new Error("Cannot chain onto a Fail state. This ends the state machine."); + } + + public catch(_targetState: IInternalState, _errors: string[]): void { + throw new Error("Cannot catch errors on a Fail."); + } + }; + + constructor(parent: StateMachineDefinition, id: string, props: FailProps) { + super(parent, id, { + Type: StateType.Fail, + Error: props.error, + Cause: props.cause + }); + } + + public toStateChain(): IStateChain { + return new StateChain(new Fail.Internals(this)); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts new file mode 100644 index 0000000000000..1e4ecabb95ae8 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts @@ -0,0 +1,85 @@ +import cdk = require('@aws-cdk/cdk'); +import { Errors, IStateChain, RetryProps } from '../asl-external-api'; +import { IInternalState, StateBehavior, StateType } from '../asl-internal-api'; +import { StateChain } from '../asl-state-chain'; +import { State } from './state'; +import { StateMachineDefinition } from './state-machine-definition'; +import { renderNextEnd, renderRetry } from './util'; + +export interface ParallelProps { + inputPath?: string; + outputPath?: string; + resultPath?: string; +} + +export class Parallel extends State { + private static Internals = class implements IInternalState { + public readonly stateBehavior: StateBehavior = { + canHaveCatch: true, + canHaveNext: true, + elidable: false, + }; + + constructor(private readonly parallel: Parallel) { + } + + public get stateId(): string { + return this.parallel.stateId; + } + + public renderState() { + const catches = this.parallel.transitions.filter(t => t.annotation !== undefined); + const regularTransitions = this.parallel.transitions.filter(t => t.annotation === undefined); + + if (regularTransitions.length > 1) { + throw new Error(`State "${this.stateId}" can only have one outgoing transition`); + } + + return { + ...this.parallel.renderBaseState(), + ...renderNextEnd(regularTransitions), + Catch: catches.length === 0 ? undefined : catches.map(c => c.annotation), + Retry: new cdk.Token(() => this.parallel.retries.length === 0 ? undefined : this.parallel.retries.map(renderRetry)), + }; + } + + public next(targetState: IInternalState): void { + this.parallel.addNextTransition(targetState); + } + + public catch(targetState: IInternalState, errors: string[]): void { + this.parallel.addTransition(targetState, { + ErrorEquals: errors, + Next: targetState.stateId + }); + } + }; + + private readonly branches: StateMachineDefinition[] = []; + private readonly retries = new Array(); + + constructor(parent: StateMachineDefinition, id: string, props: ParallelProps = {}) { + super(parent, id, { + Type: StateType.Parallel, + InputPath: props.inputPath, + OutputPath: props.outputPath, + ResultPath: props.resultPath, + Branches: new cdk.Token(() => this.branches.map(b => b.renderStateMachine())) + }); + } + + public parallel(definition: StateMachineDefinition) { + this.branches.push(definition); + } + + public retry(props: RetryProps = {}) { + if (!props.errors) { + props.errors = [Errors.all]; + } + this.retries.push(props); + } + + public toStateChain(): IStateChain { + return new StateChain(new Parallel.Internals(this)); + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts new file mode 100644 index 0000000000000..ec8ba43aaabad --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts @@ -0,0 +1,62 @@ +import { IStateChain } from '../asl-external-api'; +import { IInternalState, StateBehavior, StateType } from '../asl-internal-api'; +import { StateChain } from '../asl-state-chain'; +import { State } from './state'; +import { StateMachineDefinition } from './state-machine-definition'; +import { renderNextEnd } from './util'; + +export interface PassProps { + inputPath?: string; + outputPath?: string; + elidable?: boolean; +} + +export class Pass extends State { + private static Internals = class implements IInternalState { + constructor(private readonly pass: Pass) { + } + + public get stateId(): string { + return this.pass.stateId; + } + + public get stateBehavior(): StateBehavior { + return { + canHaveNext: true, + canHaveCatch: false, + elidable: this.pass.elidable + }; + } + + public renderState() { + const regularTransitions = this.pass.getTransitions(false); + + return { + ...this.pass.renderBaseState(), + ...renderNextEnd(regularTransitions), + }; + } + + public next(targetState: IInternalState): void { + this.pass.addNextTransition(targetState); + } + + public catch(_targetState: IInternalState, _errors: string[]): void { + throw new Error("Cannot catch errors on a Pass."); + } + }; + private readonly elidable: boolean; + + constructor(parent: StateMachineDefinition, id: string, props: PassProps = {}) { + super(parent, id, { + Type: StateType.Pass, + InputPath: props.inputPath, + OutputPath: props.outputPath + }); + this.elidable = (props.elidable || false) && !props.inputPath && !props.outputPath; + } + + public toStateChain(): IStateChain { + return new StateChain(new Pass.Internals(this)); + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/state-machine-definition.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/state-machine-definition.ts new file mode 100644 index 0000000000000..2e85d45a41dee --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/state-machine-definition.ts @@ -0,0 +1,74 @@ +import iam = require('@aws-cdk/aws-iam'); +import cdk = require('@aws-cdk/cdk'); +import { IChainable, IState, IStateChain } from "../asl-external-api"; + +export interface StateMachineDefinitionProps { + timeoutSeconds?: number; +} + +export class StateMachineDefinition extends cdk.Construct implements IChainable { + /** + * Used to find this Construct back in the construct tree + */ + public readonly isStateMachine = true; + + private readonly timeoutSeconds?: number; + private readonly policyStatements = new Array(); + private readonly states = new Array(); + private startState?: IState; + private policyRole?: iam.Role; + private sm?: IStateChain; + + constructor(parent: cdk.Construct, id: string, props: StateMachineDefinitionProps = {}) { + super(parent, id); + this.timeoutSeconds = props.timeoutSeconds; + } + + public start(state: IState): IStateChain { + this.startState = state; + return state.toStateChain(); + } + + public addToRolePolicy(statement: cdk.PolicyStatement) { + // This may be called before and after attaching to a StateMachine. + // Cache the policy statements added in this way if before attaching, + // otherwise attach to role directly. + if (this.policyRole) { + this.policyRole.addToPolicy(statement); + } else { + this.policyStatements.push(statement); + } + } + + public addPolicyStatementsToRole(role: iam.Role) { + // Add all cached policy statements, then remember the policy + // for future additions. + for (const s of this.policyStatements) { + role.addToPolicy(s); + } + this.policyStatements.splice(0); // Clear array + this.policyRole = role; + } + + public toStateChain(): IStateChain { + if (!this.sm) { + throw new Error('No state machine define with .define()'); + } + + // FIXME: Use somewhere + Array.isArray(this.timeoutSeconds); + + return this.sm; + } + + public renderStateMachine(): any { + return {}; + } + + public _addState(state: IState) { + if (this.startState === undefined) { + this.startState = state; + } + this.states.push(state); + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts new file mode 100644 index 0000000000000..f26e54db53e83 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts @@ -0,0 +1,71 @@ +import cdk = require('@aws-cdk/cdk'); +import { IChainable, IStateChain } from "../asl-external-api"; +import { IInternalState, Transition } from '../asl-internal-api'; +import { StateMachineDefinition } from './state-machine-definition'; + +export abstract class State extends cdk.Construct implements IChainable { + protected readonly transitions = new Array(); + + constructor(parent: StateMachineDefinition, id: string, private readonly options: any) { + super(parent, id); + + parent._addState(this); + } + + public abstract toStateChain(): IStateChain; + + /** + * Convenience function to immediately go into State Machine mode + */ + public then(sm: IChainable): IStateChain { + return this.toStateChain().then(sm); + } + + public catch(handler: IChainable, ...errors: string[]): IStateChain { + return this.toStateChain().catch(handler, ...errors); + } + + /** + * Find the top-level StateMachine we're part of + */ + protected containingStateMachine(): StateMachineDefinition { + let curr: cdk.Construct | undefined = this; + while (curr && !isStateMachine(curr)) { + curr = curr.parent; + } + if (!curr) { + throw new Error('Could not find encompassing StateMachine'); + } + return curr; + } + + protected renderBaseState(): any { + return this.options; + } + + /** + * Return the name of this state + */ + protected get stateId(): string { + return this.ancestors(this.containingStateMachine()).map(x => x.id).join('/'); + } + + protected addTransition(targetState: IInternalState, annotation?: any) { + this.transitions.push({ targetState, annotation }); + } + + protected getTransitions(withAnnotation: boolean): Transition[] { + return this.transitions.filter(t => (t.annotation === undefined) === withAnnotation); + } + + protected addNextTransition(targetState: IInternalState): void { + if (this.getTransitions(false).length > 0) { + throw new Error(`State ${this.stateId} already has a Next transition`); + } + this.addTransition(targetState); + } +} + +function isStateMachine(construct: cdk.Construct): construct is StateMachineDefinition { + return (construct as any).isStateMachine; +} diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts new file mode 100644 index 0000000000000..ad0f72476f68d --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts @@ -0,0 +1,52 @@ +import { IStateChain } from '../asl-external-api'; +import { IInternalState, StateBehavior, StateType } from '../asl-internal-api'; +import { StateChain } from '../asl-state-machine'; +import { State } from './state'; +import { StateMachineDefinition } from './state-machine-definition'; + +export interface SucceedProps { + elidable?: boolean; +} + +export class Succeed extends State { + private static Internals = class implements IInternalState { + constructor(private readonly succeed: Succeed) { + } + + public get stateId(): string { + return this.succeed.stateId; + } + + public renderState() { + return this.succeed.renderBaseState(); + } + + public get stateBehavior(): StateBehavior { + return { + canHaveCatch: false, + canHaveNext: false, + elidable: this.succeed.elidable, + }; + } + + public next(_targetState: IInternalState): void { + throw new Error("Cannot chain onto a Succeed state; this ends the state machine."); + } + + public catch(_targetState: IInternalState, _errors: string[]): void { + throw new Error("Cannot catch errors on a Succeed."); + } + }; + private readonly elidable: boolean; + + constructor(parent: StateMachineDefinition, id: string, props: SucceedProps = {}) { + super(parent, id, { + Type: StateType.Succeed + }); + this.elidable = props.elidable || false; + } + + public toStateChain(): IStateChain { + return new StateChain(new Succeed.Internals(this)); + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts new file mode 100644 index 0000000000000..1aafbae0593e6 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts @@ -0,0 +1,108 @@ +import cdk = require('@aws-cdk/cdk'); +import { Errors, IStateChain, RetryProps } from '../asl-external-api'; +import { IInternalState, StateBehavior, StateType } from '../asl-internal-api'; +import { StateChain } from '../asl-state-chain'; +import { State } from './state'; +import { StateMachineDefinition } from './state-machine-definition'; +import { renderNextEnd, renderRetry } from './util'; + +/** + * Interface for objects that can be invoked in a Task state + */ +export interface IStepFunctionsTaskResource { + /** + * Return the properties required for using this object as a Task resource + */ + asStepFunctionsTaskResource(callingTask: Task): StepFunctionsTaskResourceProps; +} + +export interface StepFunctionsTaskResourceProps { + resourceArn: cdk.Arn; +} + +export interface TaskProps { + resource: IStepFunctionsTaskResource; + inputPath?: string; + outputPath?: string; + resultPath?: string; + timeoutSeconds?: number; + heartbeatSeconds?: number; +} + +export class Task extends State { + private static Internals = class implements IInternalState { + public readonly stateBehavior: StateBehavior = { + canHaveCatch: true, + canHaveNext: true, + elidable: false, + }; + + constructor(private readonly task: Task) { + } + + public get stateId(): string { + return this.task.stateId; + } + + public renderState() { + const catches = this.task.transitions.filter(t => t.annotation !== undefined); + const regularTransitions = this.task.transitions.filter(t => t.annotation === undefined); + + if (regularTransitions.length > 1) { + throw new Error(`State "${this.stateId}" can only have one outgoing transition`); + } + + return { + ...this.task.renderBaseState(), + ...renderNextEnd(regularTransitions), + Catch: catches.length === 0 ? undefined : catches.map(c => c.annotation), + Retry: this.task.retries.length === 0 ? undefined : this.task.retries.map(renderRetry), + }; + } + + public next(targetState: IInternalState): void { + this.task.addNextTransition(targetState); + } + + public catch(targetState: IInternalState, errors: string[]): void { + this.task.addTransition(targetState, { + ErrorEquals: errors, + Next: targetState.stateId + }); + } + }; + + private readonly resourceProps: StepFunctionsTaskResourceProps; + private readonly retries = new Array(); + + constructor(parent: StateMachineDefinition, id: string, props: TaskProps) { + super(parent, id, { + Type: StateType.Task, + InputPath: props.inputPath, + OutputPath: props.outputPath, + Resource: new cdk.Token(() => this.resourceProps.resourceArn), + ResultPath: props.resultPath, + TimeoutSeconds: props.timeoutSeconds, + HeartbeatSeconds: props.heartbeatSeconds + }); + this.resourceProps = props.resource.asStepFunctionsTaskResource(this); + } + + /** + * Add a policy statement to the role that ultimately executes this + */ + public addToRolePolicy(statement: cdk.PolicyStatement) { + this.containingStateMachine().addToRolePolicy(statement); + } + + public retry(props: RetryProps = {}) { + if (!props.errors) { + props.errors = [Errors.all]; + } + this.retries.push(props); + } + + public toStateChain(): IStateChain { + return new StateChain(new Task.Internals(this)); + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/util.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/util.ts new file mode 100644 index 0000000000000..753ea82307698 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/util.ts @@ -0,0 +1,22 @@ +import { RetryProps } from "../asl-external-api"; +import { Transition } from "../asl-internal-api"; + +export function renderNextEnd(transitions: Transition[]) { + if (transitions.some(t => t.annotation !== undefined)) { + throw new Error('renderNextEnd() can only be used on default transitions'); + } + + return { + Next: transitions.length > 0 ? transitions[0].targetState.stateId : undefined, + End: transitions.length > 0 ? undefined : true, + }; +} + +export function renderRetry(retry: RetryProps) { + return { + ErrorEquals: retry.errors, + IntervalSeconds: retry.intervalSeconds, + MaxAttempts: retry.maxAttempts, + BackoffRate: retry.backoffRate + }; +} diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/wait.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/wait.ts new file mode 100644 index 0000000000000..384d8385f216e --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/wait.ts @@ -0,0 +1,61 @@ +import { IStateChain } from '../asl-external-api'; +import { IInternalState, StateBehavior, StateType } from '../asl-internal-api'; +import { StateChain } from '../asl-state-chain'; +import { State } from './state'; +import { StateMachineDefinition } from './state-machine-definition'; +import { renderNextEnd } from './util'; + +export interface WaitProps { + seconds?: number; + timestamp?: string; + + secondsPath?: string; + timestampPath?: string; +} + +export class Wait extends State { + private static Internals = class implements IInternalState { + public readonly stateBehavior: StateBehavior = { + canHaveCatch: false, + canHaveNext: true, + elidable: false + }; + + constructor(private readonly wait: Wait) { + } + + public get stateId(): string { + return this.wait.stateId; + } + + public renderState() { + return { + ...this.wait.renderBaseState(), + ...renderNextEnd(this.wait.transitions), + }; + } + + public next(targetState: IInternalState): void { + this.wait.addTransition(targetState); + } + + public catch(_targetState: IInternalState, _errors: string[]): void { + throw new Error("Cannot catch errors on a Wait."); + } + }; + + constructor(parent: StateMachineDefinition, id: string, props: WaitProps) { + // FIXME: Validate input + super(parent, id, { + Type: StateType.Task, + Seconds: props.seconds, + Timestamp: props.timestamp, + SecondsPath: props.secondsPath, + TimestampPath: props.timestampPath + }); + } + + public toStateChain(): IStateChain { + return new StateChain(new Wait.Internals(this)); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/task-resource.ts b/packages/@aws-cdk/aws-stepfunctions/lib/task-resource.ts deleted file mode 100644 index d0752176ef11c..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions/lib/task-resource.ts +++ /dev/null @@ -1,16 +0,0 @@ -import cdk = require('@aws-cdk/cdk'); -import { Task } from './asl-states'; - -/** - * Interface for objects that can be invoked in a Task state - */ -export interface IStepFunctionsTaskResource { - /** - * Return the properties required for using this object as a Task resource - */ - asStepFunctionsTaskResource(callingTask: Task): StepFunctionsTaskResourceProps; -} - -export interface StepFunctionsTaskResourceProps { - resourceArn: cdk.Arn; -} diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts deleted file mode 100644 index 8ffd9f91402f6..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.amazon-states-language.ts +++ /dev/null @@ -1,392 +0,0 @@ -import { Test } from 'nodeunit'; -import { asl } from './../lib'; - -function roundtrip(obj: any) { - return JSON.parse(JSON.stringify(obj)); -} - -export = { - /* - 'Hello World example state machine'(test: Test) { - test.deepEqual( - JSON.parse( - new asl.StateMachine({ - comment: "A simple minimal example of the States language", - startAt: "Hello World", - states: new asl.States({ - "Hello World": new asl.TaskState({ - resource: "arn:aws:lambda:us-east-1:123456789012:function:HelloWorld", - end: true - }) - }) - }).definitionString() - ), - { - Comment: "A simple minimal example of the States language", - StartAt: "Hello World", - States: { - "Hello World": { - Type: "Task", - Resource: "arn:aws:lambda:us-east-1:123456789012:function:HelloWorld", - End: true - } - } - } - ); - test.done(); - }, - */ - - 'Models Complex retry scenarios example'(test: Test) { - test.deepEqual( - roundtrip( - new asl.TaskState({ - resource: "arn:aws:swf:us-east-1:123456789012:task:X", - next: "Y", - retry: new asl.Retriers([ - new asl.Retrier({ - errorEquals: ["ErrorA", "ErrorB"], - intervalSeconds: 1, - backoffRate: 2, - maxAttempts: 2 - }), - new asl.Retrier({ - errorEquals: ["ErrorC"], - intervalSeconds: 5 - }) - ]), - catch: new asl.Catchers([ - new asl.Catcher({ - errorEquals: [asl.ErrorCode.ALL], - next: "Z" - }) - ]) - }) - ), - { - Type: "Task", - Resource: "arn:aws:swf:us-east-1:123456789012:task:X", - Next: "Y", - Retry: [ - { - ErrorEquals: ["ErrorA", "ErrorB"], - IntervalSeconds: 1, - BackoffRate: 2, - MaxAttempts: 2 - }, - { - ErrorEquals: ["ErrorC"], - IntervalSeconds: 5 - } - ], - Catch: [ - { - ErrorEquals: ["States.ALL"], - Next: "Z" - } - ] - } - ); - test.done(); - }, - 'Pass state example'(test: Test) { - test.deepEqual( - roundtrip( - new asl.PassState({ - result: { - "x-datum": 0.381018, - "y-datum": 622.2269926397355 - }, - resultPath: "$.coords", - next: "End" - }) - ), - { - Type: "Pass", - Result: { - "x-datum": 0.381018, - "y-datum": 622.2269926397355 - }, - ResultPath: "$.coords", - Next: "End" - } - ); - test.done(); - }, - 'Task state example'(test: Test) { - test.deepEqual( - roundtrip( - new asl.TaskState({ - comment: "Task State example", - resource: "arn:aws:swf:us-east-1:123456789012:task:HelloWorld", - next: "NextState", - timeoutSeconds: 300, - heartbeatSeconds: 60 - }) - ), - { - Comment: "Task State example", - Type: "Task", - Resource: "arn:aws:swf:us-east-1:123456789012:task:HelloWorld", - Next: "NextState", - TimeoutSeconds: 300, - HeartbeatSeconds: 60 - } - ); - test.done(); - }, - 'Choice state example'(test: Test) { - test.deepEqual( - roundtrip( - new asl.ChoiceState({ - choices: new asl.ChoiceRules( - new asl.ChoiceRule({ - comparisonOperation: new asl.NotComparisonOperation( - new asl.StringEqualsComparisonOperation({ - variable: "$.type", - value: "Private" - }) - ), - next: "Public" - }), - new asl.ChoiceRule({ - comparisonOperation: new asl.AndComparisonOperation( - new asl.NumericGreaterThanEqualsComparisonOperation({ - variable: "$.value", - value: 20 - }), - new asl.NumericLessThanComparisonOperation({ - variable: "$.value", - value: 30 - }) - ), - next: "ValueInTwenties" - }) - ), - default: "DefaultState" - }) - ), - { - Type: "Choice", - Choices: [ - { - Not: { - Variable: "$.type", - StringEquals: "Private" - }, - Next: "Public" - }, - { - And: [ - { - Variable: "$.value", - NumericGreaterThanEquals: 20 - }, - { - Variable: "$.value", - NumericLessThan: 30 - } - ], - Next: "ValueInTwenties" - } - ], - Default: "DefaultState" - } - ); - test.done(); - }, - 'Wait state examples'(test: Test) { - test.deepEqual( - roundtrip(new asl.WaitState({ seconds: 10, next: "NextState" })), - { - Type: "Wait", - Seconds: 10, - Next: "NextState" - } - ); - test.deepEqual( - roundtrip(new asl.WaitState({ timestamp: "2016-03-14T01:59:00Z", next: "NextState" })), - { - Type: "Wait", - Timestamp: "2016-03-14T01:59:00Z", - Next: "NextState" - } - ); - test.deepEqual( - roundtrip(new asl.WaitState({ timestampPath: "$.expirydate", next: "NextState" })), - { - Type: "Wait", - TimestampPath: "$.expirydate", - Next: "NextState" - } - ); - test.done(); - }, - 'Succeed state example'(test: Test) { - test.deepEqual(roundtrip(new asl.SucceedState()), { Type: "Succeed" }); - test.done(); - }, - 'Fail state example'(test: Test) { - test.deepEqual( - roundtrip( - new asl.FailState({ - error: "ErrorA", - cause: "Kaiju attack" - }) - ), - { - Type: "Fail", - Error: "ErrorA", - Cause: "Kaiju attack" - } - ); - test.done(); - }, - 'Parallel state example'(test: Test) { - test.deepEqual( - roundtrip( - new asl.ParallelState({ - branches: new asl.Branches([ - new asl.Branch({ - startAt: "LookupAddress", - states: new asl.States({ - LookupAddress: new asl.TaskState({ - resource: "arn:aws:lambda:us-east-1:123456789012:function:AddressFinder", - end: true - }) - }) - }), - new asl.Branch({ - startAt: "LookupPhone", - states: new asl.States({ - LookupPhone: new asl.TaskState({ - resource: "arn:aws:lambda:us-east-1:123456789012:function:PhoneFinder", - end: true - }) - }) - }) - ]), - next: "NextState" - }) - ), - { - Type: "Parallel", - Branches: [ - { - StartAt: "LookupAddress", - States: { - LookupAddress: { - Type: "Task", - Resource: "arn:aws:lambda:us-east-1:123456789012:function:AddressFinder", - End: true - } - } - }, - { - StartAt: "LookupPhone", - States: { - LookupPhone: { - Type: "Task", - Resource: "arn:aws:lambda:us-east-1:123456789012:function:PhoneFinder", - End: true - } - } - } - ], - Next: "NextState" - } - ); - test.done(); - }, - 'Validates state names are unique'(test: Test) { - test.throws(() => { - new asl.StateMachine({ - startAt: "foo", - states: new asl.States({ - foo: new asl.PassState({ next: "bar" }), - bar: new asl.PassState({ next: "bat" }), - bat: new asl.ParallelState({ - branches: new asl.Branches([ - new asl.Branch({ - startAt: "foo", - states: new asl.States({ - foo: new asl.PassState({ end: true }) - }) - }) - ]), - end: true - }) - }) - }); - }); - test.done(); - }, - 'Validates startAt is in states'(test: Test) { - test.throws(() => { - new asl.StateMachine({ - startAt: "notFoo", - states: new asl.States({ - foo: new asl.SucceedState() - }) - }); - }); - test.throws(() => { - new asl.Branch({ - startAt: "notFoo", - states: new asl.States({ - foo: new asl.SucceedState() - }) - }); - }); - test.done(); - }, - 'Validates state names aren\'t too long'(test: Test) { - test.throws(() => { - new asl.States({ - [new Array(200).join('x')]: new asl.SucceedState() - }); - }); - test.done(); - }, - 'Validates next states are known'(test: Test) { - test.throws(() => { - new asl.States({ - foo: new asl.PassState({ next: "unknown" }) - }); - }); - test.done(); - }, - 'Validates Error.ALL appears alone'(test: Test) { - test.throws(() => { - new asl.Retrier({ - errorEquals: ["a", "b", asl.ErrorCode.ALL, "c"] - }); - }); - test.done(); - }, - 'Validates error names'(test: Test) { - test.throws(() => { - new asl.Retrier({ - errorEquals: ["States.MY_ERROR"] - }); - }); - test.done(); - }, - 'Valdiate Error.ALL must appear last'(test: Test) { - test.throws(() => { - new asl.Retriers([ - new asl.Retrier({ errorEquals: ["SomeOtherError", "BeforeERROR.ALL"] }), - new asl.Retrier({ errorEquals: [asl.ErrorCode.ALL] }), - new asl.Retrier({ errorEquals: ["SomeOtherError", "AfterERROR.ALL"] }) - ]); - }); - test.throws(() => { - new asl.Catchers([ - new asl.Retrier({ errorEquals: ["SomeOtherError", "BeforeERROR.ALL"] }), - new asl.Catcher({ errorEquals: [asl.ErrorCode.ALL], next: "" }), - new asl.Catcher({ errorEquals: ["SomeOtherError", "AfterERROR.ALL"], next: "" }) - ]); - }); - test.done(); - } -}; diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts new file mode 100644 index 0000000000000..f40169f250032 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts @@ -0,0 +1,7 @@ +import { Test } from 'nodeunit'; + +export = { + 'Tasks can add permissions to the execution role'(test: Test) { + test.done(); + }, +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts new file mode 100644 index 0000000000000..50d0638ffea3e --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts @@ -0,0 +1,625 @@ +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import stepfunctions = require('../lib'); + +export = { + 'Basic composition': { + 'A single task is a State Machine'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); + + // WHEN + new stepfunctions.Pass(sm, 'Some State'); + + // THEN + test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + StartAt: 'Some State', + States: { + 'Some State': { Type: 'Pass', End: true } + } + }); + + test.done(); + }, + + 'A sequence of two tasks is a State Machine'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); + + // WHEN + const task1 = new stepfunctions.Pass(sm, 'State One'); + const task2 = new stepfunctions.Pass(sm, 'State Two'); + + task1.then(task2); + + // THEN + test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + StartAt: 'State One', + States: { + 'State One': { Type: 'Pass', Next: 'State Two' }, + 'State Two': { Type: 'Pass', End: true }, + } + }); + + test.done(); + }, + + 'A chain can be appended to'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); + + const task1 = new stepfunctions.Pass(sm, 'State One'); + const task2 = new stepfunctions.Pass(sm, 'State Two'); + const task3 = new stepfunctions.Pass(sm, 'State Three'); + + // WHEN + task1.then(task2).then(task3); + + // THEN + test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + StartAt: 'State One', + States: { + 'State One': { Type: 'Pass', Next: 'State Two' }, + 'State Two': { Type: 'Pass', Next: 'State Three' }, + 'State Three': { Type: 'Pass', End: true }, + } + }); + + test.done(); + }, + + 'A state machine can be appended to another state machine'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); + + const task1 = new stepfunctions.Pass(sm, 'State One'); + const task2 = new stepfunctions.Pass(sm, 'State Two'); + const task3 = new stepfunctions.Pass(sm, 'State Three'); + + // WHEN + task1.then(task2.then(task3)); + + // THEN + test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + StartAt: 'State One', + States: { + 'State One': { Type: 'Pass', Next: 'State Two' }, + 'State Two': { Type: 'Pass', Next: 'State Three' }, + 'State Three': { Type: 'Pass', End: true }, + } + }); + + test.done(); + }, + + 'Two chained states must be in the same state machine definition'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const sm1 = new stepfunctions.StateMachineDefinition(stack, 'SM1'); + const sm2 = new stepfunctions.StateMachineDefinition(stack, 'SM2'); + + const pass1 = new stepfunctions.Pass(sm1, 'Pass1'); + const pass2 = new stepfunctions.Pass(sm2, 'Pass2'); + + // THEN + test.throws(() => { + pass1.then(pass2); + }); + + test.done(); + }, + + 'A state machine definition can be instantiated and chained'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); + const before = new stepfunctions.Pass(sm, 'Before'); + const after = new stepfunctions.Pass(sm, 'After'); + + // WHEN + before.then(new ReusableStateMachine(stack, 'Reusable')).then(after); + + // THEN + test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + StartAt: 'Before', + States: { + 'Before': { Type: 'Pass', Next: 'Reusable/Choice' }, + 'Reusable/Choice': { + Type: 'Choice', + Choices: [ + { Variable: '$.branch', StringEquals: 'left', Next: 'Reusable/Left Branch' }, + { Variable: '$.branch', StringEquals: 'right', Next: 'Reusable/Right Branch' }, + ] + }, + 'Reusable/Left Branch': { Type: 'Pass', Next: 'After' }, + 'Reusable/Right Branch': { Type: 'Pass', Next: 'After' }, + 'After': { Type: 'Pass', End: true }, + } + }); + + test.done(); + }, + + 'A success state cannot be chained onto'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); + + const succeed = new stepfunctions.Succeed(sm, 'Succeed'); + const pass = new stepfunctions.Pass(sm, 'Pass'); + + // WHEN + test.throws(() => { + succeed.then(pass); + }); + + test.done(); + }, + + 'A failure state cannot be chained onto'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); + const fail = new stepfunctions.Fail(sm, 'Fail', { error: 'X', cause: 'Y' }); + const pass = new stepfunctions.Pass(sm, 'Pass'); + + // WHEN + test.throws(() => { + fail.then(pass); + }); + + test.done(); + }, + + 'Parallels contains adhoc state machine definitions without scoping names'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); + + // WHEN + const para = new stepfunctions.Parallel(sm, 'Parallel'); + + // Key: the parent is the same parent as the top-level StateMachineDefinition. + const branch1 = new stepfunctions.StateMachineDefinition(stack, 'Branch1'); + branch1.start(new stepfunctions.Pass(branch1, 'One')) + .then(new stepfunctions.Pass(branch1, 'Two')); + + const branch2 = new stepfunctions.StateMachineDefinition(stack, 'Branch1'); + branch2.start(new stepfunctions.Pass(branch2, 'Three')); + + para.parallel(branch1); + para.parallel(branch2); + + // THEN + test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + StartAt: 'Parallel', + States: { + Parallel: { + Type: 'Parallel', + End: true, + Branches: [ + { + StartAt: 'One', + States: { + One: { Type: 'Pass', Next: 'Two' }, + Two: { Type: 'Pass', End: true }, + } + }, + { + StartAt: 'Three', + States: { + Three: { Type: 'Pass', End: true } + } + } + ] + } + } + }); + + test.done(); + }, + + 'Parallels can contain instantiated reusable definitions'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); + + // WHEN + const para = new stepfunctions.Parallel(sm, 'Parallel'); + para.parallel(new ReusableStateMachine(sm, 'Reusable1')); + para.parallel(new ReusableStateMachine(sm, 'Reusable2')); + + // THEN + test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + StartAt: 'Parallel', + States: { + Parallel: { + Type: 'Parallel', + End: true, + Branches: [ + { + StartAt: 'Reusable1/Choice', + States: { + 'Reusable1/Choice': { + Type: 'Choice', + Choices: [ + { Variable: '$.branch', StringEquals: 'left', Next: 'Reusable1/Left Branch' }, + { Variable: '$.branch', StringEquals: 'right', Next: 'Reusable1/Right Branch' }, + ] + }, + 'Reusable1/Left Branch': { Type: 'Pass', End: true }, + 'Reusable1/Right Branch': { Type: 'Pass', End: true }, + } + }, + { + StartAt: 'Reusable2/Choice', + States: { + 'Reusable2/Choice': { + Type: 'Choice', + Choices: [ + { Variable: '$.branch', StringEquals: 'left', Next: 'Reusable2/Left Branch' }, + { Variable: '$.branch', StringEquals: 'right', Next: 'Reusable2/Right Branch' }, + ] + }, + 'Reusable2/Left Branch': { Type: 'Pass', End: true }, + 'Reusable2/Right Branch': { Type: 'Pass', End: true }, + } + }, + ] + } + } + }); + + test.done(); + }, + + 'Chaining onto branched failure state ignores failure state'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); + + const yes = new stepfunctions.Pass(sm, 'Yes'); + const no = new stepfunctions.Fail(sm, 'No', { error: 'Failure', cause: 'Wrong branch' }); + const enfin = new stepfunctions.Pass(sm, 'Finally'); + const choice = new stepfunctions.Choice(sm, 'Choice') + .on(stepfunctions.Condition.stringEquals('$.foo', 'bar'), yes) + .otherwise(no); + + // WHEN + choice.then(enfin); + + // THEN + test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + StartAt: 'Choice', + States: { + Choice: { + Type: 'Choice', + Choices: [ + { Variable: '$.foo', StringEquals: 'bar', Next: 'Yes' }, + ], + Default: 'No', + }, + Yes: { Type: 'Pass', Next: 'Finally' }, + No: { Type: 'Fail', Error: 'Failure', Cause: 'Wrong branch' }, + Finally: { Type: 'Pass', End: true }, + } + }); + + test.done(); + }, + }, + + 'Elision': { + 'An elidable pass state that is chained onto should disappear'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); + + const first = new stepfunctions.Pass(sm, 'First'); + const success = new stepfunctions.Pass(sm, 'Success', { elidable: true }); + const second = new stepfunctions.Pass(sm, 'Second'); + + // WHEN + first.then(success).then(second); + + // THEN + test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + StartAt: 'First', + States: { + First: { Type: 'Pass', Next: 'Second' }, + Second: { Type: 'Pass', End: true }, + } + }); + + test.done(); + }, + + 'An elidable pass state at the end of a chain should disappear'(test: Test) { + const stack = new cdk.Stack(); + const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); + + const first = new stepfunctions.Pass(sm, 'First'); + const pass = new stepfunctions.Pass(sm, 'Pass', { elidable: true }); + + // WHEN + first.then(pass); + + // THEN + test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + StartAt: 'First', + States: { + First: { Type: 'Pass', End: true }, + } + }); + + test.done(); + }, + + 'An elidable success state at the end of a chain should disappear'(test: Test) { + const stack = new cdk.Stack(); + const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); + + const first = new stepfunctions.Pass(sm, 'First'); + const success = new stepfunctions.Succeed(sm, 'Success', { elidable: true }); + + // WHEN + first.then(success); + + // THEN + test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + StartAt: 'First', + States: { + First: { Type: 'Pass', End: true }, + } + }); + + test.done(); + }, + }, + + 'Goto support': { + 'State machines can have unconstrainted gotos'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); + + const one = new stepfunctions.Pass(sm, 'One'); + const two = new stepfunctions.Pass(sm, 'Two'); + + // WHEN + one.then(two).then(one); + + // THEN + test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + StartAt: 'One', + States: { + One: { Type: 'Pass', Next: 'Two' }, + Two: { Type: 'Pass', Next: 'One' }, + } + }); + + test.done(); + }, + }, + + 'Error handling': { + 'States can have error branches'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); + + const task1 = new stepfunctions.Task(sm, 'Task1', { resource: new FakeResource() }); + const failure = new stepfunctions.Fail(sm, 'Failed', { error: 'DidNotWork', cause: 'We got stuck' }); + + // WHEN + task1.toStateChain().catch(failure); + + // THEN + test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + StartAt: 'Task1', + States: { + Task1: { + Type: 'Task', + Resource: 'resource', + End: true, + Catch: [ + { ErrorEquals: ['States.ALL'], Next: 'Failed' }, + ] + }, + Failed: { + Type: 'Fail', + Error: 'DidNotWork', + Cause: 'We got stuck', + } + } + }); + + test.done(); + }, + + 'Error branch is attached to all tasks in chain'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); + + const task1 = new stepfunctions.Task(sm, 'Task1', { resource: new FakeResource() }); + const task2 = new stepfunctions.Task(sm, 'Task2', { resource: new FakeResource() }); + const errorHandler = new stepfunctions.Pass(sm, 'ErrorHandler'); + + // WHEN + task1.then(task2).catch(errorHandler); + + // THEN + test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + StartAt: 'Task1', + States: { + Task1: { + Type: 'Task', + Resource: 'resource', + Next: 'Task2', + Catch: [ + { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler' }, + ] + }, + Task2: { + Type: 'Task', + Resource: 'resource', + End: true, + Catch: [ + { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler' }, + ] + }, + ErrorHandler: { Type: 'Pass', End: true } + } + }); + + test.done(); + }, + + 'Machine is wrapped in parallel if not all tasks can have catch'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); + + const task1 = new stepfunctions.Task(sm, 'Task1', { resource: new FakeResource() }); + const wait = new stepfunctions.Wait(sm, 'Wait', { seconds: 10 }); + const errorHandler = new stepfunctions.Pass(sm, 'ErrorHandler'); + + // WHEN + task1.then(wait).catch(errorHandler); + + // THEN + test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + StartAt: 'Para', + States: { + Para: { + Type: 'Parallel', + End: true, + Branches: [ + { + StartAt: 'Task1', + States: { + Task1: { + Type: 'Task', + Resource: 'resource', + Next: 'Wait', + }, + Wait: { + Type: 'Wait', + End: true, + Seconds: 10 + }, + } + } + ], + Catch: [ + { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler' }, + ] + }, + ErrorHandler: { Type: 'Pass', End: true } + } + }); + + test.done(); + }, + + 'Error branch does not count for chaining'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); + + const task1 = new stepfunctions.Task(sm, 'Task1', { resource: new FakeResource() }); + const task2 = new stepfunctions.Task(sm, 'Task2', { resource: new FakeResource() }); + const errorHandler = new stepfunctions.Pass(sm, 'ErrorHandler'); + + // WHEN + task1.catch(errorHandler).then(task2); + + // THEN + test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + StartAt: 'Task1', + States: { + Task1: { + Type: 'Task', + Resource: 'resource', + Next: 'Task2', + Catch: [ + { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler' }, + ] + }, + Task2: { Type: 'Task', Resource: 'resource', End: true }, + ErrorHandler: { Type: 'Pass', End: true }, + } + }); + + test.done(); + }, + + 'Can merge state machines with shared states'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); + + const task1 = new stepfunctions.Task(sm, 'Task1', { resource: new FakeResource() }); + const task2 = new stepfunctions.Task(sm, 'Task2', { resource: new FakeResource() }); + const failure = new stepfunctions.Fail(sm, 'Failed', { error: 'DidNotWork', cause: 'We got stuck' }); + + // WHEN + task1.catch(failure); + task2.catch(failure); + + task1.then(task2); + + // THEN + test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + StartAt: 'Task1', + States: { + Task1: { + Type: 'Task', + Next: 'Task2', + Catch: [ + { ErrorEquals: ['States.ALL'], Next: 'Failed' }, + ] + }, + Task2: { + Type: 'Task', + End: true, + Catch: [ + { ErrorEquals: ['States.ALL'], Next: 'Failed' }, + ] + }, + Failed: { + Type: 'Fail', + Error: 'DidNotWork', + Cause: 'We got stuck', + } + } + }); + + test.done(); + } + } +}; + +class ReusableStateMachine extends stepfunctions.StateMachineDefinition { + constructor(parent: cdk.Construct, id: string) { + super(parent, id); + + this.start( + new stepfunctions.Choice(this, 'Choice') + .on(stepfunctions.Condition.stringEquals('$.branch', 'left'), new stepfunctions.Pass(this, 'Left Branch')) + .on(stepfunctions.Condition.stringEquals('$.branch', 'right'), new stepfunctions.Pass(this, 'Right Branch'))); + } +} + +class FakeResource implements stepfunctions.IStepFunctionsTaskResource { + public asStepFunctionsTaskResource(_callingTask: stepfunctions.Task): stepfunctions.StepFunctionsTaskResourceProps { + return { + resourceArn: new cdk.Arn('resource') + }; + } +} From 4a1172c397d833bb4b5302dc54c3ba2dec1b1a6b Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 3 Sep 2018 12:46:52 +0200 Subject: [PATCH 09/29] Make an activity be possible to be the resource for a Task --- packages/@aws-cdk/aws-stepfunctions/lib/activity.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts b/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts index 22f6e934127c3..c95bc1a2743ac 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts @@ -1,4 +1,5 @@ import cdk = require('@aws-cdk/cdk'); +import { IStepFunctionsTaskResource, StepFunctionsTaskResourceProps, Task } from './states/task'; import { ActivityArn, ActivityName, cloudformation } from './stepfunctions.generated'; export interface ActivityProps { @@ -13,7 +14,7 @@ export interface ActivityProps { /** * Define a new StepFunctions activity */ -export class Activity extends cdk.Construct { +export class Activity extends cdk.Construct implements IStepFunctionsTaskResource { public readonly activityArn: ActivityArn; public readonly activityName: ActivityName; @@ -27,4 +28,11 @@ export class Activity extends cdk.Construct { this.activityArn = resource.ref; this.activityName = resource.activityName; } + + public asStepFunctionsTaskResource(_callingTask: Task): StepFunctionsTaskResourceProps { + // No IAM permissions necessary, execution role implicitly has Activity permissions. + return { + resourceArn: this.activityArn + }; + } } \ No newline at end of file From 9e54761fc40a8e3b9b2a8919a1520c465f2364ec Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 3 Sep 2018 17:48:11 +0200 Subject: [PATCH 10/29] Making all tests pass with mutable interface ALSO: - Add support for policies - Add support for CloudWatch event rule targets --- .../aws-stepfunctions/DESIGN_NOTES.md | 11 +- .../aws-stepfunctions/lib/asl-condition.ts | 216 ++--- .../aws-stepfunctions/lib/asl-external-api.ts | 14 +- .../aws-stepfunctions/lib/asl-internal-api.ts | 80 +- .../aws-stepfunctions/lib/asl-state-chain.ts | 64 +- .../@aws-cdk/aws-stepfunctions/lib/asl.ts | 907 ------------------ .../@aws-cdk/aws-stepfunctions/lib/index.ts | 3 +- .../aws-stepfunctions/lib/state-machine.ts | 40 +- .../aws-stepfunctions/lib/states/choice.ts | 68 +- .../aws-stepfunctions/lib/states/fail.ts | 30 +- .../aws-stepfunctions/lib/states/parallel.ts | 75 +- .../aws-stepfunctions/lib/states/pass.ts | 49 +- .../lib/states/state-machine-definition.ts | 74 -- .../lib/states/state-machine-fragment.ts | 69 ++ .../aws-stepfunctions/lib/states/state.ts | 42 +- .../aws-stepfunctions/lib/states/succeed.ts | 40 +- .../aws-stepfunctions/lib/states/task.ts | 65 +- .../aws-stepfunctions/lib/states/util.ts | 19 +- .../aws-stepfunctions/lib/states/wait.ts | 44 +- .../@aws-cdk/aws-stepfunctions/lib/util.ts | 9 - .../@aws-cdk/aws-stepfunctions/package.json | 2 + .../test/test.state-machine-resources.ts | 74 +- .../test/test.states-language.ts | 312 +++--- 23 files changed, 740 insertions(+), 1567 deletions(-) delete mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/asl.ts delete mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/states/state-machine-definition.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/states/state-machine-fragment.ts delete mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/util.ts diff --git a/packages/@aws-cdk/aws-stepfunctions/DESIGN_NOTES.md b/packages/@aws-cdk/aws-stepfunctions/DESIGN_NOTES.md index dc106f30b9e99..815146dfa2027 100644 --- a/packages/@aws-cdk/aws-stepfunctions/DESIGN_NOTES.md +++ b/packages/@aws-cdk/aws-stepfunctions/DESIGN_NOTES.md @@ -199,4 +199,13 @@ class SomeConstruct extends cdk.Construct { It's desirable to just have a StateMachineDefinition own and list everything, but that kind of requires that for Parallel you need a StateMachineDefinition -as well (but probably without appending names). \ No newline at end of file +as well (but probably without appending names). + +--- + +Notes: use a complicated object model to hide ugly parts of the API from users +(by using "internal details" classes as capabilities). + +Decouple state chainer from states. + +Decouple rendering from states to allow elidable states. \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl-condition.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl-condition.ts index a7d057deb7970..dfa23b7bd7c54 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/asl-condition.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/asl-condition.ts @@ -1,200 +1,150 @@ -export enum ComparisonOperator { - StringEquals, - StringLessThan, - StringGreaterThan, - StringLessThanEquals, - StringGreaterThanEquals, - NumericEquals, - NumericLessThan, - NumericGreaterThan, - NumericLessThanEquals, - NumericGreaterThanEquals, - BooleanEquals, - TimestampEquals, - TimestampLessThan, - TimestampGreaterThan, - TimestampLessThanEquals, - TimestampGreaterThanEquals, - And, - Or, - Not -} - export abstract class Condition { - public static stringEquals(variable: string, value: string): Condition { - return new StringEqualsComparisonOperation({ variable, value }); + public static parse(_expression: string): Condition { + throw new Error('Parsing not implemented yet!'); } - public abstract renderCondition(): any; -} - -export interface BaseVariableComparisonOperationProps { - comparisonOperator: ComparisonOperator, - value: any, - variable: string -} - -export interface VariableComparisonOperationProps { - /** - * The value to be compared against. - */ - value: any, + public static booleanEquals(variable: string, value: boolean): Condition { + return new VariableComparison(variable, ComparisonOperator.BooleanEquals, value); + } - /** - * A Path to the value to be compared. - */ - variable: string -} + public static stringEquals(variable: string, value: string): Condition { + return new VariableComparison(variable, ComparisonOperator.StringEquals, value); + } -export abstract class VariableComparisonOperation extends Condition { - constructor(private readonly props: BaseVariableComparisonOperationProps) { - super(); + public static stringLessThan(variable: string, value: string): Condition { + return new VariableComparison(variable, ComparisonOperator.StringLessThan, value); } - public renderCondition(): any { - return { - Variable: this.props.variable, - [ComparisonOperator[this.props.comparisonOperator]]: this.props.value - }; + public static stringLessThanEquals(variable: string, value: string): Condition { + return new VariableComparison(variable, ComparisonOperator.StringLessThanEquals, value); } -} -class StringEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.StringEquals } }); + public static stringGreaterThan(variable: string, value: string): Condition { + return new VariableComparison(variable, ComparisonOperator.StringGreaterThan, value); } -} -export class StringLessThanComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.StringLessThan } }); + public static stringGreaterThanEquals(variable: string, value: string): Condition { + return new VariableComparison(variable, ComparisonOperator.StringGreaterThanEquals, value); } -} -export class StringGreaterThanComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.StringGreaterThan } }); + public static numericEquals(variable: string, value: number): Condition { + return new VariableComparison(variable, ComparisonOperator.NumericEquals, value); } -} -export class StringLessThanEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.StringLessThanEquals } }); + public static numericLessThan(variable: string, value: number): Condition { + return new VariableComparison(variable, ComparisonOperator.NumericLessThan, value); } -} -export class StringGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.StringGreaterThanEquals } }); + public static numericLessThanEquals(variable: string, value: number): Condition { + return new VariableComparison(variable, ComparisonOperator.NumericLessThanEquals, value); } -} -export class NumericEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.NumericEquals } }); + public static numericGreaterThan(variable: string, value: number): Condition { + return new VariableComparison(variable, ComparisonOperator.NumericGreaterThan, value); } -} -export class NumericLessThanComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.NumericLessThan } }); + public static numericGreaterThanEquals(variable: string, value: number): Condition { + return new VariableComparison(variable, ComparisonOperator.NumericGreaterThanEquals, value); } -} -export class NumericGreaterThanComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.NumericGreaterThan } }); + public static timestampEquals(variable: string, value: string): Condition { + return new VariableComparison(variable, ComparisonOperator.TimestampEquals, value); } -} -export class NumericLessThanEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.NumericLessThanEquals } }); + public static timestampLessThan(variable: string, value: string): Condition { + return new VariableComparison(variable, ComparisonOperator.TimestampLessThan, value); } -} -export class NumericGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.NumericGreaterThanEquals } }); + public static timestampLessThanEquals(variable: string, value: string): Condition { + return new VariableComparison(variable, ComparisonOperator.TimestampLessThanEquals, value); } -} -export class BooleanEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.BooleanEquals } }); + public static timestampGreaterThan(variable: string, value: string): Condition { + return new VariableComparison(variable, ComparisonOperator.TimestampGreaterThan, value); } -} -export class TimestampEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.TimestampEquals } }); + public static timestampGreaterThanEquals(variable: string, value: string): Condition { + return new VariableComparison(variable, ComparisonOperator.TimestampGreaterThanEquals, value); } -} -export class TimestampLessThanComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.TimestampLessThan } }); + public static and(...conditions: Condition[]): Condition { + return new CompoundCondition(CompoundOperator.And, ...conditions); } -} -export class TimestampGreaterThanComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.TimestampGreaterThan } }); + public static or(...conditions: Condition[]): Condition { + return new CompoundCondition(CompoundOperator.Or, ...conditions); } -} -export class TimestampLessThanEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.TimestampLessThanEquals } }); + public static not(condition: Condition): Condition { + return new NotCondition(condition); } + + public abstract renderCondition(): any; } -export class TimestampGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.TimestampGreaterThanEquals } }); - } +enum ComparisonOperator { + StringEquals, + StringLessThan, + StringGreaterThan, + StringLessThanEquals, + StringGreaterThanEquals, + NumericEquals, + NumericLessThan, + NumericGreaterThan, + NumericLessThanEquals, + NumericGreaterThanEquals, + BooleanEquals, + TimestampEquals, + TimestampLessThan, + TimestampGreaterThan, + TimestampLessThanEquals, + TimestampGreaterThanEquals, } -export interface ArrayComparisonOperationProps { - comparisonOperator: ComparisonOperator, - comparisonOperations: Condition[] +enum CompoundOperator { + And, + Or, } -export abstract class ArrayComparisonOperation extends Condition { - constructor(private readonly props: ArrayComparisonOperationProps) { +class VariableComparison extends Condition { + constructor(private readonly variable: string, private readonly comparisonOperator: ComparisonOperator, private readonly value: any) { super(); - if (props.comparisonOperations.length === 0) { - throw new Error('\'comparisonOperations\' is empty. Must be non-empty array of ChoiceRules'); - } } public renderCondition(): any { return { - [ComparisonOperator[this.props.comparisonOperator]]: this.props.comparisonOperations + Variable: this.variable, + [ComparisonOperator[this.comparisonOperator]]: this.value }; } } -export class AndComparisonOperation extends ArrayComparisonOperation { - constructor(...comparisonOperations: Condition[]) { - super({ comparisonOperator: ComparisonOperator.And, comparisonOperations }); +class CompoundCondition extends Condition { + private readonly conditions: Condition[]; + + constructor(private readonly operator: CompoundOperator, ...conditions: Condition[]) { + super(); + this.conditions = conditions; + if (conditions.length === 0) { + throw new Error('Must supply at least one inner condition for a logical combination'); + } } -} -export class OrComparisonOperation extends ArrayComparisonOperation { - constructor(...comparisonOperations: Condition[]) { - super({ comparisonOperator: ComparisonOperator.Or, comparisonOperations }); + public renderCondition(): any { + return { + [CompoundOperator[this.operator]]: this.conditions.map(c => c.renderCondition()) + }; } } -export class NotComparisonOperation extends Condition { +class NotCondition extends Condition { constructor(private readonly comparisonOperation: Condition) { super(); } public renderCondition(): any { return { - [ComparisonOperator[ComparisonOperator.Not]]: this.comparisonOperation + Not: this.comparisonOperation }; } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl-external-api.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl-external-api.ts index 69cc46da95149..340af14fe9a20 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/asl-external-api.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/asl-external-api.ts @@ -1,3 +1,4 @@ +import cdk = require('@aws-cdk/cdk'); import { IInternalState } from "./asl-internal-api"; export interface IChainable { @@ -7,12 +8,17 @@ export interface IChainable { export interface IStateChain extends IChainable { readonly startState: IInternalState; - then(state: IChainable): IStateChain; - catch(targetState: IChainable, ...errors: string[]): IStateChain; + next(state: IChainable): IStateChain; + onError(targetState: IChainable, ...errors: string[]): IStateChain; + + closure(): IStateChain; + + renderStateMachine(): RenderedStateMachine; } -// tslint:disable-next-line:no-empty-interface -export interface IState extends IChainable { +export interface RenderedStateMachine { + stateMachineDefinition: any; + policyStatements: cdk.PolicyStatement[]; } /** diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl-internal-api.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl-internal-api.ts index 513a03f2db595..ca64d134a45c4 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/asl-internal-api.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/asl-internal-api.ts @@ -1,21 +1,16 @@ +import cdk = require('@aws-cdk/cdk'); + export interface IInternalState { readonly stateId: string; - readonly stateBehavior: StateBehavior; - - next(targetState: IInternalState): void; - catch(targetState: IInternalState, errors: string[]): void; - renderState(): any; -} + readonly canHaveCatch: boolean; + readonly hasOpenNextTransition: boolean; + readonly policyStatements: cdk.PolicyStatement[]; -export interface StateBehavior { - elidable: boolean; - canHaveNext: boolean; - canHaveCatch: boolean; -} + addNext(targetState: IInternalState): void; + addCatch(targetState: IInternalState, errors: string[]): void; -export interface Transition { - targetState: IInternalState; - annotation: any; + accessibleStates(): IInternalState[]; + renderState(): any; } export enum StateType { @@ -27,3 +22,60 @@ export enum StateType { Fail = 'Fail', Parallel = 'Parallel' } + +export interface Transition { + transitionType: TransitionType; + targetState: IInternalState; + annotation?: any; +} + +export enum TransitionType { + Next = 'Next', + Catch = 'Catch', + Choice = 'Choices', + Default = 'Default', +} + +export class Transitions { + private readonly transitions = new Array(); + + public add(transitionType: TransitionType, targetState: IInternalState, annotation?: any) { + this.transitions.push({ transitionType, targetState, annotation }); + } + + public has(type: TransitionType): boolean { + return this.get(type).length > 0; + } + + public get(type: TransitionType): Transition[] { + return this.transitions.filter(t => t.transitionType === type); + } + + public all(): Transition[] { + return this.transitions; + } + + public renderSingle(type: TransitionType, otherwise: any = {}): any { + const transitions = this.get(type); + if (transitions.length === 0) { + return otherwise; + } + return { + [type]: transitions[0].targetState.stateId + }; + } + + public renderList(type: TransitionType): any { + const transitions = this.get(type); + if (transitions.length === 0) { + return {}; + } + + return { + [type]: transitions.map(t => ({ + ...t.annotation, + Next: t.targetState.stateId, + })) + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl-state-chain.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl-state-chain.ts index 79cf40856ed18..9cc34b5ce6bce 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/asl-state-chain.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/asl-state-chain.ts @@ -1,4 +1,5 @@ -import { Errors, IChainable, IStateChain } from './asl-external-api'; +import cdk = require('@aws-cdk/cdk'); +import { Errors, IChainable, IStateChain, RenderedStateMachine } from './asl-external-api'; import { IInternalState } from './asl-internal-api'; export class StateChain implements IStateChain { @@ -9,7 +10,11 @@ export class StateChain implements IStateChain { constructor(startState: IInternalState) { this.allStates.add(startState); + // Even if the state doesn't allow .next()ing onto it, still set as + // active state so we trigger the per-State exception (which is more + // informative than the generic "no active states" exception). this.activeStates.add(startState); + this._startState = startState; } @@ -17,17 +22,17 @@ export class StateChain implements IStateChain { return this._startState; } - public then(state: IChainable): IStateChain { + public next(state: IChainable): IStateChain { const sm = state.toStateChain(); const ret = this.clone(); if (this.activeStates.size === 0) { - throw new Error('Cannot chain onto state machine; no end states'); + throw new Error('Cannot add to chain; there are no chainable states without a "Next" transition.'); } for (const endState of this.activeStates) { - endState.next(sm.startState); + endState.addNext(sm.startState); } ret.absorb(sm); @@ -40,14 +45,14 @@ export class StateChain implements IStateChain { return this; } - public catch(handler: IChainable, ...errors: string[]): IStateChain { + public onError(handler: IChainable, ...errors: string[]): IStateChain { if (errors.length === 0) { errors = [Errors.all]; } const sm = handler.toStateChain(); - const canApplyDirectly = Array.from(this.allStates).every(s => s.stateBehavior.canHaveCatch); + const canApplyDirectly = Array.from(this.allStates).every(s => s.canHaveCatch); if (!canApplyDirectly) { // Can't easily create a Parallel here automatically since we need a // StateMachineDefinition parent and need to invent a unique name. @@ -56,7 +61,7 @@ export class StateChain implements IStateChain { const ret = this.clone(); for (const state of this.allStates) { - state.catch(sm.startState, errors); + state.addCatch(sm.startState, errors); } // Those states are now part of the state machine, but we don't include @@ -66,7 +71,50 @@ export class StateChain implements IStateChain { return ret; } - public absorb(other: IStateChain) { + /** + * Return a closure that contains all accessible states from the given start state + * + * This sets all active ends to the active ends of all accessible states. + */ + public closure(): IStateChain { + const ret = new StateChain(this.startState); + + const queue = this.startState.accessibleStates(); + while (queue.length > 0) { + const state = queue.splice(0, 1)[0]; + if (!ret.allStates.has(state)) { + ret.allStates.add(state); + queue.push(...state.accessibleStates()); + } + } + + ret.activeStates = new Set(Array.from(ret.allStates).filter(s => s.hasOpenNextTransition)); + + return ret; + } + + public renderStateMachine(): RenderedStateMachine { + // Rendering always implies rendering the closure + const closed = this.closure(); + + const policies = new Array(); + + const states: any = {}; + for (const state of accessMachineInternals(closed).allStates) { + states[state.stateId] = state.renderState(); + policies.push(...state.policyStatements); + } + + return { + stateMachineDefinition: { + StartAt: this.startState.stateId, + States: states + }, + policyStatements: policies + }; + } + + private absorb(other: IStateChain) { const sdm = accessMachineInternals(other); for (const state of sdm.allStates) { this.allStates.add(state); diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl.ts deleted file mode 100644 index 3711733f077a3..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions/lib/asl.ts +++ /dev/null @@ -1,907 +0,0 @@ -import { CloudFormationJSON, Token } from "@aws-cdk/cdk"; -import { isString } from "util"; -import { requireNextOrEnd, requireOneOf } from "./util"; - -export namespace asl { - - /** - * Converts all keys to PascalCase when serializing to JSON. - */ - export class PascalCaseJson { - public readonly props: any; - - constructor(props: any) { - this.props = props; - } - - public toJSON() { - return this.toPascalCase(this.props); - } - - private toPascalCase(o: any) { - const out: { [index: string]: any } = {}; - for (const k in o) { - if (o.hasOwnProperty(k)) { - out[k[0].toUpperCase() + k.substring(1)] = o[k]; - } - } - return out; - } - } - - export interface StateMachineProps extends BranchProps { - /** - * The version of the States language. - * - * @default "1.0" - * - * {@link https://states-language.net/spec.html#toplevelfields} - */ - version?: string, - - /** - * The maximum number of seconds the machine is allowed to run. - * - * If the machine runs longer than the specified time, then the - * interpreter fails the machine with a {@link ErrorCode#Timeout} - * Error Name. - * - * {@link https://states-language.net/spec.html#toplevelfields} - */ - timeoutSeconds?: number, - } - - /** - * A State Machine which can serialize to a JSON object - */ - export class StateMachine extends PascalCaseJson { - constructor(props: StateMachineProps) { - if (isString(props.startAt) && !props.states.hasState(props.startAt)) { - throw new Error(`Specified startAt state '${props.startAt}' does not exist in states map`); - } - super(props); - } - - // tslint:disable:max-line-length - /** - * Returns a JSON representation for use with CloudFormation. - * - * @link http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html#cfn-stepfunctions-statemachine-definitionstring - */ - // tslint:enable:max-line-length - public definitionString() { - return CloudFormationJSON.stringify(this.toJSON()); - } - } - - export interface Commentable { - /** - * A comment provided as human-readable description - */ - comment?: string - } - - export interface BranchProps extends Commentable { - /** - * Represents the states in this State Machine. - * - * {@link https://states-language.net/spec.html#toplevelfields} - */ - states: States, - - /** - * Name of the state the interpreter starts running the machine at. - * - * Must exactly match one of the names of the {@link states} field. - * - * {@link https://states-language.net/spec.html#toplevelfields} - */ - startAt: string - } - - export class Branch extends PascalCaseJson { - private readonly states: States; - - constructor(props: BranchProps) { - if (isString(props.startAt) && !props.states.hasState(props.startAt)) { - throw new Error(`Specified startAt state '${props.startAt}' does not exist in states map`); - } - super(props); - this.states = props.states; - } - - public stateNames(): string[] { - return this.states.stateNames(); - } - } - - /** - * The States of the State Machine. - * - * State names must have length of less than or equal to 128 characters. - * State names must be unique within the scope of the whole state machine. - * - * {@link https://states-language.net/spec.html#states-fields} - */ - export class States extends PascalCaseJson { - constructor(states: { [key: string]: State }) { - const longNames = Object.keys(states).filter(n => n.length > 128); - if (longNames.length > 0) { - throw new Error(`State names ${JSON.stringify(longNames)} exceed 128 characters in length`); - } - for (const state of Object.values(states)) { - const next = state.next(); - if (!state.isTerminal() && next.length === 0 && !(state instanceof ChoiceState)) { - throw new Error(`Non-terminal and non-ChoiceState state '${state}' does not have a 'next' field`); - } - } - super(states); - } - - public toJSON() { - return this.props; - } - - public hasState(name: string) { - return this.props.hasOwnProperty(name); - } - - public stateNames(): string[] { - const names: string[] = Object.keys(this.props); - Object.values(this.props).map( - state => (state instanceof ParallelState) ? state.stateNames() : [] - ).forEach(branchNames => branchNames.forEach(name => names.push(name))); - return names; - } - } - - export interface NextField { - /** - * The name of the next state to execute. - * - * Must exactly match the name of another state in the state machine. - */ - next: string; - } - - export interface EndField { - /** - * Marks the state as an End State. - * - * After the interpreter executes an End State, the state machine will - * terminate and return a result. - */ - end: true; - } - - export interface NextOrEndField { - /** - * The name of the next state to execute. - * - * Must exactly match the name of another state in the state machine. - */ - next?: string, - - /** - * Marks the state as an End State. - * - * After the interpreter executes an End State, the state machine will - * terminate and return a result. - */ - end?: true - } - - export interface InputOutputPathFields { - /** - * A {@link https://states-language.net/spec.html#path Path} applied to - * a State's raw input to select some or all of it. - * - * The selection is used as the input to the State. - * - * If `null` the raw input is discarded, and teh effective input for - * the state is an empty JSON object. - * - * @default "$", which selects the whole raw input - * - * {@link https://states-language.net/spec.html#filters} - */ - inputPath?: string; - - /** - * A {@link https://states-language.net/spec.html#path Path} applied to - * a State's output after the application of {@link ResultPathField#resultPath ResultPath} - * leading in the generation of the raw input for the next state. - * - * If `null` the input and result are discarded, and the effective - * output for the state is an empty JSON object. - * - * @default "$", which is effectively the result of processing {@link ResultPathField#resultPath ResultPath} - * - * {@link https://states-language.net/spec.html#filters} - */ - outputPath?: string | Token; - } - - export interface ResultPathField { - /** - * A {@link https://states-language.net/spec.html#ref-paths Reference Path} - * which specifies the where to place the result, relative to the raw input. - * - * If the input has a field which matches the ResultPath value, then in - * the * output, that field is discarded and overwritten. Otherwise, a - * new field is created. - * - * If `null` the state's own raw output is discarded and its raw input - * becomes its result. - * - * @default "$", the state's result overwrites and replaces the raw input - * - * {@link https://states-language.net/spec.html#filters} - */ - resultPath?: string | Token; - } - - /** - * Predefined Error Codes - * - * {@link https://states-language.net/spec.html#appendix-a} - */ - export enum ErrorCode { - /** - * A wild-card which matches any Error Name. - */ - ALL = "States.ALL", - - /** - * A {@link TaskState Task State} either ran longer than the {@link TaskStateProps#timeoutSeconds TimeoutSeconds} value, - * or failed to heartbeat for a time longer than the {@link TaskStateProps#heartbeatSeconds HeartbeatSeconds} value. - */ - Timeout = "States.Timeout", - - /** - * A {@link TaskState Task State} failed during the execution. - */ - TaskFailed = "States.TaskFailed", - - /** - * A {@link TaskState Task State} failed because it had insufficient privileges to - * execute the specified code. - */ - Permissions = "States.Permissions", - - /** - * A {@link TaskState Task State} failed because it had insufficient privileges to - * execute the specified code. - */ - ResultPathMatchFailure = "States.ResultPathMatchFailure", - - /** - * A branch of a {@link ParallelState Parallel state} failed. - */ - BranchFailed = "States.BranchFailed", - - /** - * A {@link ChoiceState Choice state} failed to find a match for the condition field - * extracted from its input. - */ - NoChoiceMatched = "States.NoChoiceMatched" - } - - export interface ErrorEquals { - /** - * A non-empty array of {@link https://states-language.net/spec.html#error-names Error Names} - * this rule should match. - * - * Can use {@link ErrorCode} values for pre-defined Error Codes. - * - * The reserved error name {@link ErrorCode#ALL} is a wild-card and - * matches any Error Name. It must appear alone in the array and must - * appear in the last {@link Retrier}/{@link Catcher} in the - * {@link RetryCatchFields#retry}/{@link RetryCatchFields#catch} arrays - * - * {@link https://states-language.net/spec.html#errors} - */ - errorEquals: Array - } - - export interface RetrierProps extends ErrorEquals { - /** - * Number of seconds before first retry attempt. - * - * Must be a positive integer. - * - * @default 1 - */ - intervalSeconds?: number | Token - - /** - * Maximum number of retry attempts. - * - * Must be a non-negative integer. May be set to 0 to specify no retries - * - * @default 3 - */ - maxAttempts?: number | Token - - /** - * Multiplier tha tincreases the retry interval on each attempt. - * - * Must be greater than or equal to 1.0. - * - * @default 2.0 - */ - backoffRate?: number | Token - } - - function validateErrorEquals(errors: Array) { - if (errors.length === 0) { - throw new Error('ErrorEquals is empty. Must be a non-empty array of Error Names'); - } - if (errors.length > 1 && errors.includes(ErrorCode.ALL)) { - throw new Error(`Error name '${ErrorCode.ALL}' is specified along with other Error Names. '${ErrorCode.ALL}' must appear alone.`); - } - errors.forEach(name => { - if (isString(name) && !(Object.values(ErrorCode).includes(name) || !name.startsWith("States."))) { - throw new Error(`'${name}' is not a valid Error Name`); - } - }); - } - - /** - * A retry policy for the specified errors. - * - * {@link https://states-language.net/spec.html#errors} - */ - export class Retrier extends PascalCaseJson { - constructor(props: RetrierProps) { - validateErrorEquals(props.errorEquals); - if (props.backoffRate && props.backoffRate < 1.0) { - throw new Error(`backoffRate '${props.backoffRate}' is not >= 1.0`); - } - super(props); - } - } - - export interface CatcherProps extends ErrorEquals, ResultPathField, NextField { - - } - - /** - * A fallback state for the specified errors. - * - * {@link https://states-language.net/spec.html#errors} - */ - export class Catcher extends PascalCaseJson { - constructor(props: CatcherProps) { - validateErrorEquals(props.errorEquals); - super(props); - } - } - - export interface RetryCatchFields { - /** - * Ordered array of {@link Retrier} the interpreter scans through on - * error. - * - * {@link https://states-language.net/spec.html#errors} - */ - retry?: Retriers; - - /** - * Ordered array of {@link Catcher} the interpeter scans through to - * handle errors when there is no {@link retry} or retries have been - * exhausted. - * - * {@link https://states-language.net/spec.html#errors} - */ - catch?: Catchers; - } - - function validateErrorAllNotBeforeLast(props: ErrorEquals[]) { - props.slice(0, -1).forEach(prop => { - if (prop.errorEquals.includes(ErrorCode.ALL)) { - throw new Error( - `Error code '${ErrorCode.ALL}' found before last error handler. '${ErrorCode.ALL}' must appear in the last error handler.` - ); - } - }); - } - - export class Retriers extends PascalCaseJson { - constructor(retriers: Retrier[]) { - validateErrorAllNotBeforeLast(retriers.map(retrier => retrier.props)); - super(retriers); - } - } - - export class Catchers extends PascalCaseJson { - constructor(catchers: Catcher[]) { - validateErrorAllNotBeforeLast(catchers.map(catcher => catcher.props)); - super(catchers); - } - } - - /** - * Values for the "Type" field which is required for every State object. - * - * {@link https://states-language.net/spec.html#statetypes} - */ - export enum StateType { - Pass, - Task, - Choice, - Wait, - Succeed, - Fail, - Parallel - } - - /** - * A State in a State Machine. - * - * {@link https://states-language.net/spec.html#statetypes} - */ - export interface State { - isTerminal(): boolean - next(): Array; - } - - export abstract class BaseState extends PascalCaseJson implements State { - constructor(type: StateType, props: any) { - super({ ...props, ...{ type: StateType[type] } }); - } - - public isTerminal() { - return this.props.hasOwnProperty('end') && this.props.end === true; - } - - public next() { - return (this.props.next) ? [this.props.next] : []; - } - } - - export interface PassStateProps extends Commentable, InputOutputPathFields, ResultPathField, NextOrEndField { - /** - * Treated as the output of a virtual task, placed as prescribed by the {@link resultPath} field. - * - * @default By default, the output is the input. - */ - result?: any; - } - - /** - * Passes its input to its output, performing no work. - * - * Can also be used to inject fixed data into the state machine. - * - * {@link https://states-language.net/spec.html#pass-state} - */ - export class PassState extends BaseState { - constructor(props: PassStateProps) { - requireNextOrEnd(props); - super(StateType.Pass, props); - } - } - - export interface TaskStateProps extends Commentable, InputOutputPathFields, ResultPathField, RetryCatchFields, NextOrEndField { - /** - * A URI identifying the task to execute. - * - * The States language does not constrain the URI. However, the AWS - * Step Functions intepreter only supports the ARN of an Activity or - * Lambda function. {@link https://docs.aws.amazon.com/step-functions/latest/dg/concepts-tasks.html} - */ - resource: string | Token; - - /** - * The maxium number of seconds the state will run before the interpeter - * fails it with a {@link ErrorCode.Timeout} error. - * - * Must be a positive integer, and must be smaller than {@link heartbeatSeconds} if provided. - * - * @default 60 - */ - timeoutSeconds?: number | Token; - - /** - * The number of seconds between heartbeats before the intepreter - * fails the state with a {@link ErrorCode.Timeout} error. - */ - heartbeatSeconds?: number | Token; - } - - /** - * Executes the work identified by the {@link TaskStateProps#resource} field. - * - * {@link https://states-language.net/spec.html#task-state} - */ - export class TaskState extends BaseState { - constructor(props: TaskStateProps) { - if (props.timeoutSeconds !== undefined && props.heartbeatSeconds !== undefined && props.heartbeatSeconds >= props.timeoutSeconds) { - throw new Error("heartbeatSeconds is larger than timeoutSeconds"); - } - requireNextOrEnd(props); - super(StateType.Task, props); - } - } - - export enum ComparisonOperator { - StringEquals, - StringLessThan, - StringGreaterThan, - StringLessThanEquals, - StringGreaterThanEquals, - NumericEquals, - NumericLessThan, - NumericGreaterThan, - NumericLessThanEquals, - NumericGreaterThanEquals, - BooleanEquals, - TimestampEquals, - TimestampLessThan, - TimestampGreaterThan, - TimestampLessThanEquals, - TimestampGreaterThanEquals, - And, - Or, - Not - } - - export abstract class ComparisonOperation extends PascalCaseJson { - } - - export interface BaseVariableComparisonOperationProps { - comparisonOperator: ComparisonOperator, - value: any, - variable: string | Token - } - - export interface VariableComparisonOperationProps { - /** - * The value to be compared against. - */ - value: T, - - /** - * A Path to the value to be compared. - */ - variable: string - } - - export abstract class VariableComparisonOperation extends ComparisonOperation { - constructor(props: BaseVariableComparisonOperationProps) { - super(props); - } - - public toJSON(): { [key: string]: any } { - return { - Variable: this.props.variable, - [ComparisonOperator[this.props.comparisonOperator]]: this.props.value - }; - } - } - - export class StringEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.StringEquals } }); - } - } - - export class StringLessThanComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.StringLessThan } }); - } - } - - export class StringGreaterThanComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.StringGreaterThan } }); - } - } - - export class StringLessThanEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.StringLessThanEquals } }); - } - } - - export class StringGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.StringGreaterThanEquals } }); - } - } - - export class NumericEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.NumericEquals } }); - } - } - - export class NumericLessThanComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.NumericLessThan } }); - } - } - - export class NumericGreaterThanComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.NumericGreaterThan } }); - } - } - - export class NumericLessThanEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.NumericLessThanEquals } }); - } - } - - export class NumericGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.NumericGreaterThanEquals } }); - } - } - - export class BooleanEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.BooleanEquals } }); - } - } - - export class TimestampEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.TimestampEquals } }); - } - } - - export class TimestampLessThanComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.TimestampLessThan } }); - } - } - - export class TimestampGreaterThanComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.TimestampGreaterThan } }); - } - } - - export class TimestampLessThanEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.TimestampLessThanEquals } }); - } - } - - export class TimestampGreaterThanEqualsComparisonOperation extends VariableComparisonOperation { - constructor(props: VariableComparisonOperationProps) { - super({ ...props, ...{ comparisonOperator: ComparisonOperator.TimestampGreaterThanEquals } }); - } - } - - export interface ArrayComparisonOperationProps { - comparisonOperator: ComparisonOperator, - comparisonOperations: ComparisonOperation[] - } - - export abstract class ArrayComparisonOperation extends ComparisonOperation { - constructor(props: ArrayComparisonOperationProps) { - if (props.comparisonOperations.length === 0) { - throw new Error('\'comparisonOperations\' is empty. Must be non-empty array of ChoiceRules'); - } - super({ [ComparisonOperator[props.comparisonOperator]]: props.comparisonOperations }); - } - } - - export class AndComparisonOperation extends ArrayComparisonOperation { - constructor(...comparisonOperations: ComparisonOperation[]) { - super({ comparisonOperator: ComparisonOperator.And, comparisonOperations }); - } - } - - export class OrComparisonOperation extends ArrayComparisonOperation { - constructor(...comparisonOperations: ComparisonOperation[]) { - super({ comparisonOperator: ComparisonOperator.Or, comparisonOperations }); - } - } - - export class NotComparisonOperation extends ComparisonOperation { - constructor(comparisonOperation: ComparisonOperation) { - super({ [ComparisonOperator[ComparisonOperator.Not]]: comparisonOperation }); - } - } - - export interface ChoiceRuleProps extends NextField { - comparisonOperation: ComparisonOperation; - } - - /** - * A rule desribing a conditional state transition. - */ - export class ChoiceRule extends PascalCaseJson { - constructor(props: ChoiceRuleProps) { - super({ ...props.comparisonOperation.props, next: props.next }); - } - } - - /** - * An non-empty ordered array of ChoiceRule the interpreter will scan through - * in order to make a state transition choice. - */ - export class ChoiceRules extends PascalCaseJson { - constructor(...choices: ChoiceRule[]) { - if (choices.length === 0) { - throw new Error("'Choices' array is empty. Must specify non-empty array of ChoiceRule."); - } - super(choices); - } - - public get length(): number { - return this.props.length; - } - - public get nextStates(): string[] { - return this.props.map((choiceRule: ChoiceRule) => choiceRule.props.next); - } - } - - export interface ChoiceStateProps extends Commentable, InputOutputPathFields { - /** - * Ordered array of {@link ChoiceRule}s the interpreter will scan through. - */ - choices: ChoiceRules; - - /** - * Name of a state to transition to if none of the ChoiceRules in - * {@link choices} match. - * - * @default The interpreter will raise a {@link ErrorCode.NoChoiceMatched} - * error if no {@link ChoiceRule} matches and there is no {@link default} - * specified. - */ - default?: string | Token; - } - - /** - * Adds branching logic to a state machine. - * - * {@link https://states-language.net/spec.html#choice-state} - */ - export class ChoiceState extends BaseState { - constructor(props: ChoiceStateProps) { - if (props.choices.length === 0) { - throw new Error('\'choices\' is empty. Must be non-empty array of ChoiceRules'); - } - super(StateType.Choice, props); - } - - public next() { - return this.props.choices.nextStates.concat([this.props.default]); - } - } - - export interface WaitStateProps extends Commentable, InputOutputPathFields, NextOrEndField { - /** - * Number of seconds to wait. - */ - seconds?: number | Token - - /** - * {@link https://states-language.net/spec.html#ref-paths Reference Path} to a value for - * the number of seconds to wait. - */ - secondsPath?: string | Token, - - /** - * Wait until specified absolute time. - * - * Must be an ISO-8601 extended offsete date-time formatted string. - */ - timestamp?: string | Token, - - /** - * {@link https://states-language.net/spec.html#ref-paths Reference Path} to a value for - * an absolute expiry time. - * - * Value must be an ISO-8601 extended offsete date-time formatted string. - */ - timestampPath?: string | Token - } - - /** - * Delay the state machine for a specified time. - * - * {@link https://states-language.net/spec.html#wait-state} - */ - export class WaitState extends BaseState { - constructor(props: WaitStateProps) { - requireOneOf(props, ['seconds', 'secondsPath', 'timestamp', 'timestampPath']); - requireNextOrEnd(props); - super(StateType.Wait, props); - } - } - - export interface SucceedStateProps extends Commentable, InputOutputPathFields { } - - /** - * Terminate the state machine successfully. - * - * {@link https://states-language.net/spec.html#succeed-state} - */ - export class SucceedState extends BaseState { - constructor(props: SucceedStateProps = {}) { - super(StateType.Succeed, props); - } - - public isTerminal() { - return true; - } - } - - export interface FailStateProps extends Commentable { - /** - * An Error Name used for error handling in a {@link Retrier} or {@link Catcher}, - * or for operational/diagnostic purposes. - */ - error: string | Token, - - /** - * A human-readable message describing the error. - */ - cause: string | Token - } - - /** - * Terminate the state machine and mark the execution as a failure. - * - * {@link https://states-language.net/spec.html#fail-state} - */ - export class FailState extends BaseState { - constructor(props: FailStateProps) { - super(StateType.Fail, props); - } - - public isTerminal() { - return true; - } - } - - export interface ParallelStateProps extends Commentable, InputOutputPathFields, ResultPathField, RetryCatchFields, NextOrEndField { - /** - * An array of branches to execute in parallel. - */ - branches: Branches - } - - export class Branches extends PascalCaseJson { - private readonly branches: Branch[]; - - constructor(branches: Branch[]) { - super(branches); - this.branches = branches; - } - - public stateNames(): string[] { - const names: string[] = []; - this.branches.forEach(branch => branch.stateNames().forEach(name => names.push(name))); - return names; - } - } - - /** - * Executes branches in parallel. - * - * {@link https://states-language.net/spec.html#parallel-state} - */ - export class ParallelState extends BaseState { - private readonly branches: Branches; - - constructor(props: ParallelStateProps) { - requireNextOrEnd(props); - super(StateType.Parallel, props); - this.branches = props.branches; - } - - public stateNames(): string[] { - return this.branches.stateNames(); - } - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions/lib/index.ts index 33ce4fa49c8d8..77d5dd8a7d047 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/index.ts @@ -1,5 +1,4 @@ export * from './activity'; -export * from './asl'; export * from './asl-external-api'; export * from './asl-internal-api'; export * from './asl-condition'; @@ -9,7 +8,7 @@ export * from './states/choice'; export * from './states/fail'; export * from './states/parallel'; export * from './states/pass'; -export * from './states/state-machine-definition'; +export * from './states/state-machine-fragment'; export * from './states/state'; export * from './states/succeed'; export * from './states/task'; diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts index 10c5252e96c03..49065ebc37adc 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts @@ -1,6 +1,7 @@ +import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); -import { StateMachineDefinition } from './asl-states'; +import { IChainable } from './asl-external-api'; import { cloudformation, StateMachineArn, StateMachineName } from './stepfunctions.generated'; export interface StateMachineProps { @@ -14,7 +15,7 @@ export interface StateMachineProps { /** * Definition for this state machine */ - definition: StateMachineDefinition; + definition: IChainable; /** * The execution role for the state machine service @@ -32,6 +33,9 @@ export class StateMachine extends cdk.Construct { public readonly stateMachineName: StateMachineName; public readonly stateMachineArn: StateMachineArn; + /** A role used by CloudWatch events to trigger a build */ + private eventsRole?: iam.Role; + constructor(parent: cdk.Construct, id: string, props: StateMachineProps) { super(parent, id); @@ -39,14 +43,18 @@ export class StateMachine extends cdk.Construct { assumedBy: new cdk.ServicePrincipal(new cdk.FnConcat('states.', new cdk.AwsRegion(), '.amazonaws.com').toString()), }); + const rendered = props.definition.toStateChain().renderStateMachine(); + const resource = new cloudformation.StateMachineResource(this, 'Resource', { stateMachineName: props.stateMachineName, roleArn: this.role.roleArn, - // Depending on usage, definition may change after our instantiation - // (because we're organized like a mutable object tree) - definitionString: new cdk.Token(() => cdk.CloudFormationJSON.stringify(props.definition.toStateMachine())) + definitionString: cdk.CloudFormationJSON.stringify(rendered.stateMachineDefinition), }); + for (const statement of rendered.policyStatements) { + this.addToRolePolicy(statement); + } + this.stateMachineName = resource.stateMachineName; this.stateMachineArn = resource.ref; } @@ -54,4 +62,26 @@ export class StateMachine extends cdk.Construct { public addToRolePolicy(statement: cdk.PolicyStatement) { this.role.addToPolicy(statement); } + + /** + * Allows using state machines as event rule targets. + */ + public asEventRuleTarget(_ruleArn: events.RuleArn, _ruleId: string): events.EventRuleTargetProps { + if (!this.eventsRole) { + this.eventsRole = new iam.Role(this, 'EventsRole', { + assumedBy: new cdk.ServicePrincipal('events.amazonaws.com') + }); + + this.eventsRole.addToPolicy(new cdk.PolicyStatement() + .addAction('states:StartExecution') + .addResource(this.stateMachineArn)); + } + + return { + id: this.id, + arn: this.stateMachineArn, + roleArn: this.eventsRole.roleArn, + }; + } + } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts index 36ad70f4507bc..4f32efbd95c8f 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts @@ -1,14 +1,9 @@ +import cdk = require('@aws-cdk/cdk'); import { Condition } from '../asl-condition'; import { IChainable, IStateChain } from '../asl-external-api'; -import { IInternalState, StateBehavior, StateType } from '../asl-internal-api'; +import { IInternalState, StateType, TransitionType } from '../asl-internal-api'; import { StateChain } from '../asl-state-chain'; import { State } from './state'; -import { StateMachineDefinition } from './state-machine-definition'; - -interface ChoiceBranch { - condition: Condition; - next: IChainable; -} export interface ChoiceProps { inputPath?: string; @@ -17,54 +12,37 @@ export interface ChoiceProps { export class Choice extends State { private static Internals = class implements IInternalState { - public readonly stateBehavior: StateBehavior = { - canHaveCatch: false, - canHaveNext: false, - elidable: false - }; + public readonly canHaveCatch = false; + public readonly hasOpenNextTransition = false; + public readonly stateId: string; + public readonly policyStatements = new Array(); constructor(private readonly choice: Choice) { - } - - public get stateId(): string { - return this.choice.stateId; + this.stateId = choice.stateId; } public renderState() { - const defaultTransitions = this.choice.transitions.filter(t => t.annotation === undefined); - if (defaultTransitions.length > 1) { - throw new Error('Can only have one default transition'); - } - const choices = this.choice.transitions.filter(t => t.annotation !== undefined); - return { ...this.choice.renderBaseState(), - Choices: choices.map(c => ({ - ...c.annotation, - Next: c.targetState.stateId - })), - Default: defaultTransitions.length > 0 ? defaultTransitions[0].targetState.stateId : undefined + ...this.choice.transitions.renderList(TransitionType.Choice), + ...this.choice.transitions.renderSingle(TransitionType.Default), }; } - public next(_targetState: IInternalState): void { + public addNext(_targetState: IInternalState): void { throw new Error("Cannot chain onto a Choice state. Use the state's .on() or .otherwise() instead."); } - public catch(_targetState: IInternalState, _errors: string[]): void { + public addCatch(_targetState: IInternalState, _errors: string[]): void { throw new Error("Cannot catch errors on a Choice."); } - }; - public readonly stateBehavior: StateBehavior = { - canHaveCatch: false, - canHaveNext: false, - elidable: false - }; - private readonly choices: ChoiceBranch[] = []; - private hasDefault = false; + public accessibleStates() { + return this.choice.accessibleStates(); + } + }; - constructor(parent: StateMachineDefinition, id: string, props: ChoiceProps = {}) { + constructor(parent: cdk.Construct, id: string, props: ChoiceProps = {}) { super(parent, id, { Type: StateType.Choice, InputPath: props.inputPath, @@ -73,20 +51,24 @@ export class Choice extends State { } public on(condition: Condition, next: IChainable): Choice { - this.choices.push({ condition, next }); + this.transitions.add(TransitionType.Choice, next.toStateChain().startState, condition.renderCondition()); return this; } public otherwise(next: IChainable): Choice { - if (this.hasDefault) { - throw new Error('Can only have one default transition'); + // We use the "next" transition to store the Default, even though the meaning is different. + if (this.transitions.has(TransitionType.Default)) { + throw new Error('Can only have one Default transition'); } - this.hasDefault = true; - this.addTransition(next.toStateChain().startState, undefined); + this.transitions.add(TransitionType.Default, next.toStateChain().startState); return this; } public toStateChain(): IStateChain { return new StateChain(new Choice.Internals(this)); } + + public closure(): IStateChain { + return this.toStateChain().closure(); + } } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/fail.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/fail.ts index 69ade00340cd1..c1070d59b67c8 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/fail.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/fail.ts @@ -1,8 +1,8 @@ +import cdk = require('@aws-cdk/cdk'); import { IStateChain } from '../asl-external-api'; -import { IInternalState, StateBehavior, StateType } from '../asl-internal-api'; -import { StateChain } from '../asl-state-machine'; +import { IInternalState, StateType } from '../asl-internal-api'; +import { StateChain } from '../asl-state-chain'; import { State } from './state'; -import { StateMachineDefinition } from './state-machine-definition'; export interface FailProps { error: string; @@ -11,33 +11,33 @@ export interface FailProps { export class Fail extends State { private static Internals = class implements IInternalState { - public readonly stateBehavior: StateBehavior = { - canHaveCatch: false, - canHaveNext: false, - elidable: false - }; + public readonly canHaveCatch = false; + public readonly hasOpenNextTransition = false; + public readonly stateId: string; + public readonly policyStatements = new Array(); constructor(private readonly fail: Fail) { - } - - public get stateId(): string { - return this.fail.stateId; + this.stateId = fail.stateId; } public renderState() { return this.fail.renderBaseState(); } - public next(_targetState: IInternalState): void { + public addNext(_targetState: IInternalState): void { throw new Error("Cannot chain onto a Fail state. This ends the state machine."); } - public catch(_targetState: IInternalState, _errors: string[]): void { + public addCatch(_targetState: IInternalState, _errors: string[]): void { throw new Error("Cannot catch errors on a Fail."); } + + public accessibleStates() { + return this.fail.accessibleStates(); + } }; - constructor(parent: StateMachineDefinition, id: string, props: FailProps) { + constructor(parent: cdk.Construct, id: string, props: FailProps) { super(parent, id, { Type: StateType.Fail, Error: props.error, diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts index 1e4ecabb95ae8..32aff6c400b12 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts @@ -1,10 +1,9 @@ import cdk = require('@aws-cdk/cdk'); -import { Errors, IStateChain, RetryProps } from '../asl-external-api'; -import { IInternalState, StateBehavior, StateType } from '../asl-internal-api'; +import { Errors, IChainable, IStateChain, RetryProps } from '../asl-external-api'; +import { IInternalState, StateType, TransitionType } from '../asl-internal-api'; import { StateChain } from '../asl-state-chain'; import { State } from './state'; -import { StateMachineDefinition } from './state-machine-definition'; -import { renderNextEnd, renderRetry } from './util'; +import { renderRetries } from './util'; export interface ParallelProps { inputPath?: string; @@ -14,61 +13,63 @@ export interface ParallelProps { export class Parallel extends State { private static Internals = class implements IInternalState { - public readonly stateBehavior: StateBehavior = { - canHaveCatch: true, - canHaveNext: true, - elidable: false, - }; + public readonly canHaveCatch = true; + public readonly stateId: string; constructor(private readonly parallel: Parallel) { - } - - public get stateId(): string { - return this.parallel.stateId; + this.stateId = parallel.stateId; } public renderState() { - const catches = this.parallel.transitions.filter(t => t.annotation !== undefined); - const regularTransitions = this.parallel.transitions.filter(t => t.annotation === undefined); - - if (regularTransitions.length > 1) { - throw new Error(`State "${this.stateId}" can only have one outgoing transition`); - } - return { ...this.parallel.renderBaseState(), - ...renderNextEnd(regularTransitions), - Catch: catches.length === 0 ? undefined : catches.map(c => c.annotation), - Retry: new cdk.Token(() => this.parallel.retries.length === 0 ? undefined : this.parallel.retries.map(renderRetry)), + ...renderRetries(this.parallel.retries), + ...this.parallel.transitions.renderSingle(TransitionType.Next, { End: true }), + ...this.parallel.transitions.renderList(TransitionType.Catch), }; } - public next(targetState: IInternalState): void { + public addNext(targetState: IInternalState): void { this.parallel.addNextTransition(targetState); } - public catch(targetState: IInternalState, errors: string[]): void { - this.parallel.addTransition(targetState, { - ErrorEquals: errors, - Next: targetState.stateId - }); + public addCatch(targetState: IInternalState, errors: string[]): void { + this.parallel.transitions.add(TransitionType.Catch, targetState, { ErrorEquals: errors }); + } + + public accessibleStates() { + return this.parallel.accessibleStates(); + } + + public get hasOpenNextTransition(): boolean { + return !this.parallel.hasNextTransition; + } + + public get policyStatements(): cdk.PolicyStatement[] { + const ret = new Array(); + for (const branch of this.parallel.branches) { + ret.push(...branch.toStateChain().renderStateMachine().policyStatements); + } + return ret; } }; - private readonly branches: StateMachineDefinition[] = []; + private readonly branches: IChainable[] = []; private readonly retries = new Array(); - constructor(parent: StateMachineDefinition, id: string, props: ParallelProps = {}) { + constructor(parent: cdk.Construct, id: string, props: ParallelProps = {}) { super(parent, id, { Type: StateType.Parallel, InputPath: props.inputPath, OutputPath: props.outputPath, ResultPath: props.resultPath, - Branches: new cdk.Token(() => this.branches.map(b => b.renderStateMachine())) + // Lazy because the states are mutable and they might get chained onto + // (Users shouldn't, but they might) + Branches: new cdk.Token(() => this.branches.map(b => b.toStateChain().renderStateMachine().stateMachineDefinition)) }); } - public parallel(definition: StateMachineDefinition) { + public branch(definition: IChainable) { this.branches.push(definition); } @@ -79,6 +80,14 @@ export class Parallel extends State { this.retries.push(props); } + public next(sm: IChainable): IStateChain { + return this.toStateChain().next(sm); + } + + public onError(handler: IChainable, ...errors: string[]): IStateChain { + return this.toStateChain().onError(handler, ...errors); + } + public toStateChain(): IStateChain { return new StateChain(new Parallel.Internals(this)); } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts index ec8ba43aaabad..678831ccb679b 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts @@ -1,59 +1,58 @@ -import { IStateChain } from '../asl-external-api'; -import { IInternalState, StateBehavior, StateType } from '../asl-internal-api'; +import cdk = require('@aws-cdk/cdk'); +import { IChainable, IStateChain } from '../asl-external-api'; +import { IInternalState, StateType, TransitionType } from '../asl-internal-api'; import { StateChain } from '../asl-state-chain'; import { State } from './state'; -import { StateMachineDefinition } from './state-machine-definition'; -import { renderNextEnd } from './util'; export interface PassProps { inputPath?: string; outputPath?: string; - elidable?: boolean; } export class Pass extends State { private static Internals = class implements IInternalState { - constructor(private readonly pass: Pass) { - } - - public get stateId(): string { - return this.pass.stateId; - } + public readonly canHaveCatch = false; + public readonly stateId: string; + public readonly policyStatements = new Array(); - public get stateBehavior(): StateBehavior { - return { - canHaveNext: true, - canHaveCatch: false, - elidable: this.pass.elidable - }; + constructor(private readonly pass: Pass) { + this.stateId = this.pass.stateId; } public renderState() { - const regularTransitions = this.pass.getTransitions(false); - return { ...this.pass.renderBaseState(), - ...renderNextEnd(regularTransitions), + ...this.pass.transitions.renderSingle(TransitionType.Next, { End: true }), }; } - public next(targetState: IInternalState): void { + public addNext(targetState: IInternalState): void { this.pass.addNextTransition(targetState); } - public catch(_targetState: IInternalState, _errors: string[]): void { + public addCatch(_targetState: IInternalState, _errors: string[]): void { throw new Error("Cannot catch errors on a Pass."); } + + public accessibleStates() { + return this.pass.accessibleStates(); + } + + public get hasOpenNextTransition(): boolean { + return !this.pass.hasNextTransition; + } }; - private readonly elidable: boolean; - constructor(parent: StateMachineDefinition, id: string, props: PassProps = {}) { + constructor(parent: cdk.Construct, id: string, props: PassProps = {}) { super(parent, id, { Type: StateType.Pass, InputPath: props.inputPath, OutputPath: props.outputPath }); - this.elidable = (props.elidable || false) && !props.inputPath && !props.outputPath; + } + + public next(sm: IChainable): IStateChain { + return this.toStateChain().next(sm); } public toStateChain(): IStateChain { diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/state-machine-definition.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/state-machine-definition.ts deleted file mode 100644 index 2e85d45a41dee..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/state-machine-definition.ts +++ /dev/null @@ -1,74 +0,0 @@ -import iam = require('@aws-cdk/aws-iam'); -import cdk = require('@aws-cdk/cdk'); -import { IChainable, IState, IStateChain } from "../asl-external-api"; - -export interface StateMachineDefinitionProps { - timeoutSeconds?: number; -} - -export class StateMachineDefinition extends cdk.Construct implements IChainable { - /** - * Used to find this Construct back in the construct tree - */ - public readonly isStateMachine = true; - - private readonly timeoutSeconds?: number; - private readonly policyStatements = new Array(); - private readonly states = new Array(); - private startState?: IState; - private policyRole?: iam.Role; - private sm?: IStateChain; - - constructor(parent: cdk.Construct, id: string, props: StateMachineDefinitionProps = {}) { - super(parent, id); - this.timeoutSeconds = props.timeoutSeconds; - } - - public start(state: IState): IStateChain { - this.startState = state; - return state.toStateChain(); - } - - public addToRolePolicy(statement: cdk.PolicyStatement) { - // This may be called before and after attaching to a StateMachine. - // Cache the policy statements added in this way if before attaching, - // otherwise attach to role directly. - if (this.policyRole) { - this.policyRole.addToPolicy(statement); - } else { - this.policyStatements.push(statement); - } - } - - public addPolicyStatementsToRole(role: iam.Role) { - // Add all cached policy statements, then remember the policy - // for future additions. - for (const s of this.policyStatements) { - role.addToPolicy(s); - } - this.policyStatements.splice(0); // Clear array - this.policyRole = role; - } - - public toStateChain(): IStateChain { - if (!this.sm) { - throw new Error('No state machine define with .define()'); - } - - // FIXME: Use somewhere - Array.isArray(this.timeoutSeconds); - - return this.sm; - } - - public renderStateMachine(): any { - return {}; - } - - public _addState(state: IState) { - if (this.startState === undefined) { - this.startState = state; - } - this.states.push(state); - } -} diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/state-machine-fragment.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/state-machine-fragment.ts new file mode 100644 index 0000000000000..2428349fae294 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/state-machine-fragment.ts @@ -0,0 +1,69 @@ +import cdk = require('@aws-cdk/cdk'); +import { IChainable, IStateChain } from "../asl-external-api"; + +export interface StateMachineFragmentProps { + timeoutSeconds?: number; + + /** + * Whether to add the fragment name to the states defined within + * + * @default true + */ + scopeStateNames?: boolean; +} + +export class StateMachineFragment extends cdk.Construct implements IChainable { + /** + * Used to find this Construct back in the construct tree + */ + public readonly isStateMachine = true; + + public readonly scopeStateNames: boolean; + + private readonly timeoutSeconds?: number; + private _startState?: IChainable; + + constructor(parent: cdk.Construct, id: string, props: StateMachineFragmentProps = {}) { + super(parent, id); + this.timeoutSeconds = props.timeoutSeconds; + this.scopeStateNames = props.scopeStateNames !== undefined ? props.scopeStateNames : true; + } + + /** + * Explicitly set a start state + */ + public start(state: IChainable): IStateChain { + this._startState = state; + return state.toStateChain(); + } + + public toStateChain(): IStateChain { + // FIXME: Use somewhere + Array.isArray(this.timeoutSeconds); + + // If we're converting a state machine definition to a state chain, grab the whole of it. + return this.startState.toStateChain().closure(); + } + + public next(sm: IChainable): IStateChain { + return this.toStateChain().next(sm); + } + + private get startState(): IChainable { + if (this._startState) { + return this._startState; + } + + // If no explicit start state given, find the first child that is a state + const firstStateChild = this.children.find(isChainable); + if (!isChainable(firstStateChild)) { + throw new Error('State Machine definition does not contain any states'); + } + + return firstStateChild as IChainable; + } +} + +function isChainable(x: any): x is IChainable { + return x && x.toStateChain !== undefined; +} diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts index f26e54db53e83..798067c042def 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts @@ -1,36 +1,23 @@ import cdk = require('@aws-cdk/cdk'); import { IChainable, IStateChain } from "../asl-external-api"; -import { IInternalState, Transition } from '../asl-internal-api'; -import { StateMachineDefinition } from './state-machine-definition'; +import { IInternalState, Transitions, TransitionType } from '../asl-internal-api'; +import { StateMachineFragment } from './state-machine-fragment'; export abstract class State extends cdk.Construct implements IChainable { - protected readonly transitions = new Array(); + protected readonly transitions = new Transitions(); - constructor(parent: StateMachineDefinition, id: string, private readonly options: any) { + constructor(parent: cdk.Construct, id: string, private readonly options: any) { super(parent, id); - - parent._addState(this); } public abstract toStateChain(): IStateChain; - /** - * Convenience function to immediately go into State Machine mode - */ - public then(sm: IChainable): IStateChain { - return this.toStateChain().then(sm); - } - - public catch(handler: IChainable, ...errors: string[]): IStateChain { - return this.toStateChain().catch(handler, ...errors); - } - /** * Find the top-level StateMachine we're part of */ - protected containingStateMachine(): StateMachineDefinition { + protected containingStateMachineFragments(): StateMachineFragment { let curr: cdk.Construct | undefined = this; - while (curr && !isStateMachine(curr)) { + while (curr && !isStateMachineFragment(curr)) { curr = curr.parent; } if (!curr) { @@ -47,25 +34,26 @@ export abstract class State extends cdk.Construct implements IChainable { * Return the name of this state */ protected get stateId(): string { - return this.ancestors(this.containingStateMachine()).map(x => x.id).join('/'); + const parentDefs: cdk.Construct[] = this.ancestors().filter(c => (isStateMachineFragment(c) && c.scopeStateNames) || c === this); + return parentDefs.map(x => x.id).join('/'); } - protected addTransition(targetState: IInternalState, annotation?: any) { - this.transitions.push({ targetState, annotation }); + protected accessibleStates(): IInternalState[] { + return this.transitions.all().map(t => t.targetState); } - protected getTransitions(withAnnotation: boolean): Transition[] { - return this.transitions.filter(t => (t.annotation === undefined) === withAnnotation); + protected get hasNextTransition() { + return this.transitions.has(TransitionType.Next); } protected addNextTransition(targetState: IInternalState): void { - if (this.getTransitions(false).length > 0) { + if (this.hasNextTransition) { throw new Error(`State ${this.stateId} already has a Next transition`); } - this.addTransition(targetState); + this.transitions.add(TransitionType.Next, targetState); } } -function isStateMachine(construct: cdk.Construct): construct is StateMachineDefinition { +function isStateMachineFragment(construct: cdk.Construct): construct is StateMachineFragment { return (construct as any).isStateMachine; } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts index ad0f72476f68d..03bde37727108 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts @@ -1,49 +1,41 @@ +import cdk = require('@aws-cdk/cdk'); import { IStateChain } from '../asl-external-api'; -import { IInternalState, StateBehavior, StateType } from '../asl-internal-api'; -import { StateChain } from '../asl-state-machine'; +import { IInternalState, StateType } from '../asl-internal-api'; +import { StateChain } from '../asl-state-chain'; import { State } from './state'; -import { StateMachineDefinition } from './state-machine-definition'; - -export interface SucceedProps { - elidable?: boolean; -} export class Succeed extends State { private static Internals = class implements IInternalState { - constructor(private readonly succeed: Succeed) { - } + public readonly hasOpenNextTransition = false; + public readonly canHaveCatch = false; + public readonly stateId: string; + public readonly policyStatements = new Array(); - public get stateId(): string { - return this.succeed.stateId; + constructor(private readonly succeed: Succeed) { + this.stateId = succeed.stateId; } public renderState() { return this.succeed.renderBaseState(); } - public get stateBehavior(): StateBehavior { - return { - canHaveCatch: false, - canHaveNext: false, - elidable: this.succeed.elidable, - }; - } - - public next(_targetState: IInternalState): void { + public addNext(_targetState: IInternalState): void { throw new Error("Cannot chain onto a Succeed state; this ends the state machine."); } - public catch(_targetState: IInternalState, _errors: string[]): void { + public addCatch(_targetState: IInternalState, _errors: string[]): void { throw new Error("Cannot catch errors on a Succeed."); } + + public accessibleStates() { + return this.succeed.accessibleStates(); + } }; - private readonly elidable: boolean; - constructor(parent: StateMachineDefinition, id: string, props: SucceedProps = {}) { + constructor(parent: cdk.Construct, id: string) { super(parent, id, { Type: StateType.Succeed }); - this.elidable = props.elidable || false; } public toStateChain(): IStateChain { diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts index 1aafbae0593e6..b6b1055149e78 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts @@ -1,10 +1,9 @@ import cdk = require('@aws-cdk/cdk'); -import { Errors, IStateChain, RetryProps } from '../asl-external-api'; -import { IInternalState, StateBehavior, StateType } from '../asl-internal-api'; +import { Errors, IChainable, IStateChain, RetryProps } from '../asl-external-api'; +import { IInternalState, StateType, TransitionType } from '../asl-internal-api'; import { StateChain } from '../asl-state-chain'; import { State } from './state'; -import { StateMachineDefinition } from './state-machine-definition'; -import { renderNextEnd, renderRetry } from './util'; +import { renderRetries } from './util'; /** * Interface for objects that can be invoked in a Task state @@ -18,6 +17,7 @@ export interface IStepFunctionsTaskResource { export interface StepFunctionsTaskResourceProps { resourceArn: cdk.Arn; + policyStatements?: cdk.PolicyStatement[]; } export interface TaskProps { @@ -31,51 +31,45 @@ export interface TaskProps { export class Task extends State { private static Internals = class implements IInternalState { - public readonly stateBehavior: StateBehavior = { - canHaveCatch: true, - canHaveNext: true, - elidable: false, - }; + public readonly canHaveCatch = true; + public readonly stateId: string; + public readonly policyStatements: cdk.PolicyStatement[]; constructor(private readonly task: Task) { - } - - public get stateId(): string { - return this.task.stateId; + this.stateId = task.stateId; + this.policyStatements = task.resourceProps.policyStatements || []; } public renderState() { - const catches = this.task.transitions.filter(t => t.annotation !== undefined); - const regularTransitions = this.task.transitions.filter(t => t.annotation === undefined); - - if (regularTransitions.length > 1) { - throw new Error(`State "${this.stateId}" can only have one outgoing transition`); - } - return { ...this.task.renderBaseState(), - ...renderNextEnd(regularTransitions), - Catch: catches.length === 0 ? undefined : catches.map(c => c.annotation), - Retry: this.task.retries.length === 0 ? undefined : this.task.retries.map(renderRetry), + ...renderRetries(this.task.retries), + ...this.task.transitions.renderSingle(TransitionType.Next, { End: true }), + ...this.task.transitions.renderList(TransitionType.Catch), }; } - public next(targetState: IInternalState): void { + public addNext(targetState: IInternalState): void { this.task.addNextTransition(targetState); } - public catch(targetState: IInternalState, errors: string[]): void { - this.task.addTransition(targetState, { - ErrorEquals: errors, - Next: targetState.stateId - }); + public addCatch(targetState: IInternalState, errors: string[]): void { + this.task.transitions.add(TransitionType.Catch, targetState, { ErrorEquals: errors }); + } + + public accessibleStates() { + return this.task.accessibleStates(); + } + + public get hasOpenNextTransition(): boolean { + return !this.task.hasNextTransition; } }; private readonly resourceProps: StepFunctionsTaskResourceProps; private readonly retries = new Array(); - constructor(parent: StateMachineDefinition, id: string, props: TaskProps) { + constructor(parent: cdk.Construct, id: string, props: TaskProps) { super(parent, id, { Type: StateType.Task, InputPath: props.inputPath, @@ -88,11 +82,12 @@ export class Task extends State { this.resourceProps = props.resource.asStepFunctionsTaskResource(this); } - /** - * Add a policy statement to the role that ultimately executes this - */ - public addToRolePolicy(statement: cdk.PolicyStatement) { - this.containingStateMachine().addToRolePolicy(statement); + public next(sm: IChainable): IStateChain { + return this.toStateChain().next(sm); + } + + public onError(handler: IChainable, ...errors: string[]): IStateChain { + return this.toStateChain().onError(handler, ...errors); } public retry(props: RetryProps = {}) { diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/util.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/util.ts index 753ea82307698..a0bef29cf2374 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/util.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/util.ts @@ -1,16 +1,5 @@ +import cdk = require('@aws-cdk/cdk'); import { RetryProps } from "../asl-external-api"; -import { Transition } from "../asl-internal-api"; - -export function renderNextEnd(transitions: Transition[]) { - if (transitions.some(t => t.annotation !== undefined)) { - throw new Error('renderNextEnd() can only be used on default transitions'); - } - - return { - Next: transitions.length > 0 ? transitions[0].targetState.stateId : undefined, - End: transitions.length > 0 ? undefined : true, - }; -} export function renderRetry(retry: RetryProps) { return { @@ -20,3 +9,9 @@ export function renderRetry(retry: RetryProps) { BackoffRate: retry.backoffRate }; } + +export function renderRetries(retries: RetryProps[]) { + return { + Retry: new cdk.Token(() => retries.length === 0 ? undefined : retries.map(renderRetry)) + }; +} diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/wait.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/wait.ts index 384d8385f216e..5a5c234721686 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/wait.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/wait.ts @@ -1,9 +1,8 @@ -import { IStateChain } from '../asl-external-api'; -import { IInternalState, StateBehavior, StateType } from '../asl-internal-api'; +import cdk = require('@aws-cdk/cdk'); +import { IChainable, IStateChain } from '../asl-external-api'; +import { IInternalState, StateType, TransitionType } from '../asl-internal-api'; import { StateChain } from '../asl-state-chain'; import { State } from './state'; -import { StateMachineDefinition } from './state-machine-definition'; -import { renderNextEnd } from './util'; export interface WaitProps { seconds?: number; @@ -15,39 +14,42 @@ export interface WaitProps { export class Wait extends State { private static Internals = class implements IInternalState { - public readonly stateBehavior: StateBehavior = { - canHaveCatch: false, - canHaveNext: true, - elidable: false - }; + public readonly canHaveCatch = false; + public readonly stateId: string; + public readonly policyStatements = new Array(); constructor(private readonly wait: Wait) { - } - - public get stateId(): string { - return this.wait.stateId; + this.stateId = wait.stateId; } public renderState() { return { ...this.wait.renderBaseState(), - ...renderNextEnd(this.wait.transitions), + ...this.wait.transitions.renderSingle(TransitionType.Next, { End: true }), }; } - public next(targetState: IInternalState): void { - this.wait.addTransition(targetState); + public addNext(targetState: IInternalState): void { + this.wait.addNextTransition(targetState); } - public catch(_targetState: IInternalState, _errors: string[]): void { + public addCatch(_targetState: IInternalState, _errors: string[]): void { throw new Error("Cannot catch errors on a Wait."); } + + public accessibleStates() { + return this.wait.accessibleStates(); + } + + public get hasOpenNextTransition(): boolean { + return !this.wait.hasNextTransition; + } }; - constructor(parent: StateMachineDefinition, id: string, props: WaitProps) { + constructor(parent: cdk.Construct, id: string, props: WaitProps) { // FIXME: Validate input super(parent, id, { - Type: StateType.Task, + Type: StateType.Wait, Seconds: props.seconds, Timestamp: props.timestamp, SecondsPath: props.secondsPath, @@ -55,6 +57,10 @@ export class Wait extends State { }); } + public next(sm: IChainable): IStateChain { + return this.toStateChain().next(sm); + } + public toStateChain(): IStateChain { return new StateChain(new Wait.Internals(this)); } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/util.ts b/packages/@aws-cdk/aws-stepfunctions/lib/util.ts deleted file mode 100644 index 6a46b09e5f7de..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions/lib/util.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function requireOneOf(props: { [name: string]: any }, names: string[]) { - if (names.map(name => name in props).filter(x => x === true).length !== 1) { - throw new Error(`${JSON.stringify(props)} must specify exactly one of: ${names}`); - } -} - -export function requireNextOrEnd(props: any) { - requireOneOf(props, ['next', 'end']); -} diff --git a/packages/@aws-cdk/aws-stepfunctions/package.json b/packages/@aws-cdk/aws-stepfunctions/package.json index 9a24127c14b35..711cadeabb374 100644 --- a/packages/@aws-cdk/aws-stepfunctions/package.json +++ b/packages/@aws-cdk/aws-stepfunctions/package.json @@ -53,6 +53,8 @@ }, "dependencies": { "@aws-cdk/cdk": "^0.8.2", + "@aws-cdk/aws-cloudwatch": "^0.8.2", + "@aws-cdk/aws-events": "^0.8.2", "@aws-cdk/aws-iam": "^0.8.2" }, "homepage": "https://github.com/awslabs/aws-cdk" diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts index f40169f250032..497159e1916b3 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts @@ -1,7 +1,79 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; +import stepfunctions = require('../lib'); export = { 'Tasks can add permissions to the execution role'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const task = new stepfunctions.Task(stack, 'Task', { + resource: new FakeResource(), + }); + + // WHEN + new stepfunctions.StateMachine(stack, 'SM', { + definition: task + }); + + // THEN + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: "resource:Everything", + Effect: "Allow", + Resource: "resource" + } + ], + } + })); + + test.done(); + }, + + 'Tasks hidden inside a Parallel state are also included'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const task = new stepfunctions.Task(stack, 'Task', { + resource: new FakeResource(), + }); + + const para = new stepfunctions.Parallel(stack, 'Para'); + para.branch(task); + + // WHEN + new stepfunctions.StateMachine(stack, 'SM', { + definition: para + }); + + // THEN + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: "resource:Everything", + Effect: "Allow", + Resource: "resource" + } + ], + } + })); + test.done(); }, -}; \ No newline at end of file +}; + +class FakeResource implements stepfunctions.IStepFunctionsTaskResource { + public asStepFunctionsTaskResource(_callingTask: stepfunctions.Task): stepfunctions.StepFunctionsTaskResourceProps { + const resourceArn = new cdk.Arn('resource'); + + return { + resourceArn, + policyStatements: [new cdk.PolicyStatement() + .addAction('resource:Everything') + .addResource(new cdk.Arn('resource')) + ] + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts index 50d0638ffea3e..9c40fc32fb862 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts @@ -1,19 +1,19 @@ import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; import stepfunctions = require('../lib'); +import { IChainable } from '../lib'; export = { 'Basic composition': { 'A single task is a State Machine'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); // WHEN - new stepfunctions.Pass(sm, 'Some State'); + const chain = new stepfunctions.Pass(stack, 'Some State'); // THEN - test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + test.deepEqual(render(chain), { StartAt: 'Some State', States: { 'Some State': { Type: 'Pass', End: true } @@ -26,16 +26,15 @@ export = { 'A sequence of two tasks is a State Machine'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); // WHEN - const task1 = new stepfunctions.Pass(sm, 'State One'); - const task2 = new stepfunctions.Pass(sm, 'State Two'); + const task1 = new stepfunctions.Pass(stack, 'State One'); + const task2 = new stepfunctions.Pass(stack, 'State Two'); - task1.then(task2); + const chain = task1.next(task2); // THEN - test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + test.deepEqual(render(chain), { StartAt: 'State One', States: { 'State One': { Type: 'Pass', Next: 'State Two' }, @@ -46,45 +45,41 @@ export = { test.done(); }, - 'A chain can be appended to'(test: Test) { + 'You dont need to hold on to the state to render the entire state machine correctly'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); - - const task1 = new stepfunctions.Pass(sm, 'State One'); - const task2 = new stepfunctions.Pass(sm, 'State Two'); - const task3 = new stepfunctions.Pass(sm, 'State Three'); // WHEN - task1.then(task2).then(task3); + const task1 = new stepfunctions.Pass(stack, 'State One'); + const task2 = new stepfunctions.Pass(stack, 'State Two'); + + task1.next(task2); // THEN - test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + test.deepEqual(render(task1), { StartAt: 'State One', States: { 'State One': { Type: 'Pass', Next: 'State Two' }, - 'State Two': { Type: 'Pass', Next: 'State Three' }, - 'State Three': { Type: 'Pass', End: true }, + 'State Two': { Type: 'Pass', End: true }, } }); test.done(); }, - 'A state machine can be appended to another state machine'(test: Test) { + 'A chain can be appended to'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); - const task1 = new stepfunctions.Pass(sm, 'State One'); - const task2 = new stepfunctions.Pass(sm, 'State Two'); - const task3 = new stepfunctions.Pass(sm, 'State Three'); + const task1 = new stepfunctions.Pass(stack, 'State One'); + const task2 = new stepfunctions.Pass(stack, 'State Two'); + const task3 = new stepfunctions.Pass(stack, 'State Three'); // WHEN - task1.then(task2.then(task3)); + const chain = task1.next(task2).next(task3); // THEN - test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + test.deepEqual(render(chain), { StartAt: 'State One', States: { 'State One': { Type: 'Pass', Next: 'State Two' }, @@ -96,18 +91,25 @@ export = { test.done(); }, - 'Two chained states must be in the same state machine definition'(test: Test) { + 'A state machine can be appended to another state machine'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const sm1 = new stepfunctions.StateMachineDefinition(stack, 'SM1'); - const sm2 = new stepfunctions.StateMachineDefinition(stack, 'SM2'); - const pass1 = new stepfunctions.Pass(sm1, 'Pass1'); - const pass2 = new stepfunctions.Pass(sm2, 'Pass2'); + const task1 = new stepfunctions.Pass(stack, 'State One'); + const task2 = new stepfunctions.Pass(stack, 'State Two'); + const task3 = new stepfunctions.Wait(stack, 'State Three', { seconds: 10 }); + + // WHEN + const chain = task1.next(task2.next(task3)); // THEN - test.throws(() => { - pass1.then(pass2); + test.deepEqual(render(chain), { + StartAt: 'State One', + States: { + 'State One': { Type: 'Pass', Next: 'State Two' }, + 'State Two': { Type: 'Pass', Next: 'State Three' }, + 'State Three': { Type: 'Wait', End: true, Seconds: 10 }, + } }); test.done(); @@ -116,15 +118,14 @@ export = { 'A state machine definition can be instantiated and chained'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); - const before = new stepfunctions.Pass(sm, 'Before'); - const after = new stepfunctions.Pass(sm, 'After'); + const before = new stepfunctions.Pass(stack, 'Before'); + const after = new stepfunctions.Pass(stack, 'After'); // WHEN - before.then(new ReusableStateMachine(stack, 'Reusable')).then(after); + const chain = before.next(new ReusableStateMachine(stack, 'Reusable')).next(after); // THEN - test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + test.deepEqual(render(chain), { StartAt: 'Before', States: { 'Before': { Type: 'Pass', Next: 'Reusable/Choice' }, @@ -147,14 +148,14 @@ export = { 'A success state cannot be chained onto'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); + const sm = new stepfunctions.StateMachineFragment(stack, 'SM'); const succeed = new stepfunctions.Succeed(sm, 'Succeed'); const pass = new stepfunctions.Pass(sm, 'Pass'); // WHEN test.throws(() => { - succeed.then(pass); + succeed.toStateChain().next(pass); }); test.done(); @@ -163,39 +164,33 @@ export = { 'A failure state cannot be chained onto'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); + const sm = new stepfunctions.StateMachineFragment(stack, 'SM'); const fail = new stepfunctions.Fail(sm, 'Fail', { error: 'X', cause: 'Y' }); const pass = new stepfunctions.Pass(sm, 'Pass'); // WHEN test.throws(() => { - fail.then(pass); + fail.toStateChain().next(pass); }); test.done(); }, - 'Parallels contains adhoc state machine definitions without scoping names'(test: Test) { + 'Parallels can contain direct states'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); - - // WHEN - const para = new stepfunctions.Parallel(sm, 'Parallel'); - - // Key: the parent is the same parent as the top-level StateMachineDefinition. - const branch1 = new stepfunctions.StateMachineDefinition(stack, 'Branch1'); - branch1.start(new stepfunctions.Pass(branch1, 'One')) - .then(new stepfunctions.Pass(branch1, 'Two')); - const branch2 = new stepfunctions.StateMachineDefinition(stack, 'Branch1'); - branch2.start(new stepfunctions.Pass(branch2, 'Three')); + const one = new stepfunctions.Pass(stack, 'One'); + const two = new stepfunctions.Pass(stack, 'Two'); + const three = new stepfunctions.Pass(stack, 'Three'); - para.parallel(branch1); - para.parallel(branch2); + // WHEN + const para = new stepfunctions.Parallel(stack, 'Parallel'); + para.branch(one.next(two)); + para.branch(three); // THEN - test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + test.deepEqual(render(para), { StartAt: 'Parallel', States: { Parallel: { @@ -226,15 +221,14 @@ export = { 'Parallels can contain instantiated reusable definitions'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); // WHEN - const para = new stepfunctions.Parallel(sm, 'Parallel'); - para.parallel(new ReusableStateMachine(sm, 'Reusable1')); - para.parallel(new ReusableStateMachine(sm, 'Reusable2')); + const para = new stepfunctions.Parallel(stack, 'Parallel'); + para.branch(new ReusableStateMachine(stack, 'Reusable1')); + para.branch(new ReusableStateMachine(stack, 'Reusable2')); // THEN - test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + test.deepEqual(render(para), { StartAt: 'Parallel', States: { Parallel: { @@ -280,20 +274,19 @@ export = { 'Chaining onto branched failure state ignores failure state'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); - const yes = new stepfunctions.Pass(sm, 'Yes'); - const no = new stepfunctions.Fail(sm, 'No', { error: 'Failure', cause: 'Wrong branch' }); - const enfin = new stepfunctions.Pass(sm, 'Finally'); - const choice = new stepfunctions.Choice(sm, 'Choice') + const yes = new stepfunctions.Pass(stack, 'Yes'); + const no = new stepfunctions.Fail(stack, 'No', { error: 'Failure', cause: 'Wrong branch' }); + const enfin = new stepfunctions.Pass(stack, 'Finally'); + const choice = new stepfunctions.Choice(stack, 'Choice') .on(stepfunctions.Condition.stringEquals('$.foo', 'bar'), yes) .otherwise(no); // WHEN - choice.then(enfin); + choice.closure().next(enfin); // THEN - test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + test.deepEqual(render(choice), { StartAt: 'Choice', States: { Choice: { @@ -313,88 +306,19 @@ export = { }, }, - 'Elision': { - 'An elidable pass state that is chained onto should disappear'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); - - const first = new stepfunctions.Pass(sm, 'First'); - const success = new stepfunctions.Pass(sm, 'Success', { elidable: true }); - const second = new stepfunctions.Pass(sm, 'Second'); - - // WHEN - first.then(success).then(second); - - // THEN - test.deepEqual(cdk.resolve(sm.renderStateMachine()), { - StartAt: 'First', - States: { - First: { Type: 'Pass', Next: 'Second' }, - Second: { Type: 'Pass', End: true }, - } - }); - - test.done(); - }, - - 'An elidable pass state at the end of a chain should disappear'(test: Test) { - const stack = new cdk.Stack(); - const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); - - const first = new stepfunctions.Pass(sm, 'First'); - const pass = new stepfunctions.Pass(sm, 'Pass', { elidable: true }); - - // WHEN - first.then(pass); - - // THEN - test.deepEqual(cdk.resolve(sm.renderStateMachine()), { - StartAt: 'First', - States: { - First: { Type: 'Pass', End: true }, - } - }); - - test.done(); - }, - - 'An elidable success state at the end of a chain should disappear'(test: Test) { - const stack = new cdk.Stack(); - const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); - - const first = new stepfunctions.Pass(sm, 'First'); - const success = new stepfunctions.Succeed(sm, 'Success', { elidable: true }); - - // WHEN - first.then(success); - - // THEN - test.deepEqual(cdk.resolve(sm.renderStateMachine()), { - StartAt: 'First', - States: { - First: { Type: 'Pass', End: true }, - } - }); - - test.done(); - }, - }, - 'Goto support': { 'State machines can have unconstrainted gotos'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); - const one = new stepfunctions.Pass(sm, 'One'); - const two = new stepfunctions.Pass(sm, 'Two'); + const one = new stepfunctions.Pass(stack, 'One'); + const two = new stepfunctions.Pass(stack, 'Two'); // WHEN - one.then(two).then(one); + const chain = one.next(two).next(one); // THEN - test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + test.deepEqual(render(chain), { StartAt: 'One', States: { One: { Type: 'Pass', Next: 'Two' }, @@ -410,16 +334,14 @@ export = { 'States can have error branches'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); - - const task1 = new stepfunctions.Task(sm, 'Task1', { resource: new FakeResource() }); - const failure = new stepfunctions.Fail(sm, 'Failed', { error: 'DidNotWork', cause: 'We got stuck' }); + const task1 = new stepfunctions.Task(stack, 'Task1', { resource: new FakeResource() }); + const failure = new stepfunctions.Fail(stack, 'Failed', { error: 'DidNotWork', cause: 'We got stuck' }); // WHEN - task1.toStateChain().catch(failure); + const chain = task1.onError(failure); // THEN - test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + test.deepEqual(render(chain), { StartAt: 'Task1', States: { Task1: { @@ -444,17 +366,16 @@ export = { 'Error branch is attached to all tasks in chain'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); - const task1 = new stepfunctions.Task(sm, 'Task1', { resource: new FakeResource() }); - const task2 = new stepfunctions.Task(sm, 'Task2', { resource: new FakeResource() }); - const errorHandler = new stepfunctions.Pass(sm, 'ErrorHandler'); + const task1 = new stepfunctions.Task(stack, 'Task1', { resource: new FakeResource() }); + const task2 = new stepfunctions.Task(stack, 'Task2', { resource: new FakeResource() }); + const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); // WHEN - task1.then(task2).catch(errorHandler); + const chain = task1.next(task2).onError(errorHandler); // THEN - test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + test.deepEqual(render(chain), { StartAt: 'Task1', States: { Task1: { @@ -480,20 +401,23 @@ export = { test.done(); }, + /* + + ** FIXME: Not implemented at the moment, since we need to make a Construct for this and the + name and parent aren't obvious. + 'Machine is wrapped in parallel if not all tasks can have catch'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); - const task1 = new stepfunctions.Task(sm, 'Task1', { resource: new FakeResource() }); - const wait = new stepfunctions.Wait(sm, 'Wait', { seconds: 10 }); - const errorHandler = new stepfunctions.Pass(sm, 'ErrorHandler'); + const task1 = new stepfunctions.Task(stack, 'Task1', { resource: new FakeResource() }); + const wait = new stepfunctions.Wait(stack, 'Wait', { seconds: 10 }); + const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); - // WHEN - task1.then(wait).catch(errorHandler); + const chain = task1.next(wait).onError(errorHandler); // THEN - test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + test.deepEqual(render(chain), { StartAt: 'Para', States: { Para: { @@ -526,21 +450,21 @@ export = { test.done(); }, + */ - 'Error branch does not count for chaining'(test: Test) { + 'Chaining does not chain onto error handler state'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); - const task1 = new stepfunctions.Task(sm, 'Task1', { resource: new FakeResource() }); - const task2 = new stepfunctions.Task(sm, 'Task2', { resource: new FakeResource() }); - const errorHandler = new stepfunctions.Pass(sm, 'ErrorHandler'); + const task1 = new stepfunctions.Task(stack, 'Task1', { resource: new FakeResource() }); + const task2 = new stepfunctions.Task(stack, 'Task2', { resource: new FakeResource() }); + const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); // WHEN - task1.catch(errorHandler).then(task2); + const chain = task1.onError(errorHandler).next(task2); // THEN - test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + test.deepEqual(render(chain), { StartAt: 'Task1', States: { Task1: { @@ -559,27 +483,58 @@ export = { test.done(); }, + 'After calling .closure() do chain onto error state'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const task1 = new stepfunctions.Task(stack, 'Task1', { resource: new FakeResource() }); + const task2 = new stepfunctions.Task(stack, 'Task2', { resource: new FakeResource() }); + const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); + + // WHEN + const chain = task1.onError(errorHandler).closure().next(task2); + + // THEN + test.deepEqual(render(chain), { + StartAt: 'Task1', + States: { + Task1: { + Type: 'Task', + Resource: 'resource', + Next: 'Task2', + Catch: [ + { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler' }, + ] + }, + ErrorHandler: { Type: 'Pass', Next: 'Task2' }, + Task2: { Type: 'Task', Resource: 'resource', End: true }, + } + }); + + test.done(); + }, + 'Can merge state machines with shared states'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const sm = new stepfunctions.StateMachineDefinition(stack, 'SM'); - const task1 = new stepfunctions.Task(sm, 'Task1', { resource: new FakeResource() }); - const task2 = new stepfunctions.Task(sm, 'Task2', { resource: new FakeResource() }); - const failure = new stepfunctions.Fail(sm, 'Failed', { error: 'DidNotWork', cause: 'We got stuck' }); + const task1 = new stepfunctions.Task(stack, 'Task1', { resource: new FakeResource() }); + const task2 = new stepfunctions.Task(stack, 'Task2', { resource: new FakeResource() }); + const failure = new stepfunctions.Fail(stack, 'Failed', { error: 'DidNotWork', cause: 'We got stuck' }); // WHEN - task1.catch(failure); - task2.catch(failure); + task1.onError(failure); + task2.onError(failure); - task1.then(task2); + task1.next(task2); // THEN - test.deepEqual(cdk.resolve(sm.renderStateMachine()), { + test.deepEqual(render(task1), { StartAt: 'Task1', States: { Task1: { Type: 'Task', + Resource: 'resource', Next: 'Task2', Catch: [ { ErrorEquals: ['States.ALL'], Next: 'Failed' }, @@ -587,6 +542,7 @@ export = { }, Task2: { Type: 'Task', + Resource: 'resource', End: true, Catch: [ { ErrorEquals: ['States.ALL'], Next: 'Failed' }, @@ -605,7 +561,7 @@ export = { } }; -class ReusableStateMachine extends stepfunctions.StateMachineDefinition { +class ReusableStateMachine extends stepfunctions.StateMachineFragment { constructor(parent: cdk.Construct, id: string) { super(parent, id); @@ -623,3 +579,7 @@ class FakeResource implements stepfunctions.IStepFunctionsTaskResource { }; } } + +function render(sm: IChainable) { + return cdk.resolve(sm.toStateChain().renderStateMachine().stateMachineDefinition); +} \ No newline at end of file From c4e849fe083124c8289277f6ab43ae4d1fb5ace3 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 3 Sep 2018 17:50:37 +0200 Subject: [PATCH 11/29] Add TODO --- packages/@aws-cdk/aws-stepfunctions/TODO.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 packages/@aws-cdk/aws-stepfunctions/TODO.txt diff --git a/packages/@aws-cdk/aws-stepfunctions/TODO.txt b/packages/@aws-cdk/aws-stepfunctions/TODO.txt new file mode 100644 index 0000000000000..02714836dafa6 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/TODO.txt @@ -0,0 +1,3 @@ +- Metrics +- Integ tests +- Readme From 0c32d6d192e34785862772702d99437101a5cc29 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 3 Sep 2018 18:19:12 +0200 Subject: [PATCH 12/29] Some more tests --- .../aws-stepfunctions/lib/activity.ts | 8 +++--- .../aws-stepfunctions/lib/states/state.ts | 14 ---------- .../test/test.states-language.ts | 28 +++++++++++++++++++ 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts b/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts index c95bc1a2743ac..506c1f1239413 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts @@ -6,9 +6,9 @@ export interface ActivityProps { /** * The name for this activity. * - * The name is required. + * @default If not supplied, a name is generated */ - activityName: string; + activityName?: string; } /** @@ -18,11 +18,11 @@ export class Activity extends cdk.Construct implements IStepFunctionsTaskResourc public readonly activityArn: ActivityArn; public readonly activityName: ActivityName; - constructor(parent: cdk.Construct, id: string, props: ActivityProps) { + constructor(parent: cdk.Construct, id: string, props: ActivityProps = {}) { super(parent, id); const resource = new cloudformation.ActivityResource(this, 'Resource', { - activityName: props.activityName + activityName: props.activityName || this.uniqueId }); this.activityArn = resource.ref; diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts index 798067c042def..e98ede893d395 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts @@ -12,20 +12,6 @@ export abstract class State extends cdk.Construct implements IChainable { public abstract toStateChain(): IStateChain; - /** - * Find the top-level StateMachine we're part of - */ - protected containingStateMachineFragments(): StateMachineFragment { - let curr: cdk.Construct | undefined = this; - while (curr && !isStateMachineFragment(curr)) { - curr = curr.parent; - } - if (!curr) { - throw new Error('Could not find encompassing StateMachine'); - } - return curr; - } - protected renderBaseState(): any { return this.options; } diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts index 9c40fc32fb862..846fc8d0fcfc7 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts @@ -115,6 +115,24 @@ export = { test.done(); }, + 'Start state in a StateMachineFragment can be implicit'(test: Test) { + const stack = new cdk.Stack(); + + const sm = new ReusableStateMachineWithImplicitStartState(stack, 'Reusable'); + test.equals(sm.toStateChain().startState.stateId, 'Reusable/Choice'); + + test.done(); + }, + + 'Can skip adding names in StateMachineFragment'(test: Test) { + const stack = new cdk.Stack(); + + const sm = new ReusableStateMachineWithImplicitStartState(stack, 'Reusable', { scopeStateNames: false }); + test.equals(sm.toStateChain().startState.stateId, 'Choice'); + + test.done(); + }, + 'A state machine definition can be instantiated and chained'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -572,6 +590,16 @@ class ReusableStateMachine extends stepfunctions.StateMachineFragment { } } +class ReusableStateMachineWithImplicitStartState extends stepfunctions.StateMachineFragment { + constructor(parent: cdk.Construct, id: string, props: stepfunctions.StateMachineFragmentProps = {}) { + super(parent, id, props); + + const choice = new stepfunctions.Choice(this, 'Choice'); + choice.on(stepfunctions.Condition.stringEquals('$.branch', 'left'), new stepfunctions.Pass(this, 'Left Branch')); + choice.on(stepfunctions.Condition.stringEquals('$.branch', 'right'), new stepfunctions.Pass(this, 'Right Branch')); + } +} + class FakeResource implements stepfunctions.IStepFunctionsTaskResource { public asStepFunctionsTaskResource(_callingTask: stepfunctions.Task): stepfunctions.StepFunctionsTaskResourceProps { return { From 2b402f960a2efc2aa2a6e621e61eaec6162f03cb Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 3 Sep 2018 21:41:58 +0200 Subject: [PATCH 13/29] Add defaultRetry, get rid of internal ugliness in public API ALSO - Add Activity test --- .../aws-stepfunctions/DESIGN_NOTES.md | 7 +- .../aws-stepfunctions/lib/asl-external-api.ts | 35 ++++++++- .../aws-stepfunctions/lib/asl-internal-api.ts | 19 +++-- .../aws-stepfunctions/lib/asl-state-chain.ts | 34 +++++---- .../aws-stepfunctions/lib/states/choice.ts | 23 ++++-- .../aws-stepfunctions/lib/states/fail.ts | 12 ++- .../aws-stepfunctions/lib/states/parallel.ts | 16 ++-- .../aws-stepfunctions/lib/states/pass.ts | 12 ++- .../aws-stepfunctions/lib/states/state.ts | 8 +- .../aws-stepfunctions/lib/states/succeed.ts | 12 ++- .../aws-stepfunctions/lib/states/task.ts | 13 +++- .../aws-stepfunctions/lib/states/wait.ts | 12 ++- .../aws-stepfunctions/test/test.activity.ts | 48 ++++++++++++ .../test/test.states-language.ts | 76 ++++++++++++++++++- 14 files changed, 263 insertions(+), 64 deletions(-) create mode 100644 packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts diff --git a/packages/@aws-cdk/aws-stepfunctions/DESIGN_NOTES.md b/packages/@aws-cdk/aws-stepfunctions/DESIGN_NOTES.md index 815146dfa2027..a32f4fd31d97f 100644 --- a/packages/@aws-cdk/aws-stepfunctions/DESIGN_NOTES.md +++ b/packages/@aws-cdk/aws-stepfunctions/DESIGN_NOTES.md @@ -208,4 +208,9 @@ Notes: use a complicated object model to hide ugly parts of the API from users Decouple state chainer from states. -Decouple rendering from states to allow elidable states. \ No newline at end of file +Decouple rendering from states to allow elidable states. + +'then' is a reserved word in Ruby +'catch' is a reserved word in many languages + +https://halyph.com/blog/2016/11/28/prog-lang-reserved-words.html \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl-external-api.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl-external-api.ts index 340af14fe9a20..2eddeb55e64f6 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/asl-external-api.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/asl-external-api.ts @@ -1,18 +1,45 @@ import cdk = require('@aws-cdk/cdk'); -import { IInternalState } from "./asl-internal-api"; export interface IChainable { + /** + * Return a chain representing this state object. + */ toStateChain(): IStateChain; } export interface IStateChain extends IChainable { - readonly startState: IInternalState; - + /** + * Add a state or chain onto the existing chain. + * + * Returns a new chain with the state/chain added. + */ next(state: IChainable): IStateChain; - onError(targetState: IChainable, ...errors: string[]): IStateChain; + /** + * Add a Catch handlers to the states in the chain + * + * If the chain does not consist completely of states that + * can have error handlers applied, it is wrapped in a Parallel + * block first. + */ + onError(errorHandler: IChainable, ...errors: string[]): IStateChain; + + /** + * Add retries to all states in the chains which can have retries applied + */ + defaultRetry(retry?: RetryProps): void; + + /** + * Return a chain with all states reachable from the current chain. + * + * This includes the states reachable via error handler (Catch) + * transitions. + */ closure(): IStateChain; + /** + * Apply the closure, then render the state machine. + */ renderStateMachine(): RenderedStateMachine; } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl-internal-api.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl-internal-api.ts index ca64d134a45c4..81ebc9385fc60 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/asl-internal-api.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/asl-internal-api.ts @@ -1,4 +1,6 @@ import cdk = require('@aws-cdk/cdk'); +import { IStateChain, RetryProps } from './asl-external-api'; +import { accessChainInternals } from './asl-state-chain'; export interface IInternalState { readonly stateId: string; @@ -6,10 +8,11 @@ export interface IInternalState { readonly hasOpenNextTransition: boolean; readonly policyStatements: cdk.PolicyStatement[]; - addNext(targetState: IInternalState): void; - addCatch(targetState: IInternalState, errors: string[]): void; + addNext(target: IStateChain): void; + addCatch(target: IStateChain, errors: string[]): void; + addRetry(retry?: RetryProps): void; - accessibleStates(): IInternalState[]; + accessibleChains(): IStateChain[]; renderState(): any; } @@ -25,7 +28,7 @@ export enum StateType { export interface Transition { transitionType: TransitionType; - targetState: IInternalState; + targetChain: IStateChain; annotation?: any; } @@ -39,8 +42,8 @@ export enum TransitionType { export class Transitions { private readonly transitions = new Array(); - public add(transitionType: TransitionType, targetState: IInternalState, annotation?: any) { - this.transitions.push({ transitionType, targetState, annotation }); + public add(transitionType: TransitionType, targetChain: IStateChain, annotation?: any) { + this.transitions.push({ transitionType, targetChain, annotation }); } public has(type: TransitionType): boolean { @@ -61,7 +64,7 @@ export class Transitions { return otherwise; } return { - [type]: transitions[0].targetState.stateId + [type]: accessChainInternals(transitions[0].targetChain).startState.stateId }; } @@ -74,7 +77,7 @@ export class Transitions { return { [type]: transitions.map(t => ({ ...t.annotation, - Next: t.targetState.stateId, + Next: accessChainInternals(t.targetChain).startState.stateId, })) }; } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl-state-chain.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl-state-chain.ts index 9cc34b5ce6bce..988a90b9923d7 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/asl-state-chain.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/asl-state-chain.ts @@ -1,5 +1,5 @@ import cdk = require('@aws-cdk/cdk'); -import { Errors, IChainable, IStateChain, RenderedStateMachine } from './asl-external-api'; +import { Errors, IChainable, IStateChain, RenderedStateMachine, RetryProps } from './asl-external-api'; import { IInternalState } from './asl-internal-api'; export class StateChain implements IStateChain { @@ -32,11 +32,11 @@ export class StateChain implements IStateChain { } for (const endState of this.activeStates) { - endState.addNext(sm.startState); + endState.addNext(sm); } ret.absorb(sm); - ret.activeStates = new Set(accessMachineInternals(sm).activeStates); + ret.activeStates = new Set(accessChainInternals(sm).activeStates); return ret; } @@ -61,7 +61,7 @@ export class StateChain implements IStateChain { const ret = this.clone(); for (const state of this.allStates) { - state.addCatch(sm.startState, errors); + state.addCatch(sm, errors); } // Those states are now part of the state machine, but we don't include @@ -79,12 +79,14 @@ export class StateChain implements IStateChain { public closure(): IStateChain { const ret = new StateChain(this.startState); - const queue = this.startState.accessibleStates(); + const queue = this.startState.accessibleChains(); while (queue.length > 0) { - const state = queue.splice(0, 1)[0]; - if (!ret.allStates.has(state)) { - ret.allStates.add(state); - queue.push(...state.accessibleStates()); + const chain = queue.splice(0, 1)[0]; + for (const state of accessChainInternals(chain).allStates) { + if (!ret.allStates.has(state)) { + ret.allStates.add(state); + queue.push(...state.accessibleChains()); + } } } @@ -100,7 +102,7 @@ export class StateChain implements IStateChain { const policies = new Array(); const states: any = {}; - for (const state of accessMachineInternals(closed).allStates) { + for (const state of accessChainInternals(closed).allStates) { states[state.stateId] = state.renderState(); policies.push(...state.policyStatements); } @@ -114,8 +116,14 @@ export class StateChain implements IStateChain { }; } - private absorb(other: IStateChain) { - const sdm = accessMachineInternals(other); + public defaultRetry(retry: RetryProps = {}): void { + for (const state of this.allStates) { + state.addRetry(retry); + } + } + + public absorb(other: IStateChain) { + const sdm = accessChainInternals(other); for (const state of sdm.allStates) { this.allStates.add(state); } @@ -136,6 +144,6 @@ export class StateChain implements IStateChain { * only distract, but parts that other states need to achieve their * work. */ -export function accessMachineInternals(x: IStateChain): StateChain { +export function accessChainInternals(x: IStateChain): StateChain { return x as StateChain; } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts index 4f32efbd95c8f..19df69d3591b0 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts @@ -1,6 +1,6 @@ import cdk = require('@aws-cdk/cdk'); import { Condition } from '../asl-condition'; -import { IChainable, IStateChain } from '../asl-external-api'; +import { IChainable, IStateChain, RetryProps } from '../asl-external-api'; import { IInternalState, StateType, TransitionType } from '../asl-internal-api'; import { StateChain } from '../asl-state-chain'; import { State } from './state'; @@ -29,17 +29,21 @@ export class Choice extends State { }; } - public addNext(_targetState: IInternalState): void { + public addNext(_targetState: IStateChain): void { throw new Error("Cannot chain onto a Choice state. Use the state's .on() or .otherwise() instead."); } - public addCatch(_targetState: IInternalState, _errors: string[]): void { + public addCatch(_targetState: IStateChain, _errors: string[]): void { throw new Error("Cannot catch errors on a Choice."); } - public accessibleStates() { + public accessibleChains() { return this.choice.accessibleStates(); } + + public addRetry(_retry?: RetryProps): void { + // Nothing + } }; constructor(parent: cdk.Construct, id: string, props: ChoiceProps = {}) { @@ -51,7 +55,7 @@ export class Choice extends State { } public on(condition: Condition, next: IChainable): Choice { - this.transitions.add(TransitionType.Choice, next.toStateChain().startState, condition.renderCondition()); + this.transitions.add(TransitionType.Choice, next.toStateChain(), condition.renderCondition()); return this; } @@ -60,12 +64,17 @@ export class Choice extends State { if (this.transitions.has(TransitionType.Default)) { throw new Error('Can only have one Default transition'); } - this.transitions.add(TransitionType.Default, next.toStateChain().startState); + this.transitions.add(TransitionType.Default, next.toStateChain()); return this; } public toStateChain(): IStateChain { - return new StateChain(new Choice.Internals(this)); + const chain = new StateChain(new Choice.Internals(this)); + for (const transition of this.transitions.all()) { + chain.absorb(transition.targetChain); + } + + return chain; } public closure(): IStateChain { diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/fail.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/fail.ts index c1070d59b67c8..1ca2cc602b701 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/fail.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/fail.ts @@ -1,5 +1,5 @@ import cdk = require('@aws-cdk/cdk'); -import { IStateChain } from '../asl-external-api'; +import { IStateChain, RetryProps } from '../asl-external-api'; import { IInternalState, StateType } from '../asl-internal-api'; import { StateChain } from '../asl-state-chain'; import { State } from './state'; @@ -24,17 +24,21 @@ export class Fail extends State { return this.fail.renderBaseState(); } - public addNext(_targetState: IInternalState): void { + public addNext(_targetState: IStateChain): void { throw new Error("Cannot chain onto a Fail state. This ends the state machine."); } - public addCatch(_targetState: IInternalState, _errors: string[]): void { + public addCatch(_targetState: IStateChain, _errors: string[]): void { throw new Error("Cannot catch errors on a Fail."); } - public accessibleStates() { + public accessibleChains() { return this.fail.accessibleStates(); } + + public addRetry(_retry?: RetryProps): void { + // Nothing + } }; constructor(parent: cdk.Construct, id: string, props: FailProps) { diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts index 32aff6c400b12..a595879f05a1f 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts @@ -29,15 +29,19 @@ export class Parallel extends State { }; } - public addNext(targetState: IInternalState): void { + public addNext(targetState: IStateChain): void { this.parallel.addNextTransition(targetState); } - public addCatch(targetState: IInternalState, errors: string[]): void { + public addCatch(targetState: IStateChain, errors: string[]): void { this.parallel.transitions.add(TransitionType.Catch, targetState, { ErrorEquals: errors }); } - public accessibleStates() { + public addRetry(retry?: RetryProps): void { + this.parallel.retry(retry); + } + + public accessibleChains() { return this.parallel.accessibleStates(); } @@ -69,15 +73,17 @@ export class Parallel extends State { }); } - public branch(definition: IChainable) { + public branch(definition: IChainable): Parallel { this.branches.push(definition); + return this; } - public retry(props: RetryProps = {}) { + public retry(props: RetryProps = {}): Parallel { if (!props.errors) { props.errors = [Errors.all]; } this.retries.push(props); + return this; } public next(sm: IChainable): IStateChain { diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts index 678831ccb679b..ce729fa9a1b25 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts @@ -1,5 +1,5 @@ import cdk = require('@aws-cdk/cdk'); -import { IChainable, IStateChain } from '../asl-external-api'; +import { IChainable, IStateChain, RetryProps } from '../asl-external-api'; import { IInternalState, StateType, TransitionType } from '../asl-internal-api'; import { StateChain } from '../asl-state-chain'; import { State } from './state'; @@ -26,15 +26,19 @@ export class Pass extends State { }; } - public addNext(targetState: IInternalState): void { + public addNext(targetState: IStateChain): void { this.pass.addNextTransition(targetState); } - public addCatch(_targetState: IInternalState, _errors: string[]): void { + public addCatch(_targetState: IStateChain, _errors: string[]): void { throw new Error("Cannot catch errors on a Pass."); } - public accessibleStates() { + public addRetry(_retry?: RetryProps): void { + // Nothing + } + + public accessibleChains() { return this.pass.accessibleStates(); } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts index e98ede893d395..3b751299d96ae 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts @@ -1,6 +1,6 @@ import cdk = require('@aws-cdk/cdk'); import { IChainable, IStateChain } from "../asl-external-api"; -import { IInternalState, Transitions, TransitionType } from '../asl-internal-api'; +import { Transitions, TransitionType } from '../asl-internal-api'; import { StateMachineFragment } from './state-machine-fragment'; export abstract class State extends cdk.Construct implements IChainable { @@ -24,15 +24,15 @@ export abstract class State extends cdk.Construct implements IChainable { return parentDefs.map(x => x.id).join('/'); } - protected accessibleStates(): IInternalState[] { - return this.transitions.all().map(t => t.targetState); + protected accessibleStates(): IStateChain[] { + return this.transitions.all().map(t => t.targetChain); } protected get hasNextTransition() { return this.transitions.has(TransitionType.Next); } - protected addNextTransition(targetState: IInternalState): void { + protected addNextTransition(targetState: IStateChain): void { if (this.hasNextTransition) { throw new Error(`State ${this.stateId} already has a Next transition`); } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts index 03bde37727108..cdac5711d463a 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts @@ -1,5 +1,5 @@ import cdk = require('@aws-cdk/cdk'); -import { IStateChain } from '../asl-external-api'; +import { IStateChain, RetryProps } from '../asl-external-api'; import { IInternalState, StateType } from '../asl-internal-api'; import { StateChain } from '../asl-state-chain'; import { State } from './state'; @@ -19,15 +19,19 @@ export class Succeed extends State { return this.succeed.renderBaseState(); } - public addNext(_targetState: IInternalState): void { + public addNext(_targetState: IStateChain): void { throw new Error("Cannot chain onto a Succeed state; this ends the state machine."); } - public addCatch(_targetState: IInternalState, _errors: string[]): void { + public addCatch(_targetState: IStateChain, _errors: string[]): void { throw new Error("Cannot catch errors on a Succeed."); } - public accessibleStates() { + public addRetry(_retry?: RetryProps): void { + // Nothing + } + + public accessibleChains() { return this.succeed.accessibleStates(); } }; diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts index b6b1055149e78..2f909962e5336 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts @@ -49,15 +49,19 @@ export class Task extends State { }; } - public addNext(targetState: IInternalState): void { + public addNext(targetState: IStateChain): void { this.task.addNextTransition(targetState); } - public addCatch(targetState: IInternalState, errors: string[]): void { + public addCatch(targetState: IStateChain, errors: string[]): void { this.task.transitions.add(TransitionType.Catch, targetState, { ErrorEquals: errors }); } - public accessibleStates() { + public addRetry(retry?: RetryProps): void { + this.task.retry(retry); + } + + public accessibleChains() { return this.task.accessibleStates(); } @@ -90,11 +94,12 @@ export class Task extends State { return this.toStateChain().onError(handler, ...errors); } - public retry(props: RetryProps = {}) { + public retry(props: RetryProps = {}): Task { if (!props.errors) { props.errors = [Errors.all]; } this.retries.push(props); + return this; } public toStateChain(): IStateChain { diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/wait.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/wait.ts index 5a5c234721686..4229effa31c7c 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/wait.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/wait.ts @@ -1,5 +1,5 @@ import cdk = require('@aws-cdk/cdk'); -import { IChainable, IStateChain } from '../asl-external-api'; +import { IChainable, IStateChain, RetryProps } from '../asl-external-api'; import { IInternalState, StateType, TransitionType } from '../asl-internal-api'; import { StateChain } from '../asl-state-chain'; import { State } from './state'; @@ -29,15 +29,19 @@ export class Wait extends State { }; } - public addNext(targetState: IInternalState): void { + public addNext(targetState: IStateChain): void { this.wait.addNextTransition(targetState); } - public addCatch(_targetState: IInternalState, _errors: string[]): void { + public addCatch(_targetState: IStateChain, _errors: string[]): void { throw new Error("Cannot catch errors on a Wait."); } - public accessibleStates() { + public addRetry(_retry?: RetryProps): void { + // Nothing + } + + public accessibleChains() { return this.wait.accessibleStates(); } diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts new file mode 100644 index 0000000000000..a63249a151a50 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts @@ -0,0 +1,48 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import stepfunctions = require('../lib'); + +export = { + 'instantiate Activity'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new stepfunctions.Activity(stack, 'Activity'); + + // THEN + expect(stack).to(haveResource('AWS::StepFunctions::Activity', { + Name: 'Activity' + })); + + test.done(); + }, + + 'Activity can be used in a Task'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const activity = new stepfunctions.Activity(stack, 'Activity'); + const task = new stepfunctions.Task(stack, 'Task', { + resource: activity + }); + new stepfunctions.StateMachine(stack, 'SM', { + definition: task + }); + + // THEN + expect(stack).to(haveResource('AWS::StepFunctions::StateMachine', { + DefinitionString: { + "Fn::Join": ["", [ + "{\"StartAt\":\"Task\",\"States\":{\"Task\":{\"Type\":\"Task\",\"Resource\":\"", + { Ref: "Activity04690B0A" }, + "\",\"End\":true}}}" + ]] + }, + })); + + test.done(); + }, +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts index 846fc8d0fcfc7..9e15e36ed9b64 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts @@ -116,19 +116,27 @@ export = { }, 'Start state in a StateMachineFragment can be implicit'(test: Test) { + // GIVEN const stack = new cdk.Stack(); + // WHEN const sm = new ReusableStateMachineWithImplicitStartState(stack, 'Reusable'); - test.equals(sm.toStateChain().startState.stateId, 'Reusable/Choice'); + + // THEN + test.equals(render(sm).StartAt, 'Reusable/Choice'); test.done(); }, 'Can skip adding names in StateMachineFragment'(test: Test) { + // GIVEN const stack = new cdk.Stack(); + // WHEN const sm = new ReusableStateMachineWithImplicitStartState(stack, 'Reusable', { scopeStateNames: false }); - test.equals(sm.toStateChain().startState.stateId, 'Choice'); + + // THEN + test.equals(render(sm).StartAt, 'Choice'); test.done(); }, @@ -419,6 +427,70 @@ export = { test.done(); }, + 'Add default retries on all tasks in the chain, but not those outside'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const task1 = new stepfunctions.Task(stack, 'Task1', { resource: new FakeResource() }); + const task2 = new stepfunctions.Task(stack, 'Task2', { resource: new FakeResource() }); + const task3 = new stepfunctions.Task(stack, 'Task3', { resource: new FakeResource() }); + const task4 = new stepfunctions.Task(stack, 'Task4', { resource: new FakeResource() }); + const task5 = new stepfunctions.Task(stack, 'Task5', { resource: new FakeResource() }); + const choice = new stepfunctions.Choice(stack, 'Choice'); + const errorHandler1 = new stepfunctions.Task(stack, 'ErrorHandler1', { resource: new FakeResource() }); + const errorHandler2 = new stepfunctions.Task(stack, 'ErrorHandler2', { resource: new FakeResource() }); + const para = new stepfunctions.Parallel(stack, 'Para'); + + // WHEN + task1.next(task2); + para.onError(errorHandler2); + + task2.onError(errorHandler1) + .next(choice + .on(stepfunctions.Condition.stringEquals('$.var', 'value'), + task3.next(task4)) + .otherwise(para + .branch(task5))) + .defaultRetry(); + + // THEN + const theCatch1 = { Catch: [ { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler1' } ] }; + const theCatch2 = { Catch: [ { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler2' } ] }; + const theRetry = { Retry: [ { ErrorEquals: ['States.ALL'] } ] }; + + test.deepEqual(render(task1), { + StartAt: 'Task1', + States: { + Task1: { Next: 'Task2', Type: 'Task', Resource: 'resource' }, + Task2: { Next: 'Choice', Type: 'Task', Resource: 'resource', ...theCatch1, ...theRetry }, + ErrorHandler1: { End: true, Type: 'Task', Resource: 'resource', ...theRetry }, + Choice: { + Type: 'Choice', + Choices: [ { Variable: '$.var', StringEquals: 'value', Next: 'Task3' } ], + Default: 'Para', + }, + Task3: { Next: 'Task4', Type: 'Task', Resource: 'resource', ...theRetry }, + Task4: { End: true, Type: 'Task', Resource: 'resource', ...theRetry }, + Para: { + Type: 'Parallel', + End: true, + Branches: [ + { + StartAt: 'Task5', + States: { + Task5: { End: true, Type: 'Task', Resource: 'resource' } + } + } + ], + ...theCatch2, + ...theRetry + }, + ErrorHandler2: { End: true, Type: 'Task', Resource: 'resource' }, + } + }); + + test.done(); + }, + /* ** FIXME: Not implemented at the moment, since we need to make a Construct for this and the From 9a805541775d58b29e6fd9356120d601c110a8aa Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 3 Sep 2018 22:03:33 +0200 Subject: [PATCH 14/29] Add integration test --- .../aws-stepfunctions/lib/asl-external-api.ts | 2 +- .../aws-stepfunctions/lib/asl-state-chain.ts | 3 +- .../aws-stepfunctions/lib/state-machine.ts | 6 ++ .../lib/states/state-machine-fragment.ts | 7 -- .../@aws-cdk/aws-stepfunctions/package.json | 1 + .../test/integ.job-poller.expected.json | 75 +++++++++++++++++++ .../test/integ.job-poller.ts | 51 +++++++++++++ 7 files changed, 136 insertions(+), 9 deletions(-) create mode 100644 packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.expected.json create mode 100644 packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.ts diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl-external-api.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl-external-api.ts index 2eddeb55e64f6..37b7cca20101a 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/asl-external-api.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/asl-external-api.ts @@ -27,7 +27,7 @@ export interface IStateChain extends IChainable { /** * Add retries to all states in the chains which can have retries applied */ - defaultRetry(retry?: RetryProps): void; + defaultRetry(retry?: RetryProps): IStateChain; /** * Return a chain with all states reachable from the current chain. diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl-state-chain.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl-state-chain.ts index 988a90b9923d7..78957e112899f 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/asl-state-chain.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/asl-state-chain.ts @@ -116,10 +116,11 @@ export class StateChain implements IStateChain { }; } - public defaultRetry(retry: RetryProps = {}): void { + public defaultRetry(retry: RetryProps = {}): IStateChain { for (const state of this.allStates) { state.addRetry(retry); } + return this; } public absorb(other: IStateChain) { diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts index 49065ebc37adc..5d5adbef84551 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts @@ -23,6 +23,9 @@ export interface StateMachineProps { * @default A role is automatically created */ role?: iam.Role; + + timeoutSeconds?: number; + } /** @@ -44,6 +47,9 @@ export class StateMachine extends cdk.Construct { }); const rendered = props.definition.toStateChain().renderStateMachine(); + if (props.timeoutSeconds !== undefined) { + rendered.stateMachineDefinition.TimeoutSeconds = props.timeoutSeconds; + } const resource = new cloudformation.StateMachineResource(this, 'Resource', { stateMachineName: props.stateMachineName, diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/state-machine-fragment.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/state-machine-fragment.ts index 2428349fae294..2251ba9ccf345 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/state-machine-fragment.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/state-machine-fragment.ts @@ -2,8 +2,6 @@ import cdk = require('@aws-cdk/cdk'); import { IChainable, IStateChain } from "../asl-external-api"; export interface StateMachineFragmentProps { - timeoutSeconds?: number; - /** * Whether to add the fragment name to the states defined within * @@ -20,12 +18,10 @@ export class StateMachineFragment extends cdk.Construct implements IChainable { public readonly scopeStateNames: boolean; - private readonly timeoutSeconds?: number; private _startState?: IChainable; constructor(parent: cdk.Construct, id: string, props: StateMachineFragmentProps = {}) { super(parent, id); - this.timeoutSeconds = props.timeoutSeconds; this.scopeStateNames = props.scopeStateNames !== undefined ? props.scopeStateNames : true; } @@ -38,9 +34,6 @@ export class StateMachineFragment extends cdk.Construct implements IChainable { } public toStateChain(): IStateChain { - // FIXME: Use somewhere - Array.isArray(this.timeoutSeconds); - // If we're converting a state machine definition to a state chain, grab the whole of it. return this.startState.toStateChain().closure(); } diff --git a/packages/@aws-cdk/aws-stepfunctions/package.json b/packages/@aws-cdk/aws-stepfunctions/package.json index 711cadeabb374..4ca68d61eea30 100644 --- a/packages/@aws-cdk/aws-stepfunctions/package.json +++ b/packages/@aws-cdk/aws-stepfunctions/package.json @@ -48,6 +48,7 @@ "devDependencies": { "@aws-cdk/assert": "^0.8.2", "cdk-build-tools": "^0.8.2", + "cdk-integ-tools": "^0.8.2", "cfn2ts": "^0.8.2", "pkglint": "^0.8.2" }, diff --git a/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.expected.json b/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.expected.json new file mode 100644 index 0000000000000..518e5ebee86b8 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.expected.json @@ -0,0 +1,75 @@ +{ + "Resources": { + "SubmitJobFB773A16": { + "Type": "AWS::StepFunctions::Activity", + "Properties": { + "Name": "awsstepfunctionsintegSubmitJobA2508960" + } + }, + "CheckJob5FFC1D6F": { + "Type": "AWS::StepFunctions::Activity", + "Properties": { + "Name": "awsstepfunctionsintegCheckJobC4AC762D" + } + }, + "StateMachineRoleB840431D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Submit Job\",\"States\":{\"Submit Job\":{\"Type\":\"Task\",\"Resource\":\"", + { + "Ref": "SubmitJobFB773A16" + }, + "\",\"ResultPath\":\"$.guid\",\"Retry\":[{\"ErrorEquals\":[\"States.ALL\"],\"IntervalSeconds\":1,\"MaxAttempts\":3,\"BackoffRate\":2}],\"Next\":\"Wait X Seconds\"},\"Wait X Seconds\":{\"Type\":\"Wait\",\"SecondsPath\":\"$.wait_time\",\"Next\":\"Get Job Status\"},\"Get Job Status\":{\"Type\":\"Task\",\"InputPath\":\"$.guid\",\"Resource\":\"", + { + "Ref": "CheckJob5FFC1D6F" + }, + "\",\"ResultPath\":\"$.status\",\"Retry\":[{\"ErrorEquals\":[\"States.ALL\"],\"IntervalSeconds\":1,\"MaxAttempts\":3,\"BackoffRate\":2}],\"Next\":\"Job Complete?\"},\"Job Complete?\":{\"Type\":\"Choice\",\"Choices\":[{\"Variable\":\"$.status\",\"StringEquals\":\"FAILED\",\"Next\":\"Job Failed\"},{\"Variable\":\"$.status\",\"StringEquals\":\"SUCCEEDED\",\"Next\":\"Get Final Job Status\"}]},\"Job Failed\":{\"Type\":\"Fail\",\"Error\":\"DescribeJob returned FAILED\",\"Cause\":\"AWS Batch Job Failed\"},\"Get Final Job Status\":{\"Type\":\"Task\",\"InputPath\":\"$.guid\",\"Resource\":\"", + { + "Ref": "CheckJob5FFC1D6F" + }, + "\",\"Retry\":[{\"ErrorEquals\":[\"States.ALL\"],\"IntervalSeconds\":1,\"MaxAttempts\":3,\"BackoffRate\":2}],\"End\":true}}}" + ] + ] + }, + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.ts b/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.ts new file mode 100644 index 0000000000000..e9d881489fc24 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.ts @@ -0,0 +1,51 @@ +import cdk = require( '@aws-cdk/cdk'); +import stepfunctions = require('../lib'); + +class JobPollerStack extends cdk.Stack { + constructor(parent: cdk.App, id: string, props: cdk.StackProps = {}) { + super(parent, id, props); + + const submitJobActivity = new stepfunctions.Activity(this, 'SubmitJob'); + const checkJobActivity = new stepfunctions.Activity(this, 'CheckJob'); + + const submitJob = new stepfunctions.Task(this, 'Submit Job', { + resource: submitJobActivity, + resultPath: '$.guid', + }); + const waitX = new stepfunctions.Wait(this, 'Wait X Seconds', { secondsPath: '$.wait_time' }); + const getStatus = new stepfunctions.Task(this, 'Get Job Status', { + resource: checkJobActivity, + inputPath: '$.guid', + resultPath: '$.status', + }); + const isComplete = new stepfunctions.Choice(this, 'Job Complete?'); + const jobFailed = new stepfunctions.Fail(this, 'Job Failed', { + cause: 'AWS Batch Job Failed', + error: 'DescribeJob returned FAILED', + }); + const finalStatus = new stepfunctions.Task(this, 'Get Final Job Status', { + resource: checkJobActivity, + inputPath: '$.guid', + }); + + const chain = submitJob + .next(waitX) + .next(getStatus) + .next(isComplete + .on(stepfunctions.Condition.stringEquals('$.status', 'FAILED'), jobFailed) + .on(stepfunctions.Condition.stringEquals('$.status', 'SUCCEEDED'), finalStatus)) + .defaultRetry({ + intervalSeconds: 1, + maxAttempts: 3, + backoffRate: 2 + }); + + new stepfunctions.StateMachine(this, 'StateMachine', { + definition: chain + }); + } +} + +const app = new cdk.App(process.argv); +new JobPollerStack(app, 'aws-stepfunctions-integ'); +process.stdout.write(app.run()); \ No newline at end of file From 766d0c8c4f8e1a0e683b4843ffcc9da2f6ec9f2d Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 4 Sep 2018 12:10:22 +0200 Subject: [PATCH 15/29] Add metrics --- .../@aws-cdk/aws-lambda/lib/lambda-ref.ts | 15 +- .../aws-stepfunctions/lib/activity.ts | 102 ++++++++++++- .../@aws-cdk/aws-stepfunctions/lib/index.ts | 1 + .../aws-stepfunctions/lib/state-machine.ts | 69 +++++++++ .../lib/state-transition-metrics.ts | 58 ++++++++ .../aws-stepfunctions/lib/states/task.ts | 137 ++++++++++++++++-- .../test/integ.job-poller.expected.json | 4 +- .../test/integ.job-poller.ts | 6 +- .../test/test.state-machine-resources.ts | 33 ++++- .../test/test.states-language.ts | 2 +- 10 files changed, 399 insertions(+), 28 deletions(-) create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/state-transition-metrics.ts diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts index 467ff04927877..cd4c2a37395b8 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda-ref.ts @@ -191,13 +191,16 @@ export abstract class FunctionRef extends cdk.Construct }; } - public asStepFunctionsTaskResource(callingTask: stepfunctions.Task): stepfunctions.StepFunctionsTaskResourceProps { - callingTask.addToRolePolicy(new cdk.PolicyStatement() - .addResource(this.functionArn) - .addActions("lambda:InvokeFunction")); - + public asStepFunctionsTaskResource(_callingTask: stepfunctions.Task): stepfunctions.StepFunctionsTaskResourceProps { return { - resourceArn: this.functionArn + resourceArn: this.functionArn, + metricPrefixSingular: 'LambdaFunction', + metricPrefixPlural: 'LambdaFunctions', + metricDimensions: { LambdaFunctionArn: this.functionArn }, + policyStatements: [new cdk.PolicyStatement() + .addResource(this.functionArn) + .addActions("lambda:InvokeFunction") + ] }; } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts b/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts index 506c1f1239413..3edd7b990da1f 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts @@ -1,3 +1,4 @@ +import cloudwatch = require('@aws-cdk/aws-cloudwatch'); import cdk = require('@aws-cdk/cdk'); import { IStepFunctionsTaskResource, StepFunctionsTaskResourceProps, Task } from './states/task'; import { ActivityArn, ActivityName, cloudformation } from './stepfunctions.generated'; @@ -32,7 +33,106 @@ export class Activity extends cdk.Construct implements IStepFunctionsTaskResourc public asStepFunctionsTaskResource(_callingTask: Task): StepFunctionsTaskResourceProps { // No IAM permissions necessary, execution role implicitly has Activity permissions. return { - resourceArn: this.activityArn + resourceArn: this.activityArn, + metricPrefixSingular: 'Activity', + metricPrefixPlural: 'Activities', + metricDimensions: { ActivityArn: this.activityArn }, }; } + + /** + * Return the given named metric for this Activity + * + * @default sum over 5 minutes + */ + public metric(metricName: string, props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace: 'AWS/States', + metricName, + dimensions: { ActivityArn: this.activityArn }, + statistic: 'sum', + ...props + }); + } + + /** + * The interval, in milliseconds, between the time the activity starts and the time it closes. + * + * @default average over 5 minutes + */ + public metricRunTime(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.metric('ActivityRunTime', { statistic: 'avg', ...props }); + } + + /** + * The interval, in milliseconds, for which the activity stays in the schedule state. + * + * @default average over 5 minutes + */ + public metricScheduleTime(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.metric('ActivityScheduleTime', { statistic: 'avg', ...props }); + } + + /** + * The interval, in milliseconds, between the time the activity is scheduled and the time it closes. + * + * @default average over 5 minutes + */ + public metricTime(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.metric('ActivityTime', { statistic: 'avg', ...props }); + } + + /** + * Metric for the number of times this activity is scheduled + * + * @default sum over 5 minutes + */ + public metricScheduled(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.metric('ActivitiesScheduled', props); + } + + /** + * Metric for the number of times this activity times out + * + * @default sum over 5 minutes + */ + public metricTimedOut(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.metric('ActivitiesTimedOut', props); + } + + /** + * Metric for the number of times this activity is started + * + * @default sum over 5 minutes + */ + public metricStarted(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.metric('ActivitiesStarted', props); + } + + /** + * Metric for the number of times this activity succeeds + * + * @default sum over 5 minutes + */ + public metricSucceeded(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.metric('ActivitiesSucceeded', props); + } + + /** + * Metric for the number of times this activity fails + * + * @default sum over 5 minutes + */ + public metricFailed(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.metric('ActivitiesFailed', props); + } + + /** + * Metric for the number of times the heartbeat times out for this activity + * + * @default sum over 5 minutes + */ + public metricHeartbeatTimedOut(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.metric('ActivitiesHeartbeatTimedOut', props); + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions/lib/index.ts index 77d5dd8a7d047..f1f8301c6e751 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/index.ts @@ -3,6 +3,7 @@ export * from './asl-external-api'; export * from './asl-internal-api'; export * from './asl-condition'; export * from './state-machine'; +export * from './state-transition-metrics'; export * from './states/choice'; export * from './states/fail'; diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts index 5d5adbef84551..47ce2e28d6b4d 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts @@ -1,3 +1,4 @@ +import cloudwatch = require('@aws-cdk/aws-cloudwatch'); import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); @@ -90,4 +91,72 @@ export class StateMachine extends cdk.Construct { }; } + /** + * Return the given named metric for this State Machine's executions + * + * @default sum over 5 minutes + */ + public metric(metricName: string, props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace: 'AWS/States', + metricName, + dimensions: { StateMachineArn: this.stateMachineArn }, + statistic: 'sum', + ...props + }); + } + + /** + * Metric for the number of executions that failed + * + * @default sum over 5 minutes + */ + public metricFailed(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.metric('ExecutionsFailed', props); + } + + /** + * Metric for the number of executions that were throttled + * + * @default sum over 5 minutes + */ + public metricThrottled(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.metric('ExecutionThrottled', props); + } + + /** + * Metric for the number of executions that were aborted + * + * @default sum over 5 minutes + */ + public metricAborted(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.metric('ExecutionsAborted', props); + } + + /** + * Metric for the number of executions that succeeded + * + * @default sum over 5 minutes + */ + public metricSucceeded(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.metric('ExecutionsSucceeded', props); + } + + /** + * Metric for the number of executions that succeeded + * + * @default sum over 5 minutes + */ + public metricTimedOut(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.metric('ExecutionsTimedOut', props); + } + + /** + * Metric for the number of executions that were started + * + * @default sum over 5 minutes + */ + public metricStarted(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.metric('ExecutionsStarted', props); + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-transition-metrics.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-transition-metrics.ts new file mode 100644 index 0000000000000..4dd642c722c8f --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-transition-metrics.ts @@ -0,0 +1,58 @@ +import cloudwatch = require('@aws-cdk/aws-cloudwatch'); + +/** + * Metrics on the rate limiting performed on state machine execution. + * + * These rate limits are shared across all state machines. + */ +export class StateTransitionMetric { + /** + * Return the given named metric for the service's state transition metrics + * + * @default average over 5 minutes + */ + public static metric(metricName: string, props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace: 'AWS/States', + metricName, + dimensions: { ServiceMetric: 'StateTransition' }, + ...props + }); + } + + /** + * Metric for the number of available state transitions. + * + * @default average over 5 minutes + */ + public static provisionedBucketSize(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return StateTransitionMetric.metric("ProvisionedBucketSize", props); + } + + /** + * Metric for the provisioned steady-state execution rate + * + * @default average over 5 minutes + */ + public static provisionedRefillRate(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return StateTransitionMetric.metric("ProvisionedRefillRate", props); + } + + /** + * Metric for the number of available state transitions per second + * + * @default average over 5 minutes + */ + public static consumedCapacity(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return StateTransitionMetric.metric("ConsumedCapacity", props); + } + + /** + * Metric for the number of throttled state transitions + * + * @default sum over 5 minutes + */ + public static throttledEvents(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return StateTransitionMetric.metric("ThrottledEvents", { statistic: 'sum', ...props }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts index 2f909962e5336..9614f1e7efb8d 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts @@ -1,3 +1,4 @@ +import cloudwatch = require('@aws-cdk/aws-cloudwatch'); import cdk = require('@aws-cdk/cdk'); import { Errors, IChainable, IStateChain, RetryProps } from '../asl-external-api'; import { IInternalState, StateType, TransitionType } from '../asl-internal-api'; @@ -5,21 +6,6 @@ import { StateChain } from '../asl-state-chain'; import { State } from './state'; import { renderRetries } from './util'; -/** - * Interface for objects that can be invoked in a Task state - */ -export interface IStepFunctionsTaskResource { - /** - * Return the properties required for using this object as a Task resource - */ - asStepFunctionsTaskResource(callingTask: Task): StepFunctionsTaskResourceProps; -} - -export interface StepFunctionsTaskResourceProps { - resourceArn: cdk.Arn; - policyStatements?: cdk.PolicyStatement[]; -} - export interface TaskProps { resource: IStepFunctionsTaskResource; inputPath?: string; @@ -105,4 +91,125 @@ export class Task extends State { public toStateChain(): IStateChain { return new StateChain(new Task.Internals(this)); } + + /** + * Return the given named metric for this Task + * + * @default sum over 5 minutes + */ + public metric(metricName: string, props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace: 'AWS/States', + metricName, + dimensions: this.resourceProps.metricDimensions, + statistic: 'sum', + ...props + }); + } + + /** + * The interval, in milliseconds, between the time the Task starts and the time it closes. + * + * @default average over 5 minutes + */ + public metricRunTime(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.taskMetric(this.resourceProps.metricPrefixSingular, 'RunTime', { statistic: 'avg', ...props }); + } + + /** + * The interval, in milliseconds, for which the activity stays in the schedule state. + * + * @default average over 5 minutes + */ + public metricScheduleTime(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.taskMetric(this.resourceProps.metricPrefixSingular, 'ScheduleTime', { statistic: 'avg', ...props }); + } + + /** + * The interval, in milliseconds, between the time the activity is scheduled and the time it closes. + * + * @default average over 5 minutes + */ + public metricTime(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.taskMetric(this.resourceProps.metricPrefixSingular, 'ActivityTime', { statistic: 'avg', ...props }); + } + + /** + * Metric for the number of times this activity is scheduled + * + * @default sum over 5 minutes + */ + public metricScheduled(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.taskMetric(this.resourceProps.metricPrefixPlural, 'Scheduled', props); + } + + /** + * Metric for the number of times this activity times out + * + * @default sum over 5 minutes + */ + public metricTimedOut(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.taskMetric(this.resourceProps.metricPrefixPlural, 'TimedOut', props); + } + + /** + * Metric for the number of times this activity is started + * + * @default sum over 5 minutes + */ + public metricStarted(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.taskMetric(this.resourceProps.metricPrefixPlural, 'Started', props); + } + + /** + * Metric for the number of times this activity succeeds + * + * @default sum over 5 minutes + */ + public metricSucceeded(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.taskMetric(this.resourceProps.metricPrefixPlural, 'Succeeded', props); + } + + /** + * Metric for the number of times this activity fails + * + * @default sum over 5 minutes + */ + public metricFailed(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.taskMetric(this.resourceProps.metricPrefixPlural, 'Failed', props); + } + + /** + * Metric for the number of times the heartbeat times out for this activity + * + * @default sum over 5 minutes + */ + public metricHeartbeatTimedOut(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + return this.taskMetric(this.resourceProps.metricPrefixPlural, 'HeartbeatTimedOut', props); + } + + private taskMetric(prefix: string | undefined, suffix: string, props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + if (prefix === undefined) { + throw new Error('This Task Resource does not expose metrics'); + } + return this.metric(prefix + suffix, props); + } +} + +/** + * Interface for objects that can be invoked in a Task state + */ +export interface IStepFunctionsTaskResource { + /** + * Return the properties required for using this object as a Task resource + */ + asStepFunctionsTaskResource(callingTask: Task): StepFunctionsTaskResourceProps; } + +export interface StepFunctionsTaskResourceProps { + resourceArn: cdk.Arn; + policyStatements?: cdk.PolicyStatement[]; + metricPrefixSingular?: string; + metricPrefixPlural?: string; + metricDimensions?: cloudwatch.DimensionHash; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.expected.json b/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.expected.json index 518e5ebee86b8..f05b3a9a1a17b 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.expected.json @@ -55,11 +55,11 @@ { "Ref": "CheckJob5FFC1D6F" }, - "\",\"ResultPath\":\"$.status\",\"Retry\":[{\"ErrorEquals\":[\"States.ALL\"],\"IntervalSeconds\":1,\"MaxAttempts\":3,\"BackoffRate\":2}],\"Next\":\"Job Complete?\"},\"Job Complete?\":{\"Type\":\"Choice\",\"Choices\":[{\"Variable\":\"$.status\",\"StringEquals\":\"FAILED\",\"Next\":\"Job Failed\"},{\"Variable\":\"$.status\",\"StringEquals\":\"SUCCEEDED\",\"Next\":\"Get Final Job Status\"}]},\"Job Failed\":{\"Type\":\"Fail\",\"Error\":\"DescribeJob returned FAILED\",\"Cause\":\"AWS Batch Job Failed\"},\"Get Final Job Status\":{\"Type\":\"Task\",\"InputPath\":\"$.guid\",\"Resource\":\"", + "\",\"ResultPath\":\"$.status\",\"Retry\":[{\"ErrorEquals\":[\"States.ALL\"],\"IntervalSeconds\":1,\"MaxAttempts\":3,\"BackoffRate\":2}],\"Next\":\"Job Complete?\"},\"Job Complete?\":{\"Type\":\"Choice\",\"Choices\":[{\"Variable\":\"$.status\",\"StringEquals\":\"FAILED\",\"Next\":\"Job Failed\"},{\"Variable\":\"$.status\",\"StringEquals\":\"SUCCEEDED\",\"Next\":\"Get Final Job Status\"}],\"Default\":\"Wait X Seconds\"},\"Job Failed\":{\"Type\":\"Fail\",\"Error\":\"DescribeJob returned FAILED\",\"Cause\":\"AWS Batch Job Failed\"},\"Get Final Job Status\":{\"Type\":\"Task\",\"InputPath\":\"$.guid\",\"Resource\":\"", { "Ref": "CheckJob5FFC1D6F" }, - "\",\"Retry\":[{\"ErrorEquals\":[\"States.ALL\"],\"IntervalSeconds\":1,\"MaxAttempts\":3,\"BackoffRate\":2}],\"End\":true}}}" + "\",\"Retry\":[{\"ErrorEquals\":[\"States.ALL\"],\"IntervalSeconds\":1,\"MaxAttempts\":3,\"BackoffRate\":2}],\"End\":true}},\"TimeoutSeconds\":30}" ] ] }, diff --git a/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.ts b/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.ts index e9d881489fc24..9cf501a2ae12f 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.ts @@ -33,7 +33,8 @@ class JobPollerStack extends cdk.Stack { .next(getStatus) .next(isComplete .on(stepfunctions.Condition.stringEquals('$.status', 'FAILED'), jobFailed) - .on(stepfunctions.Condition.stringEquals('$.status', 'SUCCEEDED'), finalStatus)) + .on(stepfunctions.Condition.stringEquals('$.status', 'SUCCEEDED'), finalStatus) + .otherwise(waitX)) .defaultRetry({ intervalSeconds: 1, maxAttempts: 3, @@ -41,7 +42,8 @@ class JobPollerStack extends cdk.Stack { }); new stepfunctions.StateMachine(this, 'StateMachine', { - definition: chain + definition: chain, + timeoutSeconds: 30 }); } } diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts index 497159e1916b3..d71ffd1a47846 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts @@ -62,6 +62,34 @@ export = { test.done(); }, + + 'Task metrics use values returned from resource'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const task = new stepfunctions.Task(stack, 'Task', { resource: new FakeResource() }); + + // THEN + const sharedMetric = { + periodSec: 300, + namespace: 'AWS/States', + dimensions: { ResourceArn: 'resource' }, + }; + test.deepEqual(cdk.resolve(task.metricRunTime()), { + ...sharedMetric, + metricName: 'FakeResourceRunTime', + statistic: 'avg' + }); + + test.deepEqual(cdk.resolve(task.metricFailed()), { + ...sharedMetric, + metricName: 'FakeResourcesFailed', + statistic: 'sum' + }); + + test.done(); + } }; class FakeResource implements stepfunctions.IStepFunctionsTaskResource { @@ -73,7 +101,10 @@ class FakeResource implements stepfunctions.IStepFunctionsTaskResource { policyStatements: [new cdk.PolicyStatement() .addAction('resource:Everything') .addResource(new cdk.Arn('resource')) - ] + ], + metricPrefixSingular: 'FakeResource', + metricPrefixPlural: 'FakeResources', + metricDimensions: { ResourceArn: resourceArn }, }; } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts index 9e15e36ed9b64..c01480cbd178b 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts @@ -675,7 +675,7 @@ class ReusableStateMachineWithImplicitStartState extends stepfunctions.StateMach class FakeResource implements stepfunctions.IStepFunctionsTaskResource { public asStepFunctionsTaskResource(_callingTask: stepfunctions.Task): stepfunctions.StepFunctionsTaskResourceProps { return { - resourceArn: new cdk.Arn('resource') + resourceArn: new cdk.Arn('resource'), }; } } From 006f98e033fc0549cda48cc5c28bf3f478213045 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 4 Sep 2018 12:56:21 +0200 Subject: [PATCH 16/29] Metric tests for activities --- .../aws-stepfunctions/test/test.activity.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts index a63249a151a50..7e682e6d1458f 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts @@ -45,4 +45,32 @@ export = { test.done(); }, + + 'Activity exposes metrics'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const activity = new stepfunctions.Activity(stack, 'Activity'); + + // THEN + const sharedMetric = { + periodSec: 300, + namespace: 'AWS/States', + dimensions: { ActivityArn: { Ref: 'Activity04690B0A' }}, + }; + test.deepEqual(cdk.resolve(activity.metricRunTime()), { + ...sharedMetric, + metricName: 'ActivityRunTime', + statistic: 'avg' + }); + + test.deepEqual(cdk.resolve(activity.metricFailed()), { + ...sharedMetric, + metricName: 'ActivitiesFailed', + statistic: 'sum' + }); + + test.done(); + } }; \ No newline at end of file From abb85ef06dbffc44724e452397c61d40cfbe3250 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 4 Sep 2018 12:59:59 +0200 Subject: [PATCH 17/29] Add comments to all states --- packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts | 2 ++ packages/@aws-cdk/aws-stepfunctions/lib/states/fail.ts | 4 +++- .../@aws-cdk/aws-stepfunctions/lib/states/parallel.ts | 2 ++ packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts | 4 +++- .../@aws-cdk/aws-stepfunctions/lib/states/succeed.ts | 9 +++++++-- packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts | 4 +++- packages/@aws-cdk/aws-stepfunctions/lib/states/wait.ts | 5 ++++- 7 files changed, 24 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts index 19df69d3591b0..5fae2a5bd6623 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts @@ -6,6 +6,7 @@ import { StateChain } from '../asl-state-chain'; import { State } from './state'; export interface ChoiceProps { + comment?: string; inputPath?: string; outputPath?: string; } @@ -51,6 +52,7 @@ export class Choice extends State { Type: StateType.Choice, InputPath: props.inputPath, OutputPath: props.outputPath, + Comment: props.comment, }); } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/fail.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/fail.ts index 1ca2cc602b701..3f260a220ea1d 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/fail.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/fail.ts @@ -7,6 +7,7 @@ import { State } from './state'; export interface FailProps { error: string; cause: string; + comment?: string; } export class Fail extends State { @@ -45,7 +46,8 @@ export class Fail extends State { super(parent, id, { Type: StateType.Fail, Error: props.error, - Cause: props.cause + Cause: props.cause, + Comment: props.comment, }); } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts index a595879f05a1f..31a3576f39cbc 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts @@ -9,6 +9,7 @@ export interface ParallelProps { inputPath?: string; outputPath?: string; resultPath?: string; + comment?: string; } export class Parallel extends State { @@ -67,6 +68,7 @@ export class Parallel extends State { InputPath: props.inputPath, OutputPath: props.outputPath, ResultPath: props.resultPath, + Comment: props.comment, // Lazy because the states are mutable and they might get chained onto // (Users shouldn't, but they might) Branches: new cdk.Token(() => this.branches.map(b => b.toStateChain().renderStateMachine().stateMachineDefinition)) diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts index ce729fa9a1b25..1c65bc8e9f36f 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts @@ -7,6 +7,7 @@ import { State } from './state'; export interface PassProps { inputPath?: string; outputPath?: string; + comment?: string; } export class Pass extends State { @@ -51,7 +52,8 @@ export class Pass extends State { super(parent, id, { Type: StateType.Pass, InputPath: props.inputPath, - OutputPath: props.outputPath + OutputPath: props.outputPath, + Comment: props.comment, }); } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts index cdac5711d463a..7730bf83fe006 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts @@ -4,6 +4,10 @@ import { IInternalState, StateType } from '../asl-internal-api'; import { StateChain } from '../asl-state-chain'; import { State } from './state'; +export interface SucceedProps { + comment?: string; +} + export class Succeed extends State { private static Internals = class implements IInternalState { public readonly hasOpenNextTransition = false; @@ -36,9 +40,10 @@ export class Succeed extends State { } }; - constructor(parent: cdk.Construct, id: string) { + constructor(parent: cdk.Construct, id: string, props: SucceedProps = {}) { super(parent, id, { - Type: StateType.Succeed + Type: StateType.Succeed, + Comment: props.comment, }); } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts index 9614f1e7efb8d..767895591a9d9 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts @@ -13,6 +13,7 @@ export interface TaskProps { resultPath?: string; timeoutSeconds?: number; heartbeatSeconds?: number; + comment?: string; } export class Task extends State { @@ -67,7 +68,8 @@ export class Task extends State { Resource: new cdk.Token(() => this.resourceProps.resourceArn), ResultPath: props.resultPath, TimeoutSeconds: props.timeoutSeconds, - HeartbeatSeconds: props.heartbeatSeconds + HeartbeatSeconds: props.heartbeatSeconds, + Comment: props.comment, }); this.resourceProps = props.resource.asStepFunctionsTaskResource(this); } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/wait.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/wait.ts index 4229effa31c7c..d70b3a0278c48 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/wait.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/wait.ts @@ -10,6 +10,8 @@ export interface WaitProps { secondsPath?: string; timestampPath?: string; + + comment?: string; } export class Wait extends State { @@ -57,7 +59,8 @@ export class Wait extends State { Seconds: props.seconds, Timestamp: props.timestamp, SecondsPath: props.secondsPath, - TimestampPath: props.timestampPath + TimestampPath: props.timestampPath, + Comment: props.comment, }); } From dc749098c3938cb9bd64383306ce7ec37c072cfb Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 4 Sep 2018 13:15:25 +0200 Subject: [PATCH 18/29] Add resultPath to error handler --- .../aws-stepfunctions/lib/asl-external-api.ts | 14 +++++++++++++- .../aws-stepfunctions/lib/asl-internal-api.ts | 4 ++-- .../aws-stepfunctions/lib/asl-state-chain.ts | 10 +++++----- .../aws-stepfunctions/lib/states/choice.ts | 4 ++-- .../@aws-cdk/aws-stepfunctions/lib/states/fail.ts | 4 ++-- .../aws-stepfunctions/lib/states/parallel.ts | 13 ++++++++----- .../@aws-cdk/aws-stepfunctions/lib/states/pass.ts | 4 ++-- .../aws-stepfunctions/lib/states/succeed.ts | 4 ++-- .../@aws-cdk/aws-stepfunctions/lib/states/task.ts | 13 ++++++++----- .../@aws-cdk/aws-stepfunctions/lib/states/wait.ts | 4 ++-- 10 files changed, 46 insertions(+), 28 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl-external-api.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl-external-api.ts index 37b7cca20101a..96d78677ee51c 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/asl-external-api.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/asl-external-api.ts @@ -22,7 +22,7 @@ export interface IStateChain extends IChainable { * can have error handlers applied, it is wrapped in a Parallel * block first. */ - onError(errorHandler: IChainable, ...errors: string[]): IStateChain; + onError(errorHandler: IChainable, props?: CatchProps): IStateChain; /** * Add retries to all states in the chains which can have retries applied @@ -92,6 +92,9 @@ export class Errors { } export interface RetryProps { + /** + * @default All + */ errors?: string[]; /** @@ -110,4 +113,13 @@ export interface RetryProps { * @default 2 */ backoffRate?: number; +} + +export interface CatchProps { + /** + * @default All + */ + errors?: string[]; + + resultPath?: string; } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl-internal-api.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl-internal-api.ts index 81ebc9385fc60..632a0e967e9c3 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/asl-internal-api.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/asl-internal-api.ts @@ -1,5 +1,5 @@ import cdk = require('@aws-cdk/cdk'); -import { IStateChain, RetryProps } from './asl-external-api'; +import { CatchProps, IStateChain, RetryProps } from './asl-external-api'; import { accessChainInternals } from './asl-state-chain'; export interface IInternalState { @@ -9,7 +9,7 @@ export interface IInternalState { readonly policyStatements: cdk.PolicyStatement[]; addNext(target: IStateChain): void; - addCatch(target: IStateChain, errors: string[]): void; + addCatch(target: IStateChain, props?: CatchProps): void; addRetry(retry?: RetryProps): void; accessibleChains(): IStateChain[]; diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl-state-chain.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl-state-chain.ts index 78957e112899f..d74832a1212ad 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/asl-state-chain.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/asl-state-chain.ts @@ -1,5 +1,5 @@ import cdk = require('@aws-cdk/cdk'); -import { Errors, IChainable, IStateChain, RenderedStateMachine, RetryProps } from './asl-external-api'; +import { CatchProps, Errors, IChainable, IStateChain, RenderedStateMachine, RetryProps } from './asl-external-api'; import { IInternalState } from './asl-internal-api'; export class StateChain implements IStateChain { @@ -45,9 +45,9 @@ export class StateChain implements IStateChain { return this; } - public onError(handler: IChainable, ...errors: string[]): IStateChain { - if (errors.length === 0) { - errors = [Errors.all]; + public onError(handler: IChainable, props: CatchProps = {}): IStateChain { + if (!props.errors) { + props.errors = [Errors.all]; } const sm = handler.toStateChain(); @@ -61,7 +61,7 @@ export class StateChain implements IStateChain { const ret = this.clone(); for (const state of this.allStates) { - state.addCatch(sm, errors); + state.addCatch(sm, props); } // Those states are now part of the state machine, but we don't include diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts index 5fae2a5bd6623..5b620f5453020 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts @@ -1,6 +1,6 @@ import cdk = require('@aws-cdk/cdk'); import { Condition } from '../asl-condition'; -import { IChainable, IStateChain, RetryProps } from '../asl-external-api'; +import { CatchProps, IChainable, IStateChain, RetryProps } from '../asl-external-api'; import { IInternalState, StateType, TransitionType } from '../asl-internal-api'; import { StateChain } from '../asl-state-chain'; import { State } from './state'; @@ -34,7 +34,7 @@ export class Choice extends State { throw new Error("Cannot chain onto a Choice state. Use the state's .on() or .otherwise() instead."); } - public addCatch(_targetState: IStateChain, _errors: string[]): void { + public addCatch(_targetState: IStateChain, _props?: CatchProps): void { throw new Error("Cannot catch errors on a Choice."); } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/fail.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/fail.ts index 3f260a220ea1d..ec1d0800ef1f2 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/fail.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/fail.ts @@ -1,5 +1,5 @@ import cdk = require('@aws-cdk/cdk'); -import { IStateChain, RetryProps } from '../asl-external-api'; +import { CatchProps, IStateChain, RetryProps } from '../asl-external-api'; import { IInternalState, StateType } from '../asl-internal-api'; import { StateChain } from '../asl-state-chain'; import { State } from './state'; @@ -29,7 +29,7 @@ export class Fail extends State { throw new Error("Cannot chain onto a Fail state. This ends the state machine."); } - public addCatch(_targetState: IStateChain, _errors: string[]): void { + public addCatch(_targetState: IStateChain, _props?: CatchProps): void { throw new Error("Cannot catch errors on a Fail."); } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts index 31a3576f39cbc..dcebcaf4feb8c 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts @@ -1,5 +1,5 @@ import cdk = require('@aws-cdk/cdk'); -import { Errors, IChainable, IStateChain, RetryProps } from '../asl-external-api'; +import { CatchProps, Errors, IChainable, IStateChain, RetryProps } from '../asl-external-api'; import { IInternalState, StateType, TransitionType } from '../asl-internal-api'; import { StateChain } from '../asl-state-chain'; import { State } from './state'; @@ -34,8 +34,11 @@ export class Parallel extends State { this.parallel.addNextTransition(targetState); } - public addCatch(targetState: IStateChain, errors: string[]): void { - this.parallel.transitions.add(TransitionType.Catch, targetState, { ErrorEquals: errors }); + public addCatch(targetState: IStateChain, props: CatchProps = {}): void { + this.parallel.transitions.add(TransitionType.Catch, targetState, { + ErrorEquals: props.errors ? props.errors : [Errors.all], + ResultPath: props.resultPath + }); } public addRetry(retry?: RetryProps): void { @@ -92,8 +95,8 @@ export class Parallel extends State { return this.toStateChain().next(sm); } - public onError(handler: IChainable, ...errors: string[]): IStateChain { - return this.toStateChain().onError(handler, ...errors); + public onError(handler: IChainable, props: CatchProps = {}): IStateChain { + return this.toStateChain().onError(handler, props); } public toStateChain(): IStateChain { diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts index 1c65bc8e9f36f..8917792108ff3 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts @@ -1,5 +1,5 @@ import cdk = require('@aws-cdk/cdk'); -import { IChainable, IStateChain, RetryProps } from '../asl-external-api'; +import { CatchProps, IChainable, IStateChain, RetryProps } from '../asl-external-api'; import { IInternalState, StateType, TransitionType } from '../asl-internal-api'; import { StateChain } from '../asl-state-chain'; import { State } from './state'; @@ -31,7 +31,7 @@ export class Pass extends State { this.pass.addNextTransition(targetState); } - public addCatch(_targetState: IStateChain, _errors: string[]): void { + public addCatch(_targetState: IStateChain, _props?: CatchProps): void { throw new Error("Cannot catch errors on a Pass."); } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts index 7730bf83fe006..955ad16022fd0 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts @@ -1,5 +1,5 @@ import cdk = require('@aws-cdk/cdk'); -import { IStateChain, RetryProps } from '../asl-external-api'; +import { CatchProps, IStateChain, RetryProps } from '../asl-external-api'; import { IInternalState, StateType } from '../asl-internal-api'; import { StateChain } from '../asl-state-chain'; import { State } from './state'; @@ -27,7 +27,7 @@ export class Succeed extends State { throw new Error("Cannot chain onto a Succeed state; this ends the state machine."); } - public addCatch(_targetState: IStateChain, _errors: string[]): void { + public addCatch(_targetState: IStateChain, _props?: CatchProps): void { throw new Error("Cannot catch errors on a Succeed."); } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts index 767895591a9d9..ab742c7db86d9 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts @@ -1,6 +1,6 @@ import cloudwatch = require('@aws-cdk/aws-cloudwatch'); import cdk = require('@aws-cdk/cdk'); -import { Errors, IChainable, IStateChain, RetryProps } from '../asl-external-api'; +import { CatchProps, Errors, IChainable, IStateChain, RetryProps } from '../asl-external-api'; import { IInternalState, StateType, TransitionType } from '../asl-internal-api'; import { StateChain } from '../asl-state-chain'; import { State } from './state'; @@ -40,8 +40,11 @@ export class Task extends State { this.task.addNextTransition(targetState); } - public addCatch(targetState: IStateChain, errors: string[]): void { - this.task.transitions.add(TransitionType.Catch, targetState, { ErrorEquals: errors }); + public addCatch(targetState: IStateChain, props: CatchProps = {}): void { + this.task.transitions.add(TransitionType.Catch, targetState, { + ErrorEquals: props.errors ? props.errors : [Errors.all], + ResultPath: props.resultPath + }); } public addRetry(retry?: RetryProps): void { @@ -78,8 +81,8 @@ export class Task extends State { return this.toStateChain().next(sm); } - public onError(handler: IChainable, ...errors: string[]): IStateChain { - return this.toStateChain().onError(handler, ...errors); + public onError(handler: IChainable, props?: CatchProps): IStateChain { + return this.toStateChain().onError(handler, props); } public retry(props: RetryProps = {}): Task { diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/wait.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/wait.ts index d70b3a0278c48..dc7b0bd16b7d5 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/wait.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/wait.ts @@ -1,5 +1,5 @@ import cdk = require('@aws-cdk/cdk'); -import { IChainable, IStateChain, RetryProps } from '../asl-external-api'; +import { CatchProps, IChainable, IStateChain, RetryProps } from '../asl-external-api'; import { IInternalState, StateType, TransitionType } from '../asl-internal-api'; import { StateChain } from '../asl-state-chain'; import { State } from './state'; @@ -35,7 +35,7 @@ export class Wait extends State { this.wait.addNextTransition(targetState); } - public addCatch(_targetState: IStateChain, _errors: string[]): void { + public addCatch(_targetState: IStateChain, _props?: CatchProps): void { throw new Error("Cannot catch errors on a Wait."); } From b23fe581013c6e26a0fb270913250db7a7dbb6c1 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 4 Sep 2018 14:42:07 +0200 Subject: [PATCH 19/29] Add some more supported input/output/resultpaths --- .../stepfunctions-poller/cdk.json | 3 -- .../stepfunctions-poller/index.ts | 40 ------------------- .../aws-stepfunctions/lib/states/pass.ts | 2 + .../aws-stepfunctions/lib/states/succeed.ts | 4 ++ .../test/test.states-language.ts | 32 +++++++++++++++ 5 files changed, 38 insertions(+), 43 deletions(-) delete mode 100644 examples/cdk-examples-typescript/stepfunctions-poller/cdk.json delete mode 100644 examples/cdk-examples-typescript/stepfunctions-poller/index.ts diff --git a/examples/cdk-examples-typescript/stepfunctions-poller/cdk.json b/examples/cdk-examples-typescript/stepfunctions-poller/cdk.json deleted file mode 100644 index 071a16c8242fe..0000000000000 --- a/examples/cdk-examples-typescript/stepfunctions-poller/cdk.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "app": "node index" -} diff --git a/examples/cdk-examples-typescript/stepfunctions-poller/index.ts b/examples/cdk-examples-typescript/stepfunctions-poller/index.ts deleted file mode 100644 index 9c5d00dac1bef..0000000000000 --- a/examples/cdk-examples-typescript/stepfunctions-poller/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -import stepfunctions = require('@aws-cdk/aws-stepfunctions'); -import cdk = require('@aws-cdk/cdk'); - -class Poller extends stepfunctions.StateMachineDefinition { - constructor(parent: cdk.Construct, id: string) { - super(parent, id); - - const submitJob = new stepfunctions.Pass(this, 'Submit Job'); - const waitJob = new stepfunctions.Wait(this, 'Wait X Seconds', { seconds: 60 }); - const getJobStatus = new stepfunctions.Pass(this, 'Get Job Status'); - const jobComplete = new stepfunctions.Choice(this, 'Job Complete?'); - const jobFailed = new stepfunctions.Fail(this, 'Job Failed', { cause: 'Job failed', error: 'JobFailure' }); - const getFinalStatus = new stepfunctions.Pass(this, 'Get Final Job Status'); - - this.define(submitJob - .then(waitJob) - .then(getJobStatus) - .then(jobComplete - .on(stepfunctions.Condition.stringEquals('$.status', 'SUCCEEDED'), getFinalStatus) - .on(stepfunctions.Condition.stringEquals('$.status', 'FAILED'), jobFailed) - .otherwise(waitJob))); - - // States referenceable inside container are different from states - // rendered! - } -} - -class StepFunctionsDemoStack extends cdk.Stack { - constructor(parent: cdk.App, name: string) { - super(parent, name); - - new stepfunctions.StateMachine(this, 'StateMachine', { - definition: new Poller(this, 'Poller') - }); - } -} - -const app = new cdk.App(process.argv); -new StepFunctionsDemoStack(app, 'StepFunctionsDemo'); -process.stdout.write(app.run()); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts index 8917792108ff3..61d61aef6fd2c 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts @@ -7,6 +7,7 @@ import { State } from './state'; export interface PassProps { inputPath?: string; outputPath?: string; + resultPath?: string; comment?: string; } @@ -53,6 +54,7 @@ export class Pass extends State { Type: StateType.Pass, InputPath: props.inputPath, OutputPath: props.outputPath, + ResultPath: props.resultPath, Comment: props.comment, }); } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts index 955ad16022fd0..14a861ee74f25 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts @@ -5,6 +5,8 @@ import { StateChain } from '../asl-state-chain'; import { State } from './state'; export interface SucceedProps { + inputPath?: string; + outputPath?: string; comment?: string; } @@ -43,6 +45,8 @@ export class Succeed extends State { constructor(parent: cdk.Construct, id: string, props: SucceedProps = {}) { super(parent, id, { Type: StateType.Succeed, + InputPath: props.inputPath, + OutputPath: props.outputPath, Comment: props.comment, }); } diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts index c01480cbd178b..4453e0408972f 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts @@ -389,6 +389,38 @@ export = { test.done(); }, + 'Retries and errors with a result path'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const task1 = new stepfunctions.Task(stack, 'Task1', { resource: new FakeResource() }); + const failure = new stepfunctions.Fail(stack, 'Failed', { error: 'DidNotWork', cause: 'We got stuck' }); + + // WHEN + const chain = task1.retry({ errors: ['HTTPError'], maxAttempts: 2 }).onError(failure, { resultPath: '$.some_error' }).next(failure); + + // THEN + test.deepEqual(render(chain), { + StartAt: 'Task1', + States: { + Task1: { + Type: 'Task', + Resource: 'resource', + Catch: [ { ErrorEquals: ['States.ALL'], Next: 'Failed', ResultPath: '$.some_error' } ], + Retry: [ { ErrorEquals: ['HTTPError'], MaxAttempts: 2 } ], + Next: 'Failed', + }, + Failed: { + Type: 'Fail', + Error: 'DidNotWork', + Cause: 'We got stuck', + } + } + }); + + test.done(); + + }, + 'Error branch is attached to all tasks in chain'(test: Test) { // GIVEN const stack = new cdk.Stack(); From bd4f1da90c0da675fe355b57730a818babf07520 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 4 Sep 2018 14:56:18 +0200 Subject: [PATCH 20/29] Support "Result" field on "Pass" --- packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts index 61d61aef6fd2c..acfdc9c53160d 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts @@ -9,6 +9,7 @@ export interface PassProps { outputPath?: string; resultPath?: string; comment?: string; + result?: any; } export class Pass extends State { @@ -56,6 +57,7 @@ export class Pass extends State { OutputPath: props.outputPath, ResultPath: props.resultPath, Comment: props.comment, + Result: props.result, }); } From 9645ade0db5993e348d6030d2684a4e3d3e5ae38 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 4 Sep 2018 17:10:23 +0200 Subject: [PATCH 21/29] Small fixes to deal with oddities of mutable state --- .../lib/states/state-machine-fragment.ts | 14 ++++- .../test/test.states-language.ts | 62 +++++++++++++++++++ .../lib/cloudformation/cloudformation-json.ts | 3 +- 3 files changed, 76 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/state-machine-fragment.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/state-machine-fragment.ts index 2251ba9ccf345..15d6544a07ead 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/state-machine-fragment.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/state-machine-fragment.ts @@ -19,6 +19,7 @@ export class StateMachineFragment extends cdk.Construct implements IChainable { public readonly scopeStateNames: boolean; private _startState?: IChainable; + private chain?: IStateChain; constructor(parent: cdk.Construct, id: string, props: StateMachineFragmentProps = {}) { super(parent, id); @@ -34,14 +35,23 @@ export class StateMachineFragment extends cdk.Construct implements IChainable { } public toStateChain(): IStateChain { - // If we're converting a state machine definition to a state chain, grab the whole of it. - return this.startState.toStateChain().closure(); + this.freeze(); + return this.chain!; } public next(sm: IChainable): IStateChain { return this.toStateChain().next(sm); } + protected freeze() { + if (this.chain === undefined) { + // If we're converting a state machine definition to a state chain, grab the whole of it. + // We need to cache this value; because of the .closure(), it may change + // depending on whether states get chained onto the states in this fragment. + this.chain = this.startState.toStateChain().closure(); + } + } + private get startState(): IChainable { if (this._startState) { return this._startState; diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts index 4453e0408972f..4dea6a175cc16 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts @@ -605,6 +605,51 @@ export = { test.done(); }, + 'Chaining does not chain onto error handler, extended'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const task1 = new stepfunctions.Task(stack, 'Task1', { resource: new FakeResource() }); + const task2 = new stepfunctions.Task(stack, 'Task2', { resource: new FakeResource() }); + const task3 = new stepfunctions.Task(stack, 'Task3', { resource: new FakeResource() }); + const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); + + // WHEN + const chain = task1.onError(errorHandler) + .next(task2.onError(errorHandler)) + .next(task3.onError(errorHandler)); + + // THEN + const sharedTaskProps = { Type: 'Task', Resource: 'resource', Catch: [ { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler' } ] }; + test.deepEqual(render(chain), { + StartAt: 'Task1', + States: { + Task1: { Next: 'Task2', ...sharedTaskProps }, + Task2: { Next: 'Task3', ...sharedTaskProps }, + Task3: { End: true, ...sharedTaskProps }, + ErrorHandler: { Type: 'Pass', End: true }, + } + }); + + test.done(); + }, + + 'Error handler with a fragment'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const task1 = new stepfunctions.Task(stack, 'Task1', { resource: new FakeResource() }); + const task2 = new stepfunctions.Task(stack, 'Task2', { resource: new FakeResource() }); + const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); + + // WHEN + task1.onError(errorHandler) + .next(new SimpleChain(stack, 'Chain').onError(errorHandler)) + .next(task2.onError(errorHandler)); + + test.done(); + }, + 'After calling .closure() do chain onto error state'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -704,6 +749,23 @@ class ReusableStateMachineWithImplicitStartState extends stepfunctions.StateMach } } +class SimpleChain extends stepfunctions.StateMachineFragment { + private readonly task2: stepfunctions.Task; + constructor(parent: cdk.Construct, id: string) { + super(parent, id); + + const task1 = new stepfunctions.Task(this, 'Task1', { resource: new FakeResource() }); + this.task2 = new stepfunctions.Task(this, 'Task2', { resource: new FakeResource() }); + + task1.next(this.task2); + } + + public onError(state: stepfunctions.IChainable, props?: stepfunctions.CatchProps): SimpleChain { + this.task2.onError(state, props); + return this; + } +} + class FakeResource implements stepfunctions.IStepFunctionsTaskResource { public asStepFunctionsTaskResource(_callingTask: stepfunctions.Task): stepfunctions.StepFunctionsTaskResourceProps { return { diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts index b5d9b03d54e44..038621de039e0 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/cloudformation-json.ts @@ -37,6 +37,7 @@ export class CloudFormationJSON { * Recurse into a structure, replace all intrinsics with IntrinsicTokens. */ function deepReplaceIntrinsics(x: any): any { + if (x == null) { return x; } if (isIntrinsic(x)) { return wrapIntrinsic(x); } @@ -94,4 +95,4 @@ function deepQuoteStringsForJSON(x: any): any { } return x; -} \ No newline at end of file +} From c53238dfbb6889e744a63b623c7c0e2fd5647825 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 4 Sep 2018 17:13:22 +0200 Subject: [PATCH 22/29] Shorten generated name if necessary --- packages/@aws-cdk/aws-stepfunctions/lib/activity.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts b/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts index 3edd7b990da1f..4adbc99cd3c6b 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts @@ -23,7 +23,7 @@ export class Activity extends cdk.Construct implements IStepFunctionsTaskResourc super(parent, id); const resource = new cloudformation.ActivityResource(this, 'Resource', { - activityName: props.activityName || this.uniqueId + activityName: props.activityName || this.generateName() }); this.activityArn = resource.ref; @@ -135,4 +135,12 @@ export class Activity extends cdk.Construct implements IStepFunctionsTaskResourc public metricHeartbeatTimedOut(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { return this.metric('ActivitiesHeartbeatTimedOut', props); } -} \ No newline at end of file + + private generateName(): string { + const name = this.uniqueId; + if (name.length > 80) { + return name.substring(0, 40) + name.substring(name.length - 40); + } + return name; + } +} From f8d534e1ff13e1ae6dd7a38fe14b97433473aafc Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 21 Sep 2018 15:23:09 +0200 Subject: [PATCH 23/29] Update to latest API on 'master' --- packages/@aws-cdk/aws-stepfunctions/lib/activity.ts | 8 ++++---- .../@aws-cdk/aws-stepfunctions/lib/state-machine.ts | 10 +++++----- packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts | 2 +- .../test/test.state-machine-resources.ts | 4 ++-- .../aws-stepfunctions/test/test.states-language.ts | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts b/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts index 4adbc99cd3c6b..a35422ac352b3 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts @@ -1,7 +1,7 @@ import cloudwatch = require('@aws-cdk/aws-cloudwatch'); import cdk = require('@aws-cdk/cdk'); import { IStepFunctionsTaskResource, StepFunctionsTaskResourceProps, Task } from './states/task'; -import { ActivityArn, ActivityName, cloudformation } from './stepfunctions.generated'; +import { cloudformation } from './stepfunctions.generated'; export interface ActivityProps { /** @@ -16,8 +16,8 @@ export interface ActivityProps { * Define a new StepFunctions activity */ export class Activity extends cdk.Construct implements IStepFunctionsTaskResource { - public readonly activityArn: ActivityArn; - public readonly activityName: ActivityName; + public readonly activityArn: string; + public readonly activityName: string; constructor(parent: cdk.Construct, id: string, props: ActivityProps = {}) { super(parent, id); @@ -26,7 +26,7 @@ export class Activity extends cdk.Construct implements IStepFunctionsTaskResourc activityName: props.activityName || this.generateName() }); - this.activityArn = resource.ref; + this.activityArn = resource.activityArn; this.activityName = resource.activityName; } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts index 47ce2e28d6b4d..cfe050e525ba4 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts @@ -3,7 +3,7 @@ import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { IChainable } from './asl-external-api'; -import { cloudformation, StateMachineArn, StateMachineName } from './stepfunctions.generated'; +import { cloudformation } from './stepfunctions.generated'; export interface StateMachineProps { /** @@ -34,8 +34,8 @@ export interface StateMachineProps { */ export class StateMachine extends cdk.Construct { public readonly role: iam.Role; - public readonly stateMachineName: StateMachineName; - public readonly stateMachineArn: StateMachineArn; + public readonly stateMachineName: string; + public readonly stateMachineArn: string; /** A role used by CloudWatch events to trigger a build */ private eventsRole?: iam.Role; @@ -63,7 +63,7 @@ export class StateMachine extends cdk.Construct { } this.stateMachineName = resource.stateMachineName; - this.stateMachineArn = resource.ref; + this.stateMachineArn = resource.stateMachineArn; } public addToRolePolicy(statement: cdk.PolicyStatement) { @@ -73,7 +73,7 @@ export class StateMachine extends cdk.Construct { /** * Allows using state machines as event rule targets. */ - public asEventRuleTarget(_ruleArn: events.RuleArn, _ruleId: string): events.EventRuleTargetProps { + public asEventRuleTarget(_ruleArn: string, _ruleId: string): events.EventRuleTargetProps { if (!this.eventsRole) { this.eventsRole = new iam.Role(this, 'EventsRole', { assumedBy: new cdk.ServicePrincipal('events.amazonaws.com') diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts index ab742c7db86d9..792c7babdd405 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts @@ -212,7 +212,7 @@ export interface IStepFunctionsTaskResource { } export interface StepFunctionsTaskResourceProps { - resourceArn: cdk.Arn; + resourceArn: string; policyStatements?: cdk.PolicyStatement[]; metricPrefixSingular?: string; metricPrefixPlural?: string; diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts index d71ffd1a47846..d6c0379a70cb1 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts @@ -94,13 +94,13 @@ export = { class FakeResource implements stepfunctions.IStepFunctionsTaskResource { public asStepFunctionsTaskResource(_callingTask: stepfunctions.Task): stepfunctions.StepFunctionsTaskResourceProps { - const resourceArn = new cdk.Arn('resource'); + const resourceArn = 'resource'; return { resourceArn, policyStatements: [new cdk.PolicyStatement() .addAction('resource:Everything') - .addResource(new cdk.Arn('resource')) + .addResource('resource') ], metricPrefixSingular: 'FakeResource', metricPrefixPlural: 'FakeResources', diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts index 4dea6a175cc16..9653807f5d176 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts @@ -769,7 +769,7 @@ class SimpleChain extends stepfunctions.StateMachineFragment { class FakeResource implements stepfunctions.IStepFunctionsTaskResource { public asStepFunctionsTaskResource(_callingTask: stepfunctions.Task): stepfunctions.StepFunctionsTaskResourceProps { return { - resourceArn: new cdk.Arn('resource'), + resourceArn: 'resource' }; } } From 9a84c20fa598165401241b0d6b96fc7cae23acad Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 21 Sep 2018 17:21:19 +0200 Subject: [PATCH 24/29] Finish first iteration --- packages/@aws-cdk/aws-stepfunctions/README.md | 324 +++++++++++- packages/@aws-cdk/aws-stepfunctions/TODO.txt | 3 - .../aws-stepfunctions/lib/asl-external-api.ts | 125 ----- .../aws-stepfunctions/lib/asl-internal-api.ts | 84 --- .../aws-stepfunctions/lib/asl-state-chain.ts | 150 ------ .../@aws-cdk/aws-stepfunctions/lib/chain.ts | 70 +++ .../lib/{asl-condition.ts => condition.ts} | 95 +++- .../@aws-cdk/aws-stepfunctions/lib/index.ts | 9 +- .../aws-stepfunctions/lib/state-graph.ts | 157 ++++++ .../lib/state-machine-fragment.ts | 81 +++ .../aws-stepfunctions/lib/state-machine.ts | 25 +- .../aws-stepfunctions/lib/states/choice.ts | 171 +++--- .../aws-stepfunctions/lib/states/fail.ts | 87 ++-- .../aws-stepfunctions/lib/states/parallel.ts | 188 ++++--- .../aws-stepfunctions/lib/states/pass.ts | 130 +++-- .../lib/states/state-machine-fragment.ts | 72 --- .../aws-stepfunctions/lib/states/state.ts | 486 +++++++++++++++++- .../aws-stepfunctions/lib/states/succeed.ts | 89 ++-- .../aws-stepfunctions/lib/states/task.ts | 230 ++++++--- .../aws-stepfunctions/lib/states/util.ts | 17 - .../aws-stepfunctions/lib/states/wait.ts | 132 +++-- .../@aws-cdk/aws-stepfunctions/lib/types.ts | 130 +++++ .../test/integ.job-poller.expected.json | 8 +- .../test/integ.job-poller.ts | 10 +- .../aws-stepfunctions/test/test.activity.ts | 5 +- .../aws-stepfunctions/test/test.condition.ts | 12 + .../aws-stepfunctions/test/test.metrics.ts | 44 ++ .../test/test.states-language.ts | 386 ++++++-------- 28 files changed, 2186 insertions(+), 1134 deletions(-) delete mode 100644 packages/@aws-cdk/aws-stepfunctions/TODO.txt delete mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/asl-external-api.ts delete mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/asl-internal-api.ts delete mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/asl-state-chain.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/chain.ts rename packages/@aws-cdk/aws-stepfunctions/lib/{asl-condition.ts => condition.ts} (66%) create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/state-graph.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/state-machine-fragment.ts delete mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/states/state-machine-fragment.ts delete mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/states/util.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/lib/types.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/test/test.condition.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/test/test.metrics.ts diff --git a/packages/@aws-cdk/aws-stepfunctions/README.md b/packages/@aws-cdk/aws-stepfunctions/README.md index 30aed40788ef4..147dda337a904 100644 --- a/packages/@aws-cdk/aws-stepfunctions/README.md +++ b/packages/@aws-cdk/aws-stepfunctions/README.md @@ -1,2 +1,322 @@ -## The CDK Construct Library for AWS Step Functions -This module is part of the [AWS Cloud Development Kit](https://github.com/awslabs/aws-cdk) project. +## AWS Step Functions Construct Library + +The `@aws-cdk/aws-stepfunctions` package contains constructs for building +serverless workflows. Using objects. Defining a workflow looks like this: + +```ts +const submitLambda = new lambda.Function(this, 'SubmitLambda', { ... }); +const getStatusLambda = new lambda.Function(this, 'CheckLambda', { ... }); + +const submitJob = new stepfunctions.Task(this, 'Submit Job', { + resource: submitLambda, + resultPath: '$.guid', +}); + +const waitX = new stepfunctions.Wait(this, 'Wait X Seconds', { secondsPath: '$.wait_time' }); + +const getStatus = new stepfunctions.Task(this, 'Get Job Status', { + resource: getStatusLambda, + inputPath: '$.guid', + resultPath: '$.status', +}); + +const jobFailed = new stepfunctions.Fail(this, 'Job Failed', { + cause: 'AWS Batch Job Failed', + error: 'DescribeJob returned FAILED', +}); + +const finalStatus = new stepfunctions.Task(this, 'Get Final Job Status', { + resource: getStatusLambda, + inputPath: '$.guid', +}); + +const definition = submitJob + .next(waitX) + .next(getStatus) + .next(new stepfunctions.Choice(this, 'Job Complete?') + .on(stepfunctions.Condition.stringEquals('$.status', 'FAILED'), jobFailed) + .on(stepfunctions.Condition.stringEquals('$.status', 'SUCCEEDED'), finalStatus) + .otherwise(waitX)); + +new stepfunctions.StateMachine(this, 'StateMachine', { + definition, + timeoutSeconds: 300 +}); +``` + +## State Machine + +A `stepfunctions.StateMachine` is a resource that takes a state machine +definition. The definition is specified by its start state, and encompasses +all states reachable from the start state: + +```ts +const startState = new stepfunctions.Pass(this, 'StartState'); + +new stepfunctions.StateMachine(this, 'StateMachine', { + definition: startState +}); +``` + +State machines execute using an IAM Role, which will automatically have all +permissions added that are required to make all state machine tasks execute +properly (for example, permissions to invoke any Lambda functions you add to +your workflow). A role will be created by default, but you can supply an +existing one as well. + +## Amazon States Language + +This library comes with a set of classes that model the [Amazon States +Language](https://states-language.net/spec.html). The following State classes +are supported: + +* `Task` +* `Pass` +* `Wait` +* `Choice` +* `Parallel` +* `Succeed` +* `Fail` + +An arbitrary JSON object (specified at execution start) is passed from state to +state and transformed during the execution of the workflow. For more +information, see the States Language spec. + +### Task + +A `Task` represents some work that needs to be done. It takes a `resource` +property that is either a Lambda `Function` or a Step Functions `Activity`. + +```ts +const task = new stepfunctions.Task(this, 'Invoke The Lambda', { + resource: myLambda, + inputPath: '$.input', + timeoutSeconds: 300, +}); + +// Add a retry policy +task.retry({ + intervalSeconds: 5, + maxAttempts: 10 +}); + +// Add an error handler +task.onError(errorHandlerState); + +// Set the next state +task.next(nextState); +``` + +### Pass + +A `Pass` state does no work, but it can optionally transform the execution's +JSON state. + +```ts +// Makes the current JSON state { ..., "subObject": { "hello": "world" } } +const pass = new stepfunctions.Pass(this, 'Add Hello World', { + result: { hello: "world" }, + resultPath: '$.subObject', +}); + +// Set the next state +pass.next(nextState); +``` + +### Wait + +A `Wait` state waits for a given number of seconds, or until the current time +hits a particular time. The time to wait may be taken from the execution's JSON +state. + +```ts +// Wait until it's the time mentioned in the the state object's "triggerTime" +// field. +const wait = new stepfunctions.Wait(this, 'Wait For Trigger Time', { + timestampPath: '$.triggerTime', +}); + +// Set the next state +wait.next(startTheWork); +``` + +### Choice + +A `Choice` state can take a differen path through the workflow based on the +values in the execution's JSON state: + +```ts +const choice = new stepfunctions.Choice(this, 'Did it work?'); + +// Add conditions with .on() +choice.on(stepfunctions.Condition.stringEqual('$.status', 'SUCCESS'), successState); +choice.on(stepfunctions.Condition.numberGreaterThan('$.attempts', 5), failureState); + +// Use .otherwise() to indicate what should be done if none of the conditions match +choice.otherwise(tryAgainState); +``` + +If you want to temporarily branch your workflow based on a condition, but have +all branches come together and continuing as one (similar to how an `if ... +then ... else` works in a programming language), use the `.afterwards()` method: + +```ts +const choice = new stepfunctions.Choice(this, 'What color is it?'); +choice.on(stepfunctions.Condition.stringEqual('$.color', 'BLUE'), handleBlueItem); +choice.on(stepfunctions.Condition.stringEqual('$.color', 'RED'), handleRedItem); +choice.otherwise(handleOtherItemColor); + +// Use .afterwards() to join all possible paths back together and continue +choice.afterwards().next(shipTheItem); +``` + +If your `Choice` doesn't have an `otherwise()` and none of the conditions match +the JSON state, a `NoChoiceMatched` error will be thrown. Wrap the state machine +in a `Parallel` state if you want to catch and recover from this. + +### Parallel + +A `Parallel` state executes one or more subworkflows in parallel. It can also +be used to catch and recover from errors in subworkflows. + +```ts +const parallel = new stepfunctions.Parallel(this, 'Do the work in parallel'); + +// Add branches to be executed in parallel +parallel.branch(shipItem); +parallel.branch(sendInvoice); +parallel.branch(restock); + +// Retry the whole workflow if something goes wrong +parallel.retry({ maxAttempts: 1 }); + +// How to recover from errors +parallel.onError(sendFailureNotification); + +// What to do in case everything succeeded +parallel.next(closeOrder); +``` + +### Succeed + +Reaching a `Succeed` state terminates the state machine execution with a +succesful status. + +```ts +const success = new stepfunctions.Succeed(this, 'We did it!'); +``` + +### Fail + +Reaching a `Fail` state terminates the state machine execution with a +failure status. The fail state should report the reason for the failure. +Failures can be caught by encompassing `Parallel` states. + +```ts +const success = new stepfunctions.Fail(this, 'Fail', { + error: 'WorkflowFailure', + cause: "Something went wrong" +}); +``` + +## Task Chaining + +To make defining work flows as convenient (and readable in a top-to-bottom way) +as writing regular programs, it is possible to chain most methods invocations. +In particular, the `.next()` method can be repeated. The result of a series of +`.next()` calls is called a **Chain**, and can be used when defining the jump +targets of `Choice.on` or `Parallel.branch`: + +```ts +const definition = step1 + .next(step2) + .next(choice + .on(condition1, step3.next(step4).next(step5)) + .otherwise(step6) + .afterwards()) + .next(parallel + .branch(step7.next(step8)) + .branch(step9.next(step10))) + .next(finish); + +new stepfunctions.StateMachine(this, 'StateMachine', { + definition, +}); +``` + +If you don't like the visual look of starting a chain directly off the first +step, you can use `Chain.start`: + +```ts +const definition = stepfunctions.Chain + .start(step1) + .next(step2) + .next(step3) + // ... +``` + +## State Machine Fragments + +It is possible to define reusable (or abstracted) mini-state machines by +defining a construct that implements `IChainable`, which requires you to define +two fields: + +* `startState: State`, representing the entry point into this state machine. +* `endStates: INextable[]`, representing the (one or more) states that outgoing + transitions will be added to if you chain onto the fragment. + +Since states will be named after their construct IDs, you may need to prefix the +IDs of states if you plan to instantiate the same state machine fragment +multiples times (otherwise all states in every instantiation would have the same +name). + +The class `StateMachineFragment` contains some helper functions (like +`prefixStates()`) to make it easier for you to do this. If you define your state +machine as a subclass of this, it will be convenient to use: + +```ts +interface MyJobProps { + jobFlavor: string; +} + +class MyJob extends stepfunctions.StateMachineFragment { + public readonly startState: State; + public readonly endStates: INextable[]; + + constructor(parent: cdk.Construct, id: string, props: MyJobProps) { + super(parent, id); + + const first = new stepfunctions.Task(this, 'First', { ... }); + // ... + const last = new stepfunctions.Task(this, 'Last', { ... }); + + this.startState = first; + this.endStates = [last]; + } +} + +// Do 3 different variants of MyJob in parallel +new stepfunctions.Parallel(this, 'All jobs') + .branch(new MyJob(this, 'Quick', { jobFlavor: 'quick' }).prefixStates()) + .branch(new MyJob(this, 'Medium', { jobFlavor: 'medium' }).prefixStates()) + .branch(new MyJob(this, 'Slow', { jobFlavor: 'slow' }).prefixStates()); +``` + +## Activity + +**Activities** represent work that is done on some non-Lambda worker pool. The +Step Functions workflow will submit work to this Activity, and a worker pool +that you run yourself, probably on EC2, will pull jobs from the Activity and +submit the results of individual jobs back. + +You need the ARN to do so, so if you use Activities be sure to pass the Activity +ARN into your worker pool. + +## Future work + +Contributions welcome: + +- [ ] A single `LambdaTask` class that is both a `Lambda` and a `Task` in one + might make for a nice API. +- [ ] Expression parser for Conditions. +- [ ] Simulate state machines in unit tests. diff --git a/packages/@aws-cdk/aws-stepfunctions/TODO.txt b/packages/@aws-cdk/aws-stepfunctions/TODO.txt deleted file mode 100644 index 02714836dafa6..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions/TODO.txt +++ /dev/null @@ -1,3 +0,0 @@ -- Metrics -- Integ tests -- Readme diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl-external-api.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl-external-api.ts deleted file mode 100644 index 96d78677ee51c..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions/lib/asl-external-api.ts +++ /dev/null @@ -1,125 +0,0 @@ -import cdk = require('@aws-cdk/cdk'); - -export interface IChainable { - /** - * Return a chain representing this state object. - */ - toStateChain(): IStateChain; -} - -export interface IStateChain extends IChainable { - /** - * Add a state or chain onto the existing chain. - * - * Returns a new chain with the state/chain added. - */ - next(state: IChainable): IStateChain; - - /** - * Add a Catch handlers to the states in the chain - * - * If the chain does not consist completely of states that - * can have error handlers applied, it is wrapped in a Parallel - * block first. - */ - onError(errorHandler: IChainable, props?: CatchProps): IStateChain; - - /** - * Add retries to all states in the chains which can have retries applied - */ - defaultRetry(retry?: RetryProps): IStateChain; - - /** - * Return a chain with all states reachable from the current chain. - * - * This includes the states reachable via error handler (Catch) - * transitions. - */ - closure(): IStateChain; - - /** - * Apply the closure, then render the state machine. - */ - renderStateMachine(): RenderedStateMachine; -} - -export interface RenderedStateMachine { - stateMachineDefinition: any; - policyStatements: cdk.PolicyStatement[]; -} - -/** - * Predefined error strings - */ -export class Errors { - /** - * Matches any Error. - */ - public static all = 'States.ALL'; - - /** - * A Task State either ran longer than the “TimeoutSeconds” value, or - * failed to heartbeat for a time longer than the “HeartbeatSeconds” value. - */ - public static timeout = 'States.Timeout'; - - /** - * A Task State failed during the execution. - */ - public static taskFailed = 'States.TaskFailed'; - - /** - * A Task State failed because it had insufficient privileges to execute - * the specified code. - */ - public static permissions = 'States.Permissions'; - - /** - * A Task State’s “ResultPath” field cannot be applied to the input the state received. - */ - public static resultPathMatchFailure = 'States.ResultPathMatchFailure'; - - /** - * A branch of a Parallel state failed. - */ - public static branchFailed = 'States.BranchFailed'; - - /** - * A Choice state failed to find a match for the condition field extracted - * from its input. - */ - public static noChoiceMatched = 'States.NoChoiceMatched'; -} - -export interface RetryProps { - /** - * @default All - */ - errors?: string[]; - - /** - * @default 1 - */ - intervalSeconds?: number; - - /** - * May be 0 to disable retry in case of multiple entries. - * - * @default 3 - */ - maxAttempts?: number; - - /** - * @default 2 - */ - backoffRate?: number; -} - -export interface CatchProps { - /** - * @default All - */ - errors?: string[]; - - resultPath?: string; -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl-internal-api.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl-internal-api.ts deleted file mode 100644 index 632a0e967e9c3..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions/lib/asl-internal-api.ts +++ /dev/null @@ -1,84 +0,0 @@ -import cdk = require('@aws-cdk/cdk'); -import { CatchProps, IStateChain, RetryProps } from './asl-external-api'; -import { accessChainInternals } from './asl-state-chain'; - -export interface IInternalState { - readonly stateId: string; - readonly canHaveCatch: boolean; - readonly hasOpenNextTransition: boolean; - readonly policyStatements: cdk.PolicyStatement[]; - - addNext(target: IStateChain): void; - addCatch(target: IStateChain, props?: CatchProps): void; - addRetry(retry?: RetryProps): void; - - accessibleChains(): IStateChain[]; - renderState(): any; -} - -export enum StateType { - Pass = 'Pass', - Task = 'Task', - Choice = 'Choice', - Wait = 'Wait', - Succeed = 'Succeed', - Fail = 'Fail', - Parallel = 'Parallel' -} - -export interface Transition { - transitionType: TransitionType; - targetChain: IStateChain; - annotation?: any; -} - -export enum TransitionType { - Next = 'Next', - Catch = 'Catch', - Choice = 'Choices', - Default = 'Default', -} - -export class Transitions { - private readonly transitions = new Array(); - - public add(transitionType: TransitionType, targetChain: IStateChain, annotation?: any) { - this.transitions.push({ transitionType, targetChain, annotation }); - } - - public has(type: TransitionType): boolean { - return this.get(type).length > 0; - } - - public get(type: TransitionType): Transition[] { - return this.transitions.filter(t => t.transitionType === type); - } - - public all(): Transition[] { - return this.transitions; - } - - public renderSingle(type: TransitionType, otherwise: any = {}): any { - const transitions = this.get(type); - if (transitions.length === 0) { - return otherwise; - } - return { - [type]: accessChainInternals(transitions[0].targetChain).startState.stateId - }; - } - - public renderList(type: TransitionType): any { - const transitions = this.get(type); - if (transitions.length === 0) { - return {}; - } - - return { - [type]: transitions.map(t => ({ - ...t.annotation, - Next: accessChainInternals(t.targetChain).startState.stateId, - })) - }; - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl-state-chain.ts b/packages/@aws-cdk/aws-stepfunctions/lib/asl-state-chain.ts deleted file mode 100644 index d74832a1212ad..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions/lib/asl-state-chain.ts +++ /dev/null @@ -1,150 +0,0 @@ -import cdk = require('@aws-cdk/cdk'); -import { CatchProps, Errors, IChainable, IStateChain, RenderedStateMachine, RetryProps } from './asl-external-api'; -import { IInternalState } from './asl-internal-api'; - -export class StateChain implements IStateChain { - private allStates = new Set(); - private activeStates = new Set(); - private _startState: IInternalState; - - constructor(startState: IInternalState) { - this.allStates.add(startState); - - // Even if the state doesn't allow .next()ing onto it, still set as - // active state so we trigger the per-State exception (which is more - // informative than the generic "no active states" exception). - this.activeStates.add(startState); - - this._startState = startState; - } - - public get startState(): IInternalState { - return this._startState; - } - - public next(state: IChainable): IStateChain { - const sm = state.toStateChain(); - - const ret = this.clone(); - - if (this.activeStates.size === 0) { - throw new Error('Cannot add to chain; there are no chainable states without a "Next" transition.'); - } - - for (const endState of this.activeStates) { - endState.addNext(sm); - } - - ret.absorb(sm); - ret.activeStates = new Set(accessChainInternals(sm).activeStates); - - return ret; - } - - public toStateChain(): IStateChain { - return this; - } - - public onError(handler: IChainable, props: CatchProps = {}): IStateChain { - if (!props.errors) { - props.errors = [Errors.all]; - } - - const sm = handler.toStateChain(); - - const canApplyDirectly = Array.from(this.allStates).every(s => s.canHaveCatch); - if (!canApplyDirectly) { - // Can't easily create a Parallel here automatically since we need a - // StateMachineDefinition parent and need to invent a unique name. - throw new Error('Chain contains non-Task, non-Parallel actions. Wrap this in a Parallel to catch errors.'); - } - - const ret = this.clone(); - for (const state of this.allStates) { - state.addCatch(sm, props); - } - - // Those states are now part of the state machine, but we don't include - // their active ends. - ret.absorb(sm); - - return ret; - } - - /** - * Return a closure that contains all accessible states from the given start state - * - * This sets all active ends to the active ends of all accessible states. - */ - public closure(): IStateChain { - const ret = new StateChain(this.startState); - - const queue = this.startState.accessibleChains(); - while (queue.length > 0) { - const chain = queue.splice(0, 1)[0]; - for (const state of accessChainInternals(chain).allStates) { - if (!ret.allStates.has(state)) { - ret.allStates.add(state); - queue.push(...state.accessibleChains()); - } - } - } - - ret.activeStates = new Set(Array.from(ret.allStates).filter(s => s.hasOpenNextTransition)); - - return ret; - } - - public renderStateMachine(): RenderedStateMachine { - // Rendering always implies rendering the closure - const closed = this.closure(); - - const policies = new Array(); - - const states: any = {}; - for (const state of accessChainInternals(closed).allStates) { - states[state.stateId] = state.renderState(); - policies.push(...state.policyStatements); - } - - return { - stateMachineDefinition: { - StartAt: this.startState.stateId, - States: states - }, - policyStatements: policies - }; - } - - public defaultRetry(retry: RetryProps = {}): IStateChain { - for (const state of this.allStates) { - state.addRetry(retry); - } - return this; - } - - public absorb(other: IStateChain) { - const sdm = accessChainInternals(other); - for (const state of sdm.allStates) { - this.allStates.add(state); - } - } - - private clone(): StateChain { - const ret = new StateChain(this.startState); - ret.allStates = new Set(this.allStates); - ret.activeStates = new Set(this.activeStates); - return ret; - } -} - -/** - * Access private parts of the state machine definition - * - * Parts that we don't want to show to the consumers because they'll - * only distract, but parts that other states need to achieve their - * work. - */ -export function accessChainInternals(x: IStateChain): StateChain { - return x as StateChain; -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/chain.ts b/packages/@aws-cdk/aws-stepfunctions/lib/chain.ts new file mode 100644 index 0000000000000..957f67ed384e5 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/chain.ts @@ -0,0 +1,70 @@ +import { Parallel, ParallelProps } from "./states/parallel"; +import { State } from "./states/state"; +import { IChainable, INextable } from "./types"; + +/** + * A collection of states to chain onto + * + * A Chain has a start and zero or more chainable ends. If there are + * zero ends, calling next() on the Chain will fail. + */ +export class Chain implements IChainable { + /** + * Begin a new Chain from one chainable + */ + public static start(state: IChainable) { + return new Chain(state.startState, state.endStates, state); + } + + /** + * Make a Chain with the start from one chain and the ends from another + */ + public static sequence(start: IChainable, next: IChainable) { + return new Chain(start.startState, next.endStates, next); + } + + /** + * Make a Chain with specific start and end states, and a last-added Chainable + */ + public static custom(startState: State, endStates: INextable[], lastAdded: IChainable) { + return new Chain(startState, endStates, lastAdded); + } + + /** + * Identify this Chain + */ + public readonly id: string; + + private constructor(public readonly startState: State, public readonly endStates: INextable[], private readonly lastAdded: IChainable) { + this.id = lastAdded.id; + } + + /** + * Continue normal execution with the given state + */ + public next(next: IChainable): Chain { + if (this.endStates.length === 0) { + throw new Error(`Cannot add to chain: last state in chain (${this.lastAdded.id}) does not allow it`); + } + + for (const endState of this.endStates) { + endState.next(next); + } + + return new Chain(this.startState, next.endStates, next); + } + + /** + * Return a single state that encompasses all states in the chain + * + * This can be used to add error handling to a sequence of states. + * + * Be aware that this changes the result of the inner state machine + * to be an array with the result of the state machine in it. Adjust + * your paths accordingly. For example, change 'outputPath' to + * '$[0]'. + */ + public asSingleState(id: string, props: ParallelProps = {}): Parallel { + return new Parallel(this.startState, id, props).branch(this); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/asl-condition.ts b/packages/@aws-cdk/aws-stepfunctions/lib/condition.ts similarity index 66% rename from packages/@aws-cdk/aws-stepfunctions/lib/asl-condition.ts rename to packages/@aws-cdk/aws-stepfunctions/lib/condition.ts index dfa23b7bd7c54..d7b1d80dad20a 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/asl-condition.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/condition.ts @@ -1,87 +1,149 @@ +/** + * A Condition for use in a Choice state branch + */ export abstract class Condition { - public static parse(_expression: string): Condition { - throw new Error('Parsing not implemented yet!'); - } - + /** + * Matches if a boolean field has the given value + */ public static booleanEquals(variable: string, value: boolean): Condition { return new VariableComparison(variable, ComparisonOperator.BooleanEquals, value); } + /** + * Matches if a string field has the given value + */ public static stringEquals(variable: string, value: string): Condition { return new VariableComparison(variable, ComparisonOperator.StringEquals, value); } + /** + * Matches if a string field sorts before a given value + */ public static stringLessThan(variable: string, value: string): Condition { return new VariableComparison(variable, ComparisonOperator.StringLessThan, value); } + /** + * Matches if a string field sorts equal to or before a given value + */ public static stringLessThanEquals(variable: string, value: string): Condition { return new VariableComparison(variable, ComparisonOperator.StringLessThanEquals, value); } + /** + * Matches if a string field sorts after a given value + */ public static stringGreaterThan(variable: string, value: string): Condition { return new VariableComparison(variable, ComparisonOperator.StringGreaterThan, value); } + /** + * Matches if a string field sorts after or equal to a given value + */ public static stringGreaterThanEquals(variable: string, value: string): Condition { return new VariableComparison(variable, ComparisonOperator.StringGreaterThanEquals, value); } - public static numericEquals(variable: string, value: number): Condition { + /** + * Matches if a numeric field has the given value + */ + public static numberEquals(variable: string, value: number): Condition { return new VariableComparison(variable, ComparisonOperator.NumericEquals, value); } - public static numericLessThan(variable: string, value: number): Condition { + /** + * Matches if a numeric field is less than the given value + */ + public static numberLessThan(variable: string, value: number): Condition { return new VariableComparison(variable, ComparisonOperator.NumericLessThan, value); } - public static numericLessThanEquals(variable: string, value: number): Condition { + /** + * Matches if a numeric field is less than or equal to the given value + */ + public static numberLessThanEquals(variable: string, value: number): Condition { return new VariableComparison(variable, ComparisonOperator.NumericLessThanEquals, value); } - public static numericGreaterThan(variable: string, value: number): Condition { + /** + * Matches if a numeric field is greater than the given value + */ + public static numberGreaterThan(variable: string, value: number): Condition { return new VariableComparison(variable, ComparisonOperator.NumericGreaterThan, value); } - public static numericGreaterThanEquals(variable: string, value: number): Condition { + /** + * Matches if a numeric field is greater than or equal to the given value + */ + public static numberGreaterThanEquals(variable: string, value: number): Condition { return new VariableComparison(variable, ComparisonOperator.NumericGreaterThanEquals, value); } + /** + * Matches if a timestamp field is the same time as the given timestamp + */ public static timestampEquals(variable: string, value: string): Condition { return new VariableComparison(variable, ComparisonOperator.TimestampEquals, value); } + /** + * Matches if a timestamp field is before the given timestamp + */ public static timestampLessThan(variable: string, value: string): Condition { return new VariableComparison(variable, ComparisonOperator.TimestampLessThan, value); } + /** + * Matches if a timestamp field is before or equal to the given timestamp + */ public static timestampLessThanEquals(variable: string, value: string): Condition { return new VariableComparison(variable, ComparisonOperator.TimestampLessThanEquals, value); } + /** + * Matches if a timestamp field is after the given timestamp + */ public static timestampGreaterThan(variable: string, value: string): Condition { return new VariableComparison(variable, ComparisonOperator.TimestampGreaterThan, value); } + /** + * Matches if a timestamp field is after or equal to the given timestamp + */ public static timestampGreaterThanEquals(variable: string, value: string): Condition { return new VariableComparison(variable, ComparisonOperator.TimestampGreaterThanEquals, value); } + /** + * Combine two or more conditions with a logical AND + */ public static and(...conditions: Condition[]): Condition { return new CompoundCondition(CompoundOperator.And, ...conditions); } + /** + * Combine two or more conditions with a logical OR + */ public static or(...conditions: Condition[]): Condition { return new CompoundCondition(CompoundOperator.Or, ...conditions); } + /** + * Negate a condition + */ public static not(condition: Condition): Condition { return new NotCondition(condition); } + /** + * Render Amazon States Language JSON for the condition + */ public abstract renderCondition(): any; } +/** + * Comparison Operator types + */ enum ComparisonOperator { StringEquals, StringLessThan, @@ -101,14 +163,23 @@ enum ComparisonOperator { TimestampGreaterThanEquals, } +/** + * Compound Operator types + */ enum CompoundOperator { And, Or, } +/** + * Scalar comparison + */ class VariableComparison extends Condition { constructor(private readonly variable: string, private readonly comparisonOperator: ComparisonOperator, private readonly value: any) { super(); + if (!variable.startsWith('$.')) { + throw new Error(`Variable reference must start with '$.', got '${variable}'`); + } } public renderCondition(): any { @@ -119,6 +190,9 @@ class VariableComparison extends Condition { } } +/** + * Logical compound condition + */ class CompoundCondition extends Condition { private readonly conditions: Condition[]; @@ -137,6 +211,9 @@ class CompoundCondition extends Condition { } } +/** + * Logical unary condition + */ class NotCondition extends Condition { constructor(private readonly comparisonOperation: Condition) { super(); diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions/lib/index.ts index f1f8301c6e751..8cf9837545852 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/index.ts @@ -1,15 +1,16 @@ export * from './activity'; -export * from './asl-external-api'; -export * from './asl-internal-api'; -export * from './asl-condition'; +export * from './types'; +export * from './condition'; export * from './state-machine'; +export * from './state-machine-fragment'; export * from './state-transition-metrics'; +export * from './chain'; +export * from './state-graph'; export * from './states/choice'; export * from './states/fail'; export * from './states/parallel'; export * from './states/pass'; -export * from './states/state-machine-fragment'; export * from './states/state'; export * from './states/succeed'; export * from './states/task'; diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-graph.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-graph.ts new file mode 100644 index 0000000000000..62b8cd4654a5a --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-graph.ts @@ -0,0 +1,157 @@ +import cdk = require('@aws-cdk/cdk'); +import { State } from "./states/state"; + +/** + * A collection of connected states + * + * A StateGraph is used to keep track of all states that are connected (have + * transitions between them). It does not include the substatemachines in + * a Parallel's branches: those are their own StateGraphs, but the graphs + * themselves have a hierarchical relationship as well. + * + * By assigning states to a definintive StateGraph, we verify that no state + * machines are constructed. In particular: + * + * - Every state object can only ever be in 1 StateGraph, and not inadvertently + * be used in two graphs. + * - Every stateId must be unique across all states in the entire state + * machine. + * + * All policy statements in all states in all substatemachines are bubbled so + * that the top-level StateMachine instantiation can read them all and add + * them to the IAM Role. + * + * You do not need to instantiate this class; it is used internally. + */ +export class StateGraph { + /** + * Set a timeout to render into the graph JSON. + * + * Read/write. Only makes sense on the top-level graph, subgraphs + * do not support this feature. + * + * @default No timeout + */ + public timeoutSeconds?: number; + + /** + * The accumulated policy statements + */ + public readonly policyStatements = new Array(); + + /** + * All states in this graph + */ + private readonly allStates = new Set(); + + /** + * A mapping of stateId -> Graph for all states in this graph and subgraphs + */ + private readonly allContainedStates = new Map(); + + /** + * Containing graph of this graph + */ + private superGraph?: StateGraph; + + constructor(public readonly startState: State, private readonly graphDescription: string) { + this.allStates.add(startState); + startState.bindToGraph(this); + } + + /** + * Register a state as part of this graph + * + * Called by State.bindToGraph(). + */ + public registerState(state: State) { + this.registerContainedState(state.stateId, this); + this.allStates.add(state); + } + + /** + * Register a Policy Statement used by states in this graph + */ + public registerPolicyStatement(statement: cdk.PolicyStatement) { + if (this.superGraph) { + this.superGraph.registerPolicyStatement(statement); + } else { + this.policyStatements.push(statement); + } + } + + /** + * Register this graph as a child of the given graph + * + * Resource changes will be bubbled up to the given graph. + */ + public registerSuperGraph(graph: StateGraph) { + if (this.superGraph === graph) { return; } + if (this.superGraph) { + throw new Error('Every StateGraph can only be registered into one other StateGraph'); + } + this.superGraph = graph; + this.pushContainedStatesUp(graph); + this.pushPolicyStatementsUp(graph); + } + + /** + * Return the Amazon States Language JSON for this graph + */ + public toGraphJson(): object { + const states: any = {}; + for (const state of this.allStates) { + states[state.stateId] = state.toStateJson(); + } + + return { + StartAt: this.startState.stateId, + States: states, + TimeoutSeconds: this.timeoutSeconds + }; + } + + /** + * Return a string description of this graph + */ + public toString() { + const someNodes = Array.from(this.allStates).slice(0, 3).map(x => x.stateId); + if (this.allStates.size > 3) { someNodes.push('...'); } + return `${this.graphDescription} (${someNodes.join(', ')})`; + } + + /** + * Register a stateId and graph where it was registered + */ + private registerContainedState(stateId: string, graph: StateGraph) { + if (this.superGraph) { + this.superGraph.registerContainedState(stateId, graph); + } else { + const existingGraph = this.allContainedStates.get(stateId); + if (existingGraph) { + throw new Error(`State with name '${stateId}' occurs in both ${graph} and ${existingGraph}. All states must have unique names.`); + } + + this.allContainedStates.set(stateId, graph); + } + } + + /** + * Push all contained state info up to the given super graph + */ + private pushContainedStatesUp(superGraph: StateGraph) { + for (const [stateId, graph] of this.allContainedStates) { + superGraph.registerContainedState(stateId, graph); + } + } + + /** + * Push all policy statements to into the given super graph + */ + private pushPolicyStatementsUp(superGraph: StateGraph) { + for (const policyStatement of this.policyStatements) { + superGraph.registerPolicyStatement(policyStatement); + } + } + +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine-fragment.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine-fragment.ts new file mode 100644 index 0000000000000..b6b8d37062d4e --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine-fragment.ts @@ -0,0 +1,81 @@ +import cdk = require('@aws-cdk/cdk'); +import { Chain } from './chain'; +import { Parallel, ParallelProps } from './states/parallel'; +import { State } from './states/state'; +import { IChainable, INextable } from "./types"; + +/** + * Base class for reusable state machine fragments + */ +export abstract class StateMachineFragment extends cdk.Construct implements IChainable { + /** + * The start state of this state machine fragment + */ + public abstract readonly startState: State; + + /** + * The states to chain onto if this fragment is used + */ + public abstract readonly endStates: INextable[]; + + constructor(parent: cdk.Construct, id: string) { + super(parent, id); + } + + /** + * Prefix the IDs of all states in this state machine fragment + * + * Use this to avoid multiple copies of the state machine all having the + * same state IDs. + * + * @param prefix The prefix to add. Will use construct ID by default. + */ + public prefixStates(prefix?: string): StateMachineFragment { + State.prefixStates(this, prefix || `${this.id}: `); + return this; + } + + /** + * Wrap all states in this state machine fragment up into a single state. + * + * This can be used to add retry or error handling onto this state + * machine fragment. + * + * Be aware that this changes the result of the inner state machine + * to be an array with the result of the state machine in it. Adjust + * your paths accordingly. For example, change 'outputPath' to + * '$[0]'. + */ + public asSingleState(options: SingleStateOptions = {}): Parallel { + const stateId = options.stateId || this.id; + this.prefixStates(options.prefixStates || `${stateId}: `); + + return new Parallel(this, stateId, options).branch(this); + } + + /** + * Continue normal execution with the given state + */ + public next(next: IChainable) { + return Chain.start(this).next(next); + } +} + +/** + * Options for creating a single state + */ +export interface SingleStateOptions extends ParallelProps { + /** + * ID of newly created containing state + * + * @default Construct ID of the StateMachineFragment + */ + stateId?: string; + + /** + * String to prefix all stateIds in the state machine with + * + * @default stateId + */ + prefixStates?: string; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts index cfe050e525ba4..020a4e21a5a8e 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts @@ -2,9 +2,13 @@ import cloudwatch = require('@aws-cdk/aws-cloudwatch'); import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); -import { IChainable } from './asl-external-api'; +import { StateGraph } from './state-graph'; import { cloudformation } from './stepfunctions.generated'; +import { IChainable } from './types'; +/** + * Properties for defining a State Machine + */ export interface StateMachineProps { /** * A name for the state machine @@ -25,8 +29,12 @@ export interface StateMachineProps { */ role?: iam.Role; + /** + * Maximum run time for this state machine + * + * @default No timeout + */ timeoutSeconds?: number; - } /** @@ -47,18 +55,16 @@ export class StateMachine extends cdk.Construct { assumedBy: new cdk.ServicePrincipal(new cdk.FnConcat('states.', new cdk.AwsRegion(), '.amazonaws.com').toString()), }); - const rendered = props.definition.toStateChain().renderStateMachine(); - if (props.timeoutSeconds !== undefined) { - rendered.stateMachineDefinition.TimeoutSeconds = props.timeoutSeconds; - } + const graph = new StateGraph(props.definition.startState, `State Machine ${id} definition`); + graph.timeoutSeconds = props.timeoutSeconds; const resource = new cloudformation.StateMachineResource(this, 'Resource', { stateMachineName: props.stateMachineName, roleArn: this.role.roleArn, - definitionString: cdk.CloudFormationJSON.stringify(rendered.stateMachineDefinition), + definitionString: cdk.CloudFormationJSON.stringify(graph.toGraphJson()), }); - for (const statement of rendered.policyStatements) { + for (const statement of graph.policyStatements) { this.addToRolePolicy(statement); } @@ -66,6 +72,9 @@ export class StateMachine extends cdk.Construct { this.stateMachineArn = resource.stateMachineArn; } + /** + * Add the given statement to the role's policy + */ public addToRolePolicy(statement: cdk.PolicyStatement) { this.role.addToPolicy(statement); } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts index 5b620f5453020..29c7a8910c46e 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts @@ -1,85 +1,136 @@ import cdk = require('@aws-cdk/cdk'); -import { Condition } from '../asl-condition'; -import { CatchProps, IChainable, IStateChain, RetryProps } from '../asl-external-api'; -import { IInternalState, StateType, TransitionType } from '../asl-internal-api'; -import { StateChain } from '../asl-state-chain'; -import { State } from './state'; +import { Chain } from '../chain'; +import { Condition } from '../condition'; +import { IChainable, INextable } from '../types'; +import { State, StateType } from './state'; +/** + * Properties for defining a Choice state + */ export interface ChoiceProps { + /** + * An optional description for this state + * + * @default No comment + */ comment?: string; + + /** + * JSONPath expression to select part of the state to be the input to this state. + * + * May also be the special value DISCARD, which will cause the effective + * input to be the empty object {}. + * + * @default $ + */ inputPath?: string; + + /** + * JSONPath expression to select part of the state to be the output to this state. + * + * May also be the special value DISCARD, which will cause the effective + * output to be the empty object {}. + * + * @default $ + */ outputPath?: string; } +/** + * Define a Choice in the state machine + * + * A choice state can be used to make decisions based on the execution + * state. + */ export class Choice extends State { - private static Internals = class implements IInternalState { - public readonly canHaveCatch = false; - public readonly hasOpenNextTransition = false; - public readonly stateId: string; - public readonly policyStatements = new Array(); - - constructor(private readonly choice: Choice) { - this.stateId = choice.stateId; - } - - public renderState() { - return { - ...this.choice.renderBaseState(), - ...this.choice.transitions.renderList(TransitionType.Choice), - ...this.choice.transitions.renderSingle(TransitionType.Default), - }; - } - - public addNext(_targetState: IStateChain): void { - throw new Error("Cannot chain onto a Choice state. Use the state's .on() or .otherwise() instead."); - } - - public addCatch(_targetState: IStateChain, _props?: CatchProps): void { - throw new Error("Cannot catch errors on a Choice."); - } - - public accessibleChains() { - return this.choice.accessibleStates(); - } - - public addRetry(_retry?: RetryProps): void { - // Nothing - } - }; + public readonly endStates: INextable[] = []; constructor(parent: cdk.Construct, id: string, props: ChoiceProps = {}) { - super(parent, id, { - Type: StateType.Choice, - InputPath: props.inputPath, - OutputPath: props.outputPath, - Comment: props.comment, - }); + super(parent, id, props); } + /** + * If the given condition matches, continue execution with the given state + */ public on(condition: Condition, next: IChainable): Choice { - this.transitions.add(TransitionType.Choice, next.toStateChain(), condition.renderCondition()); + super.addChoice(condition, next.startState); return this; } - public otherwise(next: IChainable): Choice { - // We use the "next" transition to store the Default, even though the meaning is different. - if (this.transitions.has(TransitionType.Default)) { - throw new Error('Can only have one Default transition'); - } - this.transitions.add(TransitionType.Default, next.toStateChain()); + /** + * If none of the given conditions match, continue execution with the given state + * + * If no conditions match and no otherwise() has been given, an execution + * error will be raised. + */ + public otherwise(def: IChainable): Choice { + super.makeDefault(def.startState); return this; } - public toStateChain(): IStateChain { - const chain = new StateChain(new Choice.Internals(this)); - for (const transition of this.transitions.all()) { - chain.absorb(transition.targetChain); + /** + * Return a Chain that contains all reachable end states from this Choice + * + * Use this to combine all possible choice paths back. + */ + public afterwards(options: AfterwardsOptions = {}): Chain { + const endStates = State.filterNextables(State.findReachableEndStates(this, { includeErrorHandlers: options.includeErrorHandlers })); + if (options.includeOtherwise && this.defaultChoice) { + throw new Error(`'includeOtherwise' set but Choice state ${this.stateId} already has an 'otherwise' transition`); } - - return chain; + if (options.includeOtherwise) { + endStates.push(new DefaultAsNext(this)); + } + return Chain.custom(this, endStates, this); } - public closure(): IStateChain { - return this.toStateChain().closure(); + /** + * Return the Amazon States Language object for this state + */ + public toStateJson(): object { + return { + Type: StateType.Choice, + Comment: this.comment, + ...this.renderInputOutput(), + ...this.renderChoices(), + }; } } + +/** + * Options for selecting the choice paths + */ +export interface AfterwardsOptions { + /** + * Whether to include error handling states + * + * If this is true, all states which are error handlers (added through 'onError') + * and states reachable via error handlers will be included as well. + * + * @default false + */ + includeErrorHandlers?: boolean; + + /** + * Whether to include the default/otherwise transition for the current Choice state + * + * If this is true and the current Choice does not have a default outgoing + * transition, one will be added included when .next() is called on the chain. + * + * @default false + */ + includeOtherwise?: boolean; +} + +/** + * Adapter to make the .otherwise() transition settable through .next() + */ +class DefaultAsNext implements INextable { + constructor(private readonly choice: Choice) { + } + + public next(state: IChainable): Chain { + this.choice.otherwise(state); + return Chain.sequence(this.choice, state); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/fail.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/fail.ts index ec1d0800ef1f2..4dd5cbd5fd261 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/fail.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/fail.ts @@ -1,57 +1,58 @@ import cdk = require('@aws-cdk/cdk'); -import { CatchProps, IStateChain, RetryProps } from '../asl-external-api'; -import { IInternalState, StateType } from '../asl-internal-api'; -import { StateChain } from '../asl-state-chain'; -import { State } from './state'; +import { INextable } from '../types'; +import { State, StateType } from './state'; +/** + * Properties for defining a Fail state + */ export interface FailProps { - error: string; - cause: string; + /** + * An optional description for this state + * + * @default No comment + */ comment?: string; -} - -export class Fail extends State { - private static Internals = class implements IInternalState { - public readonly canHaveCatch = false; - public readonly hasOpenNextTransition = false; - public readonly stateId: string; - public readonly policyStatements = new Array(); - - constructor(private readonly fail: Fail) { - this.stateId = fail.stateId; - } - - public renderState() { - return this.fail.renderBaseState(); - } - public addNext(_targetState: IStateChain): void { - throw new Error("Cannot chain onto a Fail state. This ends the state machine."); - } + /** + * Error code used to represent this failure + */ + error: string; - public addCatch(_targetState: IStateChain, _props?: CatchProps): void { - throw new Error("Cannot catch errors on a Fail."); - } + /** + * A description for the cause of the failure + * + * @default No description + */ + cause?: string; +} - public accessibleChains() { - return this.fail.accessibleStates(); - } +/** + * Define a Fail state in the state machine + * + * Reaching a Fail state terminates the state execution in failure. + */ +export class Fail extends State { + public readonly endStates: INextable[] = []; - public addRetry(_retry?: RetryProps): void { - // Nothing - } - }; + private readonly error: string; + private readonly cause?: string; constructor(parent: cdk.Construct, id: string, props: FailProps) { - super(parent, id, { - Type: StateType.Fail, - Error: props.error, - Cause: props.cause, - Comment: props.comment, - }); + super(parent, id, props); + + this.error = props.error; + this.cause = props.cause; } - public toStateChain(): IStateChain { - return new StateChain(new Fail.Internals(this)); + /** + * Return the Amazon States Language object for this state + */ + public toStateJson(): object { + return { + Type: StateType.Fail, + Comment: this.comment, + Error: this.error, + Cause: this.cause, + }; } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts index dcebcaf4feb8c..b79ab314a8f52 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts @@ -1,105 +1,129 @@ import cdk = require('@aws-cdk/cdk'); -import { CatchProps, Errors, IChainable, IStateChain, RetryProps } from '../asl-external-api'; -import { IInternalState, StateType, TransitionType } from '../asl-internal-api'; -import { StateChain } from '../asl-state-chain'; -import { State } from './state'; -import { renderRetries } from './util'; - +import { Chain } from '../chain'; +import { StateGraph } from '../state-graph'; +import { CatchProps, IChainable, INextable, RetryProps } from '../types'; +import { renderJsonPath, State, StateType } from './state'; + +/** + * Properties for defining a Parallel state + */ export interface ParallelProps { - inputPath?: string; - outputPath?: string; - resultPath?: string; + /** + * An optional description for this state + * + * @default No comment + */ comment?: string; -} -export class Parallel extends State { - private static Internals = class implements IInternalState { - public readonly canHaveCatch = true; - public readonly stateId: string; - - constructor(private readonly parallel: Parallel) { - this.stateId = parallel.stateId; - } - - public renderState() { - return { - ...this.parallel.renderBaseState(), - ...renderRetries(this.parallel.retries), - ...this.parallel.transitions.renderSingle(TransitionType.Next, { End: true }), - ...this.parallel.transitions.renderList(TransitionType.Catch), - }; - } - - public addNext(targetState: IStateChain): void { - this.parallel.addNextTransition(targetState); - } - - public addCatch(targetState: IStateChain, props: CatchProps = {}): void { - this.parallel.transitions.add(TransitionType.Catch, targetState, { - ErrorEquals: props.errors ? props.errors : [Errors.all], - ResultPath: props.resultPath - }); - } - - public addRetry(retry?: RetryProps): void { - this.parallel.retry(retry); - } - - public accessibleChains() { - return this.parallel.accessibleStates(); - } + /** + * JSONPath expression to select part of the state to be the input to this state. + * + * May also be the special value DISCARD, which will cause the effective + * input to be the empty object {}. + * + * @default $ + */ + inputPath?: string; - public get hasOpenNextTransition(): boolean { - return !this.parallel.hasNextTransition; - } + /** + * JSONPath expression to select part of the state to be the output to this state. + * + * May also be the special value DISCARD, which will cause the effective + * output to be the empty object {}. + * + * @default $ + */ + outputPath?: string; - public get policyStatements(): cdk.PolicyStatement[] { - const ret = new Array(); - for (const branch of this.parallel.branches) { - ret.push(...branch.toStateChain().renderStateMachine().policyStatements); - } - return ret; - } - }; + /** + * JSONPath expression to indicate where to inject the state's output + * + * May also be the special value DISCARD, which will cause the state's + * input to become its output. + * + * @default $ + */ + resultPath?: string; +} - private readonly branches: IChainable[] = []; - private readonly retries = new Array(); +/** + * Define a Parallel state in the state machine + * + * A Parallel state can be used to run one or more state machines at the same + * time. + * + * The Result of a Parallel state is an array of the results of its substatemachines. + */ +export class Parallel extends State implements INextable { + public readonly endStates: INextable[]; constructor(parent: cdk.Construct, id: string, props: ParallelProps = {}) { - super(parent, id, { - Type: StateType.Parallel, - InputPath: props.inputPath, - OutputPath: props.outputPath, - ResultPath: props.resultPath, - Comment: props.comment, - // Lazy because the states are mutable and they might get chained onto - // (Users shouldn't, but they might) - Branches: new cdk.Token(() => this.branches.map(b => b.toStateChain().renderStateMachine().stateMachineDefinition)) - }); + super(parent, id, props); + + this.endStates = [this]; } - public branch(definition: IChainable): Parallel { - this.branches.push(definition); + /** + * Add retry configuration for this state + * + * This controls if and how the execution will be retried if a particular + * error occurs. + */ + public retry(props: RetryProps = {}): Parallel { + super.addRetry(props); return this; } - public retry(props: RetryProps = {}): Parallel { - if (!props.errors) { - props.errors = [Errors.all]; - } - this.retries.push(props); + /** + * Add a recovery handler for this state + * + * When a particular error occurs, execution will continue at the error + * handler instead of failing the state machine execution. + */ + public onError(handler: IChainable, props: CatchProps = {}): Parallel { + super.addCatch(handler.startState, props); return this; } - public next(sm: IChainable): IStateChain { - return this.toStateChain().next(sm); + /** + * Continue normal execution with the given state + */ + public next(next: IChainable): Chain { + super.makeNext(next.startState); + return Chain.sequence(this, next); + } + + /** + * Define a branch to run along all other branches + */ + public branch(branch: IChainable): Parallel { + const name = `Parallel '${this.stateId}' branch ${this.branches.length + 1}`; + super.addBranch(new StateGraph(branch.startState, name)); + return this; } - public onError(handler: IChainable, props: CatchProps = {}): IStateChain { - return this.toStateChain().onError(handler, props); + /** + * Validate this state + */ + public validate(): string[] { + if (this.branches.length === 0) { + return ['Parallel must have at least one branch']; + } + return []; } - public toStateChain(): IStateChain { - return new StateChain(new Parallel.Internals(this)); + /** + * Return the Amazon States Language object for this state + */ + public toStateJson(): object { + return { + Type: StateType.Parallel, + Comment: this.comment, + ResultPath: renderJsonPath(this.resultPath), + ...this.renderNextEnd(), + ...this.renderInputOutput(), + ...this.renderRetryCatch(), + ...this.renderBranches(), + }; } } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts index acfdc9c53160d..65ab9561ca742 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts @@ -1,71 +1,95 @@ import cdk = require('@aws-cdk/cdk'); -import { CatchProps, IChainable, IStateChain, RetryProps } from '../asl-external-api'; -import { IInternalState, StateType, TransitionType } from '../asl-internal-api'; -import { StateChain } from '../asl-state-chain'; -import { State } from './state'; +import { Chain } from '../chain'; +import { IChainable, INextable } from '../types'; +import { renderJsonPath, State, StateType } from './state'; +/** + * Properties for defining a Pass state + */ export interface PassProps { - inputPath?: string; - outputPath?: string; - resultPath?: string; + /** + * An optional description for this state + * + * @default No comment + */ comment?: string; - result?: any; -} - -export class Pass extends State { - private static Internals = class implements IInternalState { - public readonly canHaveCatch = false; - public readonly stateId: string; - public readonly policyStatements = new Array(); - constructor(private readonly pass: Pass) { - this.stateId = this.pass.stateId; - } - - public renderState() { - return { - ...this.pass.renderBaseState(), - ...this.pass.transitions.renderSingle(TransitionType.Next, { End: true }), - }; - } + /** + * JSONPath expression to select part of the state to be the input to this state. + * + * May also be the special value DISCARD, which will cause the effective + * input to be the empty object {}. + * + * @default $ + */ + inputPath?: string; - public addNext(targetState: IStateChain): void { - this.pass.addNextTransition(targetState); - } + /** + * JSONPath expression to select part of the state to be the output to this state. + * + * May also be the special value DISCARD, which will cause the effective + * output to be the empty object {}. + * + * @default $ + */ + outputPath?: string; - public addCatch(_targetState: IStateChain, _props?: CatchProps): void { - throw new Error("Cannot catch errors on a Pass."); - } + /** + * JSONPath expression to indicate where to inject the state's output + * + * May also be the special value DISCARD, which will cause the state's + * input to become its output. + * + * @default $ + */ + resultPath?: string; - public addRetry(_retry?: RetryProps): void { - // Nothing - } + /** + * If given, treat as the result of this operation + * + * Can be used to inject or replace the current execution state. + * + * @default No injected result + */ + result?: any; +} - public accessibleChains() { - return this.pass.accessibleStates(); - } +/** + * Define a Pass in the state machine + * + * A Pass state can be used to transform the current exeuction's state. + */ +export class Pass extends State implements INextable { + public readonly endStates: INextable[]; - public get hasOpenNextTransition(): boolean { - return !this.pass.hasNextTransition; - } - }; + private readonly result?: any; constructor(parent: cdk.Construct, id: string, props: PassProps = {}) { - super(parent, id, { - Type: StateType.Pass, - InputPath: props.inputPath, - OutputPath: props.outputPath, - ResultPath: props.resultPath, - Comment: props.comment, - Result: props.result, - }); + super(parent, id, props); + + this.result = props.result; + this.endStates = [this]; } - public next(sm: IChainable): IStateChain { - return this.toStateChain().next(sm); + /** + * Continue normal execution with the given state + */ + public next(next: IChainable): Chain { + super.makeNext(next.startState); + return Chain.sequence(this, next); } - public toStateChain(): IStateChain { - return new StateChain(new Pass.Internals(this)); + /** + * Return the Amazon States Language object for this state + */ + public toStateJson(): object { + return { + Type: StateType.Pass, + Comment: this.comment, + Result: this.result, + ResultPath: renderJsonPath(this.resultPath), + ...this.renderInputOutput(), + ...this.renderNextEnd(), + }; } } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/state-machine-fragment.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/state-machine-fragment.ts deleted file mode 100644 index 15d6544a07ead..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/state-machine-fragment.ts +++ /dev/null @@ -1,72 +0,0 @@ -import cdk = require('@aws-cdk/cdk'); -import { IChainable, IStateChain } from "../asl-external-api"; - -export interface StateMachineFragmentProps { - /** - * Whether to add the fragment name to the states defined within - * - * @default true - */ - scopeStateNames?: boolean; -} - -export class StateMachineFragment extends cdk.Construct implements IChainable { - /** - * Used to find this Construct back in the construct tree - */ - public readonly isStateMachine = true; - - public readonly scopeStateNames: boolean; - - private _startState?: IChainable; - private chain?: IStateChain; - - constructor(parent: cdk.Construct, id: string, props: StateMachineFragmentProps = {}) { - super(parent, id); - this.scopeStateNames = props.scopeStateNames !== undefined ? props.scopeStateNames : true; - } - - /** - * Explicitly set a start state - */ - public start(state: IChainable): IStateChain { - this._startState = state; - return state.toStateChain(); - } - - public toStateChain(): IStateChain { - this.freeze(); - return this.chain!; - } - - public next(sm: IChainable): IStateChain { - return this.toStateChain().next(sm); - } - - protected freeze() { - if (this.chain === undefined) { - // If we're converting a state machine definition to a state chain, grab the whole of it. - // We need to cache this value; because of the .closure(), it may change - // depending on whether states get chained onto the states in this fragment. - this.chain = this.startState.toStateChain().closure(); - } - } - - private get startState(): IChainable { - if (this._startState) { - return this._startState; - } - - // If no explicit start state given, find the first child that is a state - const firstStateChild = this.children.find(isChainable); - if (!isChainable(firstStateChild)) { - throw new Error('State Machine definition does not contain any states'); - } - - return firstStateChild as IChainable; - } -} - -function isChainable(x: any): x is IChainable { - return x && x.toStateChain !== undefined; -} diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts index 3b751299d96ae..19d957147ba42 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts @@ -1,45 +1,487 @@ import cdk = require('@aws-cdk/cdk'); -import { IChainable, IStateChain } from "../asl-external-api"; -import { Transitions, TransitionType } from '../asl-internal-api'; -import { StateMachineFragment } from './state-machine-fragment'; +import { Condition } from '../condition'; +import { StateGraph } from '../state-graph'; +import { CatchProps, DISCARD, Errors, IChainable, INextable, RetryProps } from '../types'; +/** + * Properties shared by all states + */ +export interface StateProps { + /** + * A comment describing this state + * + * @default No comment + */ + comment?: string; + + /** + * JSONPath expression to select part of the state to be the input to this state. + * + * May also be the special value DISCARD, which will cause the effective + * input to be the empty object {}. + * + * @default $ + */ + inputPath?: string; + + /** + * JSONPath expression to select part of the state to be the output to this state. + * + * May also be the special value DISCARD, which will cause the effective + * output to be the empty object {}. + * + * @default $ + */ + outputPath?: string; + + /** + * JSONPath expression to indicate where to inject the state's output + * + * May also be the special value DISCARD, which will cause the state's + * input to become its output. + * + * @default $ + */ + resultPath?: string; +} + +/** + * Base class for all other state classes + */ export abstract class State extends cdk.Construct implements IChainable { - protected readonly transitions = new Transitions(); + /** + * Add a prefix to the stateId of all States found in a construct tree + */ + public static prefixStates(root: cdk.Construct, prefix: string) { + const queue = [root]; + while (queue.length > 0) { + const el = queue.splice(0, 1)[0]!; + if (isPrefixable(el)) { + el.addPrefix(prefix); + } + queue.push(...el.children); + } + } + + /** + * Find the set of end states states reachable through transitions from the given start state + */ + public static findReachableEndStates(start: State, options: FindStateOptions = {}) { + const visited = new Set(); + const ret = new Set(); + const queue = [start]; + while (queue.length > 0) { + const state = queue.splice(0, 1)[0]!; + if (visited.has(state)) { continue; } + visited.add(state); + + const outgoing = state.outgoingTransitions(options); + + if (outgoing.length > 0) { + // We can continue + queue.push(...outgoing); + } else { + // Terminal state + ret.add(state); + } + } + return Array.from(ret); + } + + /** + * Return only the states that allow chaining from an array of states + */ + public static filterNextables(states: State[]): INextable[] { + return states.filter(isNextable) as any; + } + + /** + * First state of this Chainable + */ + public readonly startState: State; + + /** + * Continuable states of this Chainable + */ + public abstract readonly endStates: INextable[]; + + // This class has a superset of most of the features of the other states, + // and the subclasses decide which part of the features to expose. Most + // features are shared by a couple of states, and it becomes cumbersome to + // slice it out across all states. This is not great design, but it is + // pragmatic! + protected readonly comment?: string; + protected readonly inputPath?: string; + protected readonly outputPath?: string; + protected readonly resultPath?: string; + protected readonly branches: StateGraph[] = []; + protected defaultChoice?: State; + protected _next?: State; + + private readonly retries: RetryProps[] = []; + private readonly catches: CatchTransition[] = []; + private readonly choices: ChoiceTransition[] = []; + private readonly prefixes: string[] = []; + + /** + * The graph that this state is part of. + * + * Used for guaranteeing consistency between graphs and graph components. + */ + private containingGraph?: StateGraph; - constructor(parent: cdk.Construct, id: string, private readonly options: any) { + /** + * States with references to this state. + * + * Used for finding complete connected graph that a state is part of. + */ + private readonly incomingStates: State[] = []; + + constructor(parent: cdk.Construct, id: string, props: StateProps) { super(parent, id); + + this.startState = this; + + this.comment = props.comment; + this.inputPath = props.inputPath; + this.outputPath = props.outputPath; + this.resultPath = props.resultPath; } - public abstract toStateChain(): IStateChain; + /** + * Tokenized string that evaluates to the state's ID + */ + public get stateId(): string { + return this.prefixes.concat(this.id).join(''); + } + + /** + * Add a prefix to the stateId of this state + */ + public addPrefix(x: string) { + if (x !== '') { + this.prefixes.splice(0, 0, x); + } + } + + /** + * Register this state as part of the given graph + * + * Don't call this. It will be called automatically when you work + * states normally. + */ + public bindToGraph(graph: StateGraph) { + if (this.containingGraph === graph) { return; } + + if (this.containingGraph) { + // tslint:disable-next-line:max-line-length + throw new Error(`Trying to use state '${this.stateId}' in ${graph}, but is already in ${this.containingGraph}. Every state can only be used in one graph.`); + } + + this.containingGraph = graph; + this.onBindToGraph(graph); - protected renderBaseState(): any { - return this.options; + for (const incoming of this.incomingStates) { + incoming.bindToGraph(graph); + } + for (const outgoing of this.outgoingTransitions({ includeErrorHandlers: true })) { + outgoing.bindToGraph(graph); + } + for (const branch of this.branches) { + branch.registerSuperGraph(this.containingGraph); + } } /** - * Return the name of this state + * Render the state as JSON + */ + public abstract toStateJson(): object; + + /** + * Add a retrier to the retry list of this state */ - protected get stateId(): string { - const parentDefs: cdk.Construct[] = this.ancestors().filter(c => (isStateMachineFragment(c) && c.scopeStateNames) || c === this); - return parentDefs.map(x => x.id).join('/'); + protected addRetry(props: RetryProps = {}) { + this.retries.push({ + ...props, + errors: props.errors ? props.errors : [Errors.All], + }); } - protected accessibleStates(): IStateChain[] { - return this.transitions.all().map(t => t.targetChain); + /** + * Add an error handler to the catch list of this state + */ + protected addCatch(handler: State, props: CatchProps = {}) { + this.catches.push({ + next: handler, + props: { + errors: props.errors ? props.errors : [Errors.All], + resultPath: props.resultPath + } + }); + handler.addIncoming(this); + if (this.containingGraph) { + handler.bindToGraph(this.containingGraph); + } } - protected get hasNextTransition() { - return this.transitions.has(TransitionType.Next); + /** + * Make the indicated state the default transition of this state + */ + protected makeNext(next: State) { + // Can't be called 'setNext' because of JSII + if (this._next) { + throw new Error(`State '${this.id}' already has a next state`); + } + this._next = next; + next.addIncoming(this); + if (this.containingGraph) { + next.bindToGraph(this.containingGraph); + } } - protected addNextTransition(targetState: IStateChain): void { - if (this.hasNextTransition) { - throw new Error(`State ${this.stateId} already has a Next transition`); + /** + * Add a choice branch to this state + */ + protected addChoice(condition: Condition, next: State) { + this.choices.push({ condition, next }); + next.startState.addIncoming(this); + if (this.containingGraph) { + next.startState.bindToGraph(this.containingGraph); } - this.transitions.add(TransitionType.Next, targetState); } + + /** + * Add a paralle branch to this state + */ + protected addBranch(branch: StateGraph) { + this.branches.push(branch); + if (this.containingGraph) { + branch.registerSuperGraph(this.containingGraph); + } + } + + /** + * Make the indicated state the default choice transition of this state + */ + protected makeDefault(def: State) { + // Can't be called 'setDefault' because of JSII + if (this.defaultChoice) { + throw new Error(`Choice '${this.id}' already has a default next state`); + } + this.defaultChoice = def; + } + + /** + * Render the default next state in ASL JSON format + */ + protected renderNextEnd(): any { + if (this._next) { + return { Next: this._next.stateId }; + } else { + return { End: true }; + } + } + + /** + * Render the choices in ASL JSON format + */ + protected renderChoices(): any { + return { + Choices: renderList(this.choices, renderChoice), + Default: this.defaultChoice ? this.defaultChoice.stateId : undefined, + }; + } + + /** + * Render InputPath/OutputPath in ASL JSON format + */ + protected renderInputOutput(): any { + return { + InputPath: renderJsonPath(this.inputPath), + OutputPath: renderJsonPath(this.outputPath), + }; + } + + /** + * Render parallel branches in ASL JSON format + */ + protected renderBranches(): any { + return { + Branches: this.branches.map(b => b.toGraphJson()) + }; + } + + /** + * Render error recovery options in ASL JSON format + */ + protected renderRetryCatch(): any { + return { + Retry: renderList(this.retries, renderRetry), + Catch: renderList(this.catches, renderCatch), + }; + } + + /** + * Called whenever this state is bound to a graph + * + * Can be overridden by subclasses. + */ + protected onBindToGraph(graph: StateGraph) { + graph.registerState(this); + } + + /** + * Add a state to the incoming list + */ + private addIncoming(source: State) { + this.incomingStates.push(source); + } + + /** + * Return all states this state can transition to + */ + private outgoingTransitions(options: FindStateOptions): State[] { + const ret = new Array(); + if (this._next) { ret.push(this._next); } + if (this.defaultChoice) { ret.push(this.defaultChoice); } + for (const c of this.choices) { + ret.push(c.next); + } + if (options.includeErrorHandlers) { + for (const c of this.catches) { + ret.push(c.next); + } + } + return ret; + } +} + +/** + * Options for finding reachable states + */ +export interface FindStateOptions { + /** + * Whether or not to follow error-handling transitions + * + * @default false + */ + includeErrorHandlers?: boolean; +} + +/** + * A Choice Transition + */ +interface ChoiceTransition { + /** + * State to transition to + */ + next: State; + + /** + * Condition for this transition + */ + condition: Condition; +} + +/** + * Render a choice transition + */ +function renderChoice(c: ChoiceTransition) { + return { + ...c.condition.renderCondition(), + Next: c.next.stateId + }; +} + +/** + * A Catch Transition + */ +interface CatchTransition { + /** + * State to transition to + */ + next: State; + + /** + * Additional properties for this transition + */ + props: CatchProps; +} + +/** + * Render a Retry object to ASL + */ +function renderRetry(retry: RetryProps) { + return { + ErrorEquals: retry.errors, + IntervalSeconds: retry.intervalSeconds, + MaxAttempts: retry.maxAttempts, + BackoffRate: retry.backoffRate + }; +} + +/** + * Render a Catch object to ASL + */ +function renderCatch(c: CatchTransition) { + return { + ErrorEquals: c.props.errors, + ResultPath: renderJsonPath(c.props.resultPath), + Next: c.next.stateId, + }; } -function isStateMachineFragment(construct: cdk.Construct): construct is StateMachineFragment { - return (construct as any).isStateMachine; +/** + * Render a list or return undefined for an empty list + */ +export function renderList(xs: T[], fn: (x: T) => any): any { + if (xs.length === 0) { return undefined; } + return xs.map(fn); } + +/** + * Render JSON path, respecting the special value DISCARD + */ +export function renderJsonPath(jsonPath?: string): undefined | null | string { + if (jsonPath === undefined) { return undefined; } + if (jsonPath === DISCARD) { return null; } + + if (!jsonPath.startsWith('$')) { + throw new Error(`Expected JSON path to start with '$', got: ${jsonPath}`); + } + return jsonPath; +} + +/** + * Interface for structural feature testing (to make TypeScript happy) + */ +interface Prefixable { + addPrefix(x: string): void; +} + +/** + * Whether an object is a Prefixable + */ +function isPrefixable(x: any): x is Prefixable { + return typeof(x) === 'object' && x.addPrefix; +} + +/** + * Whether an object is INextable + */ +function isNextable(x: any): x is INextable { + return typeof(x) === 'object' && x.next; +} + +/** + * State types + */ +export enum StateType { + Pass = 'Pass', + Task = 'Task', + Choice = 'Choice', + Wait = 'Wait', + Succeed = 'Succeed', + Fail = 'Fail', + Parallel = 'Parallel' +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts index 14a861ee74f25..f19d63156e99d 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts @@ -1,57 +1,60 @@ import cdk = require('@aws-cdk/cdk'); -import { CatchProps, IStateChain, RetryProps } from '../asl-external-api'; -import { IInternalState, StateType } from '../asl-internal-api'; -import { StateChain } from '../asl-state-chain'; -import { State } from './state'; +import { INextable } from '../types'; +import { State, StateType } from './state'; +/** + * Properties for defining a Succeed state + */ export interface SucceedProps { - inputPath?: string; - outputPath?: string; + /** + * An optional description for this state + * + * @default No comment + */ comment?: string; -} - -export class Succeed extends State { - private static Internals = class implements IInternalState { - public readonly hasOpenNextTransition = false; - public readonly canHaveCatch = false; - public readonly stateId: string; - public readonly policyStatements = new Array(); - - constructor(private readonly succeed: Succeed) { - this.stateId = succeed.stateId; - } - - public renderState() { - return this.succeed.renderBaseState(); - } - public addNext(_targetState: IStateChain): void { - throw new Error("Cannot chain onto a Succeed state; this ends the state machine."); - } + /** + * JSONPath expression to select part of the state to be the input to this state. + * + * May also be the special value DISCARD, which will cause the effective + * input to be the empty object {}. + * + * @default $ + */ + inputPath?: string; - public addCatch(_targetState: IStateChain, _props?: CatchProps): void { - throw new Error("Cannot catch errors on a Succeed."); - } + /** + * JSONPath expression to select part of the state to be the output to this state. + * + * May also be the special value DISCARD, which will cause the effective + * output to be the empty object {}. + * + * @default $ + */ + outputPath?: string; - public addRetry(_retry?: RetryProps): void { - // Nothing - } +} - public accessibleChains() { - return this.succeed.accessibleStates(); - } - }; +/** + * Define a Succeed state in the state machine + * + * Reaching a Succeed state terminates the state execution in success. + */ +export class Succeed extends State { + public readonly endStates: INextable[] = []; constructor(parent: cdk.Construct, id: string, props: SucceedProps = {}) { - super(parent, id, { - Type: StateType.Succeed, - InputPath: props.inputPath, - OutputPath: props.outputPath, - Comment: props.comment, - }); + super(parent, id, props); } - public toStateChain(): IStateChain { - return new StateChain(new Succeed.Internals(this)); + /** + * Return the Amazon States Language object for this state + */ + public toStateJson(): object { + return { + Type: StateType.Succeed, + Comment: this.comment, + ...this.renderInputOutput(), + }; } } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts index 792c7babdd405..84c15b9ef12f4 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts @@ -1,100 +1,145 @@ import cloudwatch = require('@aws-cdk/aws-cloudwatch'); import cdk = require('@aws-cdk/cdk'); -import { CatchProps, Errors, IChainable, IStateChain, RetryProps } from '../asl-external-api'; -import { IInternalState, StateType, TransitionType } from '../asl-internal-api'; -import { StateChain } from '../asl-state-chain'; -import { State } from './state'; -import { renderRetries } from './util'; +import { Chain } from '../chain'; +import { StateGraph } from '../state-graph'; +import { CatchProps, IChainable, INextable, RetryProps } from '../types'; +import { renderJsonPath, State, StateType } from './state'; +/** + * Properties for defining a Task state + */ export interface TaskProps { + /** + * The resource that represents the work to be executed + * + * Can be either a Lambda Function or an Activity. + */ resource: IStepFunctionsTaskResource; - inputPath?: string; - outputPath?: string; - resultPath?: string; - timeoutSeconds?: number; - heartbeatSeconds?: number; - comment?: string; -} - -export class Task extends State { - private static Internals = class implements IInternalState { - public readonly canHaveCatch = true; - public readonly stateId: string; - public readonly policyStatements: cdk.PolicyStatement[]; - constructor(private readonly task: Task) { - this.stateId = task.stateId; - this.policyStatements = task.resourceProps.policyStatements || []; - } - - public renderState() { - return { - ...this.task.renderBaseState(), - ...renderRetries(this.task.retries), - ...this.task.transitions.renderSingle(TransitionType.Next, { End: true }), - ...this.task.transitions.renderList(TransitionType.Catch), - }; - } + /** + * An optional description for this state + * + * @default No comment + */ + comment?: string; - public addNext(targetState: IStateChain): void { - this.task.addNextTransition(targetState); - } + /** + * JSONPath expression to select part of the state to be the input to this state. + * + * May also be the special value DISCARD, which will cause the effective + * input to be the empty object {}. + * + * @default $ + */ + inputPath?: string; - public addCatch(targetState: IStateChain, props: CatchProps = {}): void { - this.task.transitions.add(TransitionType.Catch, targetState, { - ErrorEquals: props.errors ? props.errors : [Errors.all], - ResultPath: props.resultPath - }); - } + /** + * JSONPath expression to select part of the state to be the output to this state. + * + * May also be the special value DISCARD, which will cause the effective + * output to be the empty object {}. + * + * @default $ + */ + outputPath?: string; - public addRetry(retry?: RetryProps): void { - this.task.retry(retry); - } + /** + * JSONPath expression to indicate where to inject the state's output + * + * May also be the special value DISCARD, which will cause the state's + * input to become its output. + * + * @default $ + */ + resultPath?: string; - public accessibleChains() { - return this.task.accessibleStates(); - } + /** + * Maximum run time of this state + * + * If the state takes longer than this amount of time to complete, a 'Timeout' error is raised. + * + * @default 60 + */ + timeoutSeconds?: number; - public get hasOpenNextTransition(): boolean { - return !this.task.hasNextTransition; - } - }; + /** + * Maximum time between heart beats + * + * If the time between heart beats takes longer than this, a 'Timeout' error is raised. + * + * This is only relevant when using an Activity type as resource. + * + * @default No heart beat timeout + */ + heartbeatSeconds?: number; +} +/** + * Define a Task state in the state machine + * + * Reaching a Task state causes some work to be executed, represented + * by the Task's resource property. + */ +export class Task extends State implements INextable { + public readonly endStates: INextable[]; private readonly resourceProps: StepFunctionsTaskResourceProps; - private readonly retries = new Array(); + private readonly timeoutSeconds?: number; + private readonly heartbeatSeconds?: number; constructor(parent: cdk.Construct, id: string, props: TaskProps) { - super(parent, id, { - Type: StateType.Task, - InputPath: props.inputPath, - OutputPath: props.outputPath, - Resource: new cdk.Token(() => this.resourceProps.resourceArn), - ResultPath: props.resultPath, - TimeoutSeconds: props.timeoutSeconds, - HeartbeatSeconds: props.heartbeatSeconds, - Comment: props.comment, - }); + super(parent, id, props); + + this.timeoutSeconds = props.timeoutSeconds; + this.heartbeatSeconds = props.heartbeatSeconds; this.resourceProps = props.resource.asStepFunctionsTaskResource(this); + this.endStates = [this]; } - public next(sm: IChainable): IStateChain { - return this.toStateChain().next(sm); + /** + * Add retry configuration for this state + * + * This controls if and how the execution will be retried if a particular + * error occurs. + */ + public retry(props: RetryProps = {}): Task { + super.addRetry(props); + return this; } - public onError(handler: IChainable, props?: CatchProps): IStateChain { - return this.toStateChain().onError(handler, props); + /** + * Add a recovery handler for this state + * + * When a particular error occurs, execution will continue at the error + * handler instead of failing the state machine execution. + */ + public onError(handler: IChainable, props: CatchProps = {}): Task { + super.addCatch(handler.startState, props); + return this; } - public retry(props: RetryProps = {}): Task { - if (!props.errors) { - props.errors = [Errors.all]; - } - this.retries.push(props); - return this; + /** + * Continue normal execution with the given state + */ + public next(next: IChainable): Chain { + super.makeNext(next.startState); + return Chain.sequence(this, next); } - public toStateChain(): IStateChain { - return new StateChain(new Task.Internals(this)); + /** + * Return the Amazon States Language object for this state + */ + public toStateJson(): object { + return { + ...this.renderNextEnd(), + ...this.renderRetryCatch(), + ...this.renderInputOutput(), + Type: StateType.Task, + Comment: this.comment, + Resource: this.resourceProps.resourceArn, + ResultPath: renderJsonPath(this.resultPath), + TimeoutSeconds: this.timeoutSeconds, + HeartbeatSeconds: this.heartbeatSeconds, + }; } /** @@ -136,7 +181,7 @@ export class Task extends State { * @default average over 5 minutes */ public metricTime(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { - return this.taskMetric(this.resourceProps.metricPrefixSingular, 'ActivityTime', { statistic: 'avg', ...props }); + return this.taskMetric(this.resourceProps.metricPrefixSingular, 'Time', { statistic: 'avg', ...props }); } /** @@ -193,6 +238,13 @@ export class Task extends State { return this.taskMetric(this.resourceProps.metricPrefixPlural, 'HeartbeatTimedOut', props); } + protected onBindToGraph(graph: StateGraph) { + super.onBindToGraph(graph); + for (const policyStatement of this.resourceProps.policyStatements || []) { + graph.registerPolicyStatement(policyStatement); + } + } + private taskMetric(prefix: string | undefined, suffix: string, props?: cloudwatch.MetricCustomization): cloudwatch.Metric { if (prefix === undefined) { throw new Error('This Task Resource does not expose metrics'); @@ -211,10 +263,40 @@ export interface IStepFunctionsTaskResource { asStepFunctionsTaskResource(callingTask: Task): StepFunctionsTaskResourceProps; } +/** + * Properties that define how to refer to a TaskResource + */ export interface StepFunctionsTaskResourceProps { + /** + * The ARN of the resource + */ resourceArn: string; + + /** + * Additional policy statements to add to the execution role + * + * @default No policy roles + */ policyStatements?: cdk.PolicyStatement[]; + + /** + * Prefix for singular metric names of activity actions + * + * @default No such metrics + */ metricPrefixSingular?: string; + + /** + * Prefix for plural metric names of activity actions + * + * @default No such metrics + */ metricPrefixPlural?: string; + + /** + * The dimensions to attach to metrics + * + * @default No metrics + */ metricDimensions?: cloudwatch.DimensionHash; } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/util.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/util.ts deleted file mode 100644 index a0bef29cf2374..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/util.ts +++ /dev/null @@ -1,17 +0,0 @@ -import cdk = require('@aws-cdk/cdk'); -import { RetryProps } from "../asl-external-api"; - -export function renderRetry(retry: RetryProps) { - return { - ErrorEquals: retry.errors, - IntervalSeconds: retry.intervalSeconds, - MaxAttempts: retry.maxAttempts, - BackoffRate: retry.backoffRate - }; -} - -export function renderRetries(retries: RetryProps[]) { - return { - Retry: new cdk.Token(() => retries.length === 0 ? undefined : retries.map(renderRetry)) - }; -} diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/wait.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/wait.ts index dc7b0bd16b7d5..6dfc327fbe600 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/wait.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/wait.ts @@ -1,74 +1,98 @@ import cdk = require('@aws-cdk/cdk'); -import { CatchProps, IChainable, IStateChain, RetryProps } from '../asl-external-api'; -import { IInternalState, StateType, TransitionType } from '../asl-internal-api'; -import { StateChain } from '../asl-state-chain'; -import { State } from './state'; +import { Chain } from '../chain'; +import { IChainable, INextable } from '../types'; +import { State, StateType } from './state'; +/** + * Properties for defining a Wait state + */ export interface WaitProps { + /** + * An optional description for this state + * + * @default No comment + */ + comment?: string; + + /** + * Wait a fixed number of seconds + * + * Exactly one of seconds, secondsPath, timestamp, timestampPath must be supplied. + */ seconds?: number; + + /** + * Wait until the given ISO8601 timestamp + * + * Exactly one of seconds, secondsPath, timestamp, timestampPath must be supplied. + * + * @example 2016-03-14T01:59:00Z + */ timestamp?: string; + /** + * Wait for a number of seconds stored in the state object. + * + * Exactly one of seconds, secondsPath, timestamp, timestampPath must be supplied. + * + * @example $.waitSeconds + */ secondsPath?: string; - timestampPath?: string; - comment?: string; + /** + * Wait until a timestamp found in the state object. + * + * Exactly one of seconds, secondsPath, timestamp, timestampPath must be supplied. + * + * @example $.waitTimestamp + */ + timestampPath?: string; } -export class Wait extends State { - private static Internals = class implements IInternalState { - public readonly canHaveCatch = false; - public readonly stateId: string; - public readonly policyStatements = new Array(); - - constructor(private readonly wait: Wait) { - this.stateId = wait.stateId; - } +/** + * Define a Wait state in the state machine + * + * A Wait state can be used to delay execution of the state machine for a while. + */ +export class Wait extends State implements INextable { + public readonly endStates: INextable[]; - public renderState() { - return { - ...this.wait.renderBaseState(), - ...this.wait.transitions.renderSingle(TransitionType.Next, { End: true }), - }; - } + private readonly seconds?: number; + private readonly timestamp?: string; + private readonly secondsPath?: string; + private readonly timestampPath?: string; - public addNext(targetState: IStateChain): void { - this.wait.addNextTransition(targetState); - } - - public addCatch(_targetState: IStateChain, _props?: CatchProps): void { - throw new Error("Cannot catch errors on a Wait."); - } - - public addRetry(_retry?: RetryProps): void { - // Nothing - } - - public accessibleChains() { - return this.wait.accessibleStates(); - } + constructor(parent: cdk.Construct, id: string, props: WaitProps) { + super(parent, id, props); - public get hasOpenNextTransition(): boolean { - return !this.wait.hasNextTransition; - } - }; + this.seconds = props.seconds; + this.timestamp = props.timestamp; + this.secondsPath = props.secondsPath; + this.timestampPath = props.timestampPath; - constructor(parent: cdk.Construct, id: string, props: WaitProps) { - // FIXME: Validate input - super(parent, id, { - Type: StateType.Wait, - Seconds: props.seconds, - Timestamp: props.timestamp, - SecondsPath: props.secondsPath, - TimestampPath: props.timestampPath, - Comment: props.comment, - }); + this.endStates = [this]; } - public next(sm: IChainable): IStateChain { - return this.toStateChain().next(sm); + /** + * Continue normal execution with the given state + */ + public next(next: IChainable): Chain { + super.makeNext(next.startState); + return Chain.sequence(this, next); } - public toStateChain(): IStateChain { - return new StateChain(new Wait.Internals(this)); + /** + * Return the Amazon States Language object for this state + */ + public toStateJson(): object { + return { + Type: StateType.Wait, + Comment: this.comment, + Seconds: this.seconds, + Timestamp: this.timestamp, + SecondsPath: this.secondsPath, + TimestampPath: this.timestampPath, + ...this.renderNextEnd(), + }; } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/types.ts b/packages/@aws-cdk/aws-stepfunctions/lib/types.ts new file mode 100644 index 0000000000000..4f01885379d8c --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/types.ts @@ -0,0 +1,130 @@ +import { Chain } from './chain'; +import { State } from './states/state'; + +/** + * Interface for states that can have 'next' states + */ +export interface INextable { + next(state: IChainable): Chain; +} + +/** + * Interface for objects that can be used in a Chain + */ +export interface IChainable { + readonly id: string; + readonly startState: State; + readonly endStates: INextable[]; +} + +/** + * Predefined error strings + */ +export class Errors { + /** + * Matches any Error. + */ + public static readonly All = 'States.ALL'; + + /** + * A Task State either ran longer than the “TimeoutSeconds” value, or + * failed to heartbeat for a time longer than the “HeartbeatSeconds” value. + */ + public static readonly Timeout = 'States.Timeout'; + + /** + * A Task State failed during the execution. + */ + public static readonly TaskFailed = 'States.TaskFailed'; + + /** + * A Task State failed because it had insufficient privileges to execute + * the specified code. + */ + public static readonly Permissions = 'States.Permissions'; + + /** + * A Task State’s “ResultPath” field cannot be applied to the input the state received. + */ + public static readonly ResultPathMatchFailure = 'States.ResultPathMatchFailure'; + + /** + * A branch of a Parallel state failed. + */ + public static readonly BranchFailed = 'States.BranchFailed'; + + /** + * A Choice state failed to find a match for the condition field extracted + * from its input. + */ + public static readonly NoChoiceMatched = 'States.NoChoiceMatched'; +} + +/** + * Retry details + */ +export interface RetryProps { + /** + * Errors to retry + * + * A list of error strings to retry, which can be either predefined errors + * (for example Errors.NoChoiceMatched) or a self-defined error. + * + * @default All errors + */ + errors?: string[]; + + /** + * How many seconds to wait initially before retrying + * + * @default 1 + */ + intervalSeconds?: number; + + /** + * How many times to retry this particular error. + * + * May be 0 to disable retry for specific errors (in case you have + * a catch-all retry policy). + * + * @default 3 + */ + maxAttempts?: number; + + /** + * Multiplication for how much longer the wait interval gets on every retry + * + * @default 2 + */ + backoffRate?: number; +} + +/** + * Error handler details + */ +export interface CatchProps { + /** + * Errors to recover from by going to the given state + * + * A list of error strings to retry, which can be either predefined errors + * (for example Errors.NoChoiceMatched) or a self-defined error. + * + * @default All errors + */ + errors?: string[]; + + /** + * JSONPath expression to indicate where to inject the error data + * + * May also be the special value DISCARD, which will cause the error + * data to be discarded. + * + * @default $ + */ + resultPath?: string; +} + +/** + * Special string value to discard state input, output or result + */ +export const DISCARD = 'DISCARD'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.expected.json b/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.expected.json index f05b3a9a1a17b..00bbd00975308 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.expected.json @@ -47,19 +47,19 @@ "Fn::Join": [ "", [ - "{\"StartAt\":\"Submit Job\",\"States\":{\"Submit Job\":{\"Type\":\"Task\",\"Resource\":\"", + "{\"StartAt\":\"Submit Job\",\"States\":{\"Submit Job\":{\"Next\":\"Wait X Seconds\",\"Type\":\"Task\",\"Resource\":\"", { "Ref": "SubmitJobFB773A16" }, - "\",\"ResultPath\":\"$.guid\",\"Retry\":[{\"ErrorEquals\":[\"States.ALL\"],\"IntervalSeconds\":1,\"MaxAttempts\":3,\"BackoffRate\":2}],\"Next\":\"Wait X Seconds\"},\"Wait X Seconds\":{\"Type\":\"Wait\",\"SecondsPath\":\"$.wait_time\",\"Next\":\"Get Job Status\"},\"Get Job Status\":{\"Type\":\"Task\",\"InputPath\":\"$.guid\",\"Resource\":\"", + "\",\"ResultPath\":\"$.guid\"},\"Wait X Seconds\":{\"Type\":\"Wait\",\"SecondsPath\":\"$.wait_time\",\"Next\":\"Get Job Status\"},\"Get Job Status\":{\"Next\":\"Job Complete?\",\"InputPath\":\"$.guid\",\"Type\":\"Task\",\"Resource\":\"", { "Ref": "CheckJob5FFC1D6F" }, - "\",\"ResultPath\":\"$.status\",\"Retry\":[{\"ErrorEquals\":[\"States.ALL\"],\"IntervalSeconds\":1,\"MaxAttempts\":3,\"BackoffRate\":2}],\"Next\":\"Job Complete?\"},\"Job Complete?\":{\"Type\":\"Choice\",\"Choices\":[{\"Variable\":\"$.status\",\"StringEquals\":\"FAILED\",\"Next\":\"Job Failed\"},{\"Variable\":\"$.status\",\"StringEquals\":\"SUCCEEDED\",\"Next\":\"Get Final Job Status\"}],\"Default\":\"Wait X Seconds\"},\"Job Failed\":{\"Type\":\"Fail\",\"Error\":\"DescribeJob returned FAILED\",\"Cause\":\"AWS Batch Job Failed\"},\"Get Final Job Status\":{\"Type\":\"Task\",\"InputPath\":\"$.guid\",\"Resource\":\"", + "\",\"ResultPath\":\"$.status\"},\"Job Complete?\":{\"Type\":\"Choice\",\"Choices\":[{\"Variable\":\"$.status\",\"StringEquals\":\"FAILED\",\"Next\":\"Job Failed\"},{\"Variable\":\"$.status\",\"StringEquals\":\"SUCCEEDED\",\"Next\":\"Get Final Job Status\"}],\"Default\":\"Wait X Seconds\"},\"Job Failed\":{\"Type\":\"Fail\",\"Error\":\"DescribeJob returned FAILED\",\"Cause\":\"AWS Batch Job Failed\"},\"Get Final Job Status\":{\"End\":true,\"InputPath\":\"$.guid\",\"Type\":\"Task\",\"Resource\":\"", { "Ref": "CheckJob5FFC1D6F" }, - "\",\"Retry\":[{\"ErrorEquals\":[\"States.ALL\"],\"IntervalSeconds\":1,\"MaxAttempts\":3,\"BackoffRate\":2}],\"End\":true}},\"TimeoutSeconds\":30}" + "\"}},\"TimeoutSeconds\":30}" ] ] }, diff --git a/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.ts b/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.ts index 9cf501a2ae12f..c0006ab537f1d 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.ts @@ -28,18 +28,14 @@ class JobPollerStack extends cdk.Stack { inputPath: '$.guid', }); - const chain = submitJob + const chain = stepfunctions.Chain + .start(submitJob) .next(waitX) .next(getStatus) .next(isComplete .on(stepfunctions.Condition.stringEquals('$.status', 'FAILED'), jobFailed) .on(stepfunctions.Condition.stringEquals('$.status', 'SUCCEEDED'), finalStatus) - .otherwise(waitX)) - .defaultRetry({ - intervalSeconds: 1, - maxAttempts: 3, - backoffRate: 2 - }); + .otherwise(waitX)); new stepfunctions.StateMachine(this, 'StateMachine', { definition: chain, diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts index 7e682e6d1458f..aed33b0050006 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts @@ -36,9 +36,10 @@ export = { expect(stack).to(haveResource('AWS::StepFunctions::StateMachine', { DefinitionString: { "Fn::Join": ["", [ - "{\"StartAt\":\"Task\",\"States\":{\"Task\":{\"Type\":\"Task\",\"Resource\":\"", + "{\"StartAt\":\"Task\",\"States\":{\"Task\":{\"End\":true,\"Type\":\"Task\",\"Resource\":\"", { Ref: "Activity04690B0A" }, - "\",\"End\":true}}}" + "\"}}}" + ]] }, })); diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.condition.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.condition.ts new file mode 100644 index 0000000000000..f0577272715b8 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.condition.ts @@ -0,0 +1,12 @@ +import { Test } from 'nodeunit'; +import stepfunctions = require('../lib'); + +export = { + 'Condition variables must start with $.'(test: Test) { + test.throws(() => { + stepfunctions.Condition.stringEquals('a', 'b'); + }); + + test.done(); + } +}; diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.metrics.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.metrics.ts new file mode 100644 index 0000000000000..cd2752d396218 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.metrics.ts @@ -0,0 +1,44 @@ +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import stepfunctions = require('../lib'); + +export = { + 'Activity Task metrics and Activity metrics are the same'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const activity = new stepfunctions.Activity(stack, 'Activity'); + const task = new stepfunctions.Task(stack, 'Task', { resource: activity }); + + // WHEN + const activityMetrics = [ + activity.metricFailed(), + activity.metricHeartbeatTimedOut(), + activity.metricRunTime(), + activity.metricScheduled(), + activity.metricScheduleTime(), + activity.metricStarted(), + activity.metricSucceeded(), + activity.metricTime(), + activity.metricTimedOut() + ]; + + const taskMetrics = [ + task.metricFailed(), + task.metricHeartbeatTimedOut(), + task.metricRunTime(), + task.metricScheduled(), + task.metricScheduleTime(), + task.metricStarted(), + task.metricSucceeded(), + task.metricTime(), + task.metricTimedOut(), + ]; + + // THEN + for (let i = 0; i < activityMetrics.length; i++) { + test.deepEqual(activityMetrics[i], taskMetrics[i]); + } + + test.done(); + } +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts index 9653807f5d176..32312e2d0699b 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts @@ -1,7 +1,6 @@ import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; import stepfunctions = require('../lib'); -import { IChainable } from '../lib'; export = { 'Basic composition': { @@ -31,7 +30,9 @@ export = { const task1 = new stepfunctions.Pass(stack, 'State One'); const task2 = new stepfunctions.Pass(stack, 'State Two'); - const chain = task1.next(task2); + const chain = stepfunctions.Chain + .start(task1) + .next(task2); // THEN test.deepEqual(render(chain), { @@ -76,7 +77,10 @@ export = { const task3 = new stepfunctions.Pass(stack, 'State Three'); // WHEN - const chain = task1.next(task2).next(task3); + const chain = stepfunctions.Chain + .start(task1) + .next(task2) + .next(task3); // THEN test.deepEqual(render(chain), { @@ -100,7 +104,9 @@ export = { const task3 = new stepfunctions.Wait(stack, 'State Three', { seconds: 10 }); // WHEN - const chain = task1.next(task2.next(task3)); + const chain = stepfunctions.Chain + .start(task1) + .next(stepfunctions.Chain.start(task2).next(task3)); // THEN test.deepEqual(render(chain), { @@ -115,32 +121,6 @@ export = { test.done(); }, - 'Start state in a StateMachineFragment can be implicit'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const sm = new ReusableStateMachineWithImplicitStartState(stack, 'Reusable'); - - // THEN - test.equals(render(sm).StartAt, 'Reusable/Choice'); - - test.done(); - }, - - 'Can skip adding names in StateMachineFragment'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const sm = new ReusableStateMachineWithImplicitStartState(stack, 'Reusable', { scopeStateNames: false }); - - // THEN - test.equals(render(sm).StartAt, 'Choice'); - - test.done(); - }, - 'A state machine definition can be instantiated and chained'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -154,16 +134,16 @@ export = { test.deepEqual(render(chain), { StartAt: 'Before', States: { - 'Before': { Type: 'Pass', Next: 'Reusable/Choice' }, - 'Reusable/Choice': { + 'Before': { Type: 'Pass', Next: 'Choice' }, + 'Choice': { Type: 'Choice', Choices: [ - { Variable: '$.branch', StringEquals: 'left', Next: 'Reusable/Left Branch' }, - { Variable: '$.branch', StringEquals: 'right', Next: 'Reusable/Right Branch' }, + { Variable: '$.branch', StringEquals: 'left', Next: 'Left Branch' }, + { Variable: '$.branch', StringEquals: 'right', Next: 'Right Branch' }, ] }, - 'Reusable/Left Branch': { Type: 'Pass', Next: 'After' }, - 'Reusable/Right Branch': { Type: 'Pass', Next: 'After' }, + 'Left Branch': { Type: 'Pass', Next: 'After' }, + 'Right Branch': { Type: 'Pass', Next: 'After' }, 'After': { Type: 'Pass', End: true }, } }); @@ -174,14 +154,13 @@ export = { 'A success state cannot be chained onto'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const sm = new stepfunctions.StateMachineFragment(stack, 'SM'); - const succeed = new stepfunctions.Succeed(sm, 'Succeed'); - const pass = new stepfunctions.Pass(sm, 'Pass'); + const succeed = new stepfunctions.Succeed(stack, 'Succeed'); + const pass = new stepfunctions.Pass(stack, 'Pass'); // WHEN test.throws(() => { - succeed.toStateChain().next(pass); + pass.next(succeed).next(pass); }); test.done(); @@ -190,13 +169,12 @@ export = { 'A failure state cannot be chained onto'(test: Test) { // GIVEN const stack = new cdk.Stack(); - const sm = new stepfunctions.StateMachineFragment(stack, 'SM'); - const fail = new stepfunctions.Fail(sm, 'Fail', { error: 'X', cause: 'Y' }); - const pass = new stepfunctions.Pass(sm, 'Pass'); + const fail = new stepfunctions.Fail(stack, 'Fail', { error: 'X', cause: 'Y' }); + const pass = new stepfunctions.Pass(stack, 'Pass'); // WHEN test.throws(() => { - fail.toStateChain().next(pass); + pass.next(fail).next(pass); }); test.done(); @@ -250,8 +228,8 @@ export = { // WHEN const para = new stepfunctions.Parallel(stack, 'Parallel'); - para.branch(new ReusableStateMachine(stack, 'Reusable1')); - para.branch(new ReusableStateMachine(stack, 'Reusable2')); + para.branch(new ReusableStateMachine(stack, 'Reusable1').prefixStates('Reusable1/')); + para.branch(new ReusableStateMachine(stack, 'Reusable2').prefixStates('Reusable2/')); // THEN test.deepEqual(render(para), { @@ -297,6 +275,35 @@ export = { test.done(); }, + 'State Machine Fragments can be wrapped in a single state'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const reusable = new SimpleChain(stack, 'Hello'); + const state = reusable.asSingleState(); + + test.deepEqual(render(state), { + StartAt: 'Hello', + States: { + Hello: { + Type: 'Parallel', + End: true, + Branches: [ + { + StartAt: 'Hello: Task1', + States: { + 'Hello: Task1': { Type: 'Task', Next: 'Hello: Task2', Resource: 'resource' }, + 'Hello: Task2': { Type: 'Task', End: true, Resource: 'resource' }, + } + } + ], + }, + } + }); + + test.done(); + }, + 'Chaining onto branched failure state ignores failure state'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -309,7 +316,7 @@ export = { .otherwise(no); // WHEN - choice.closure().next(enfin); + choice.afterwards().next(enfin); // THEN test.deepEqual(render(choice), { @@ -330,6 +337,36 @@ export = { test.done(); }, + + 'Can include OTHERWISE transition for Choice in afterwards()'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const chain = new stepfunctions.Choice(stack, 'Choice') + .on(stepfunctions.Condition.stringEquals('$.foo', 'bar'), + new stepfunctions.Pass(stack, 'Yes')) + .afterwards({ includeOtherwise: true }) + .next(new stepfunctions.Pass(stack, 'Finally')); + + // THEN + test.deepEqual(render(chain), { + StartAt: 'Choice', + States: { + Choice: { + Type: 'Choice', + Choices: [ + { Variable: '$.foo', StringEquals: 'bar', Next: 'Yes' }, + ], + Default: 'Finally', + }, + Yes: { Type: 'Pass', Next: 'Finally' }, + Finally: { Type: 'Pass', End: true }, + } + }); + + test.done(); + } }, 'Goto support': { @@ -356,7 +393,7 @@ export = { }, }, - 'Error handling': { + 'Catches': { 'States can have error branches'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -421,7 +458,7 @@ export = { }, - 'Error branch is attached to all tasks in chain'(test: Test) { + 'Can wrap chain and attach error handler'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -430,121 +467,14 @@ export = { const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); // WHEN - const chain = task1.next(task2).onError(errorHandler); - - // THEN - test.deepEqual(render(chain), { - StartAt: 'Task1', - States: { - Task1: { - Type: 'Task', - Resource: 'resource', - Next: 'Task2', - Catch: [ - { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler' }, - ] - }, - Task2: { - Type: 'Task', - Resource: 'resource', - End: true, - Catch: [ - { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler' }, - ] - }, - ErrorHandler: { Type: 'Pass', End: true } - } - }); - - test.done(); - }, - - 'Add default retries on all tasks in the chain, but not those outside'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const task1 = new stepfunctions.Task(stack, 'Task1', { resource: new FakeResource() }); - const task2 = new stepfunctions.Task(stack, 'Task2', { resource: new FakeResource() }); - const task3 = new stepfunctions.Task(stack, 'Task3', { resource: new FakeResource() }); - const task4 = new stepfunctions.Task(stack, 'Task4', { resource: new FakeResource() }); - const task5 = new stepfunctions.Task(stack, 'Task5', { resource: new FakeResource() }); - const choice = new stepfunctions.Choice(stack, 'Choice'); - const errorHandler1 = new stepfunctions.Task(stack, 'ErrorHandler1', { resource: new FakeResource() }); - const errorHandler2 = new stepfunctions.Task(stack, 'ErrorHandler2', { resource: new FakeResource() }); - const para = new stepfunctions.Parallel(stack, 'Para'); - - // WHEN - task1.next(task2); - para.onError(errorHandler2); - - task2.onError(errorHandler1) - .next(choice - .on(stepfunctions.Condition.stringEquals('$.var', 'value'), - task3.next(task4)) - .otherwise(para - .branch(task5))) - .defaultRetry(); - - // THEN - const theCatch1 = { Catch: [ { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler1' } ] }; - const theCatch2 = { Catch: [ { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler2' } ] }; - const theRetry = { Retry: [ { ErrorEquals: ['States.ALL'] } ] }; - - test.deepEqual(render(task1), { - StartAt: 'Task1', - States: { - Task1: { Next: 'Task2', Type: 'Task', Resource: 'resource' }, - Task2: { Next: 'Choice', Type: 'Task', Resource: 'resource', ...theCatch1, ...theRetry }, - ErrorHandler1: { End: true, Type: 'Task', Resource: 'resource', ...theRetry }, - Choice: { - Type: 'Choice', - Choices: [ { Variable: '$.var', StringEquals: 'value', Next: 'Task3' } ], - Default: 'Para', - }, - Task3: { Next: 'Task4', Type: 'Task', Resource: 'resource', ...theRetry }, - Task4: { End: true, Type: 'Task', Resource: 'resource', ...theRetry }, - Para: { - Type: 'Parallel', - End: true, - Branches: [ - { - StartAt: 'Task5', - States: { - Task5: { End: true, Type: 'Task', Resource: 'resource' } - } - } - ], - ...theCatch2, - ...theRetry - }, - ErrorHandler2: { End: true, Type: 'Task', Resource: 'resource' }, - } - }); - - test.done(); - }, - - /* - - ** FIXME: Not implemented at the moment, since we need to make a Construct for this and the - name and parent aren't obvious. - - 'Machine is wrapped in parallel if not all tasks can have catch'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const task1 = new stepfunctions.Task(stack, 'Task1', { resource: new FakeResource() }); - const wait = new stepfunctions.Wait(stack, 'Wait', { seconds: 10 }); - const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); - - const chain = task1.next(wait).onError(errorHandler); + const chain = task1.next(task2).asSingleState('Wrapped').onError(errorHandler); // THEN test.deepEqual(render(chain), { - StartAt: 'Para', + StartAt: 'Wrapped', States: { - Para: { + Wrapped: { Type: 'Parallel', - End: true, Branches: [ { StartAt: 'Task1', @@ -552,27 +482,27 @@ export = { Task1: { Type: 'Task', Resource: 'resource', - Next: 'Wait', + Next: 'Task2', }, - Wait: { - Type: 'Wait', + Task2: { + Type: 'Task', + Resource: 'resource', End: true, - Seconds: 10 }, } } ], Catch: [ { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler' }, - ] + ], + End: true }, ErrorHandler: { Type: 'Pass', End: true } - } + }, }); test.done(); }, - */ 'Chaining does not chain onto error handler state'(test: Test) { // GIVEN @@ -644,43 +574,12 @@ export = { // WHEN task1.onError(errorHandler) - .next(new SimpleChain(stack, 'Chain').onError(errorHandler)) + .next(new SimpleChain(stack, 'Chain').catch(errorHandler)) .next(task2.onError(errorHandler)); test.done(); }, - 'After calling .closure() do chain onto error state'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const task1 = new stepfunctions.Task(stack, 'Task1', { resource: new FakeResource() }); - const task2 = new stepfunctions.Task(stack, 'Task2', { resource: new FakeResource() }); - const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); - - // WHEN - const chain = task1.onError(errorHandler).closure().next(task2); - - // THEN - test.deepEqual(render(chain), { - StartAt: 'Task1', - States: { - Task1: { - Type: 'Task', - Resource: 'resource', - Next: 'Task2', - Catch: [ - { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler' }, - ] - }, - ErrorHandler: { Type: 'Pass', Next: 'Task2' }, - Task2: { Type: 'Task', Resource: 'resource', End: true }, - } - }); - - test.done(); - }, - 'Can merge state machines with shared states'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -725,31 +624,83 @@ export = { test.done(); } - } + }, + + 'State machine validation': { + 'No duplicate state IDs'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const intermediateParent = new cdk.Construct(stack, 'Parent'); + + const state1 = new stepfunctions.Pass(stack, 'State'); + const state2 = new stepfunctions.Pass(intermediateParent, 'State'); + + state1.next(state2); + + // WHEN + test.throws(() => { + render(state1); + }); + + test.done(); + }, + + 'No duplicate state IDs even across Parallel branches'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const intermediateParent = new cdk.Construct(stack, 'Parent'); + + const state1 = new stepfunctions.Pass(stack, 'State'); + const state2 = new stepfunctions.Pass(intermediateParent, 'State'); + + const parallel = new stepfunctions.Parallel(stack, 'Parallel') + .branch(state1) + .branch(state2); + + // WHEN + test.throws(() => { + render(parallel); + }); + + test.done(); + }, + + 'No cross-parallel jumps'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const state1 = new stepfunctions.Pass(stack, 'State1'); + const state2 = new stepfunctions.Pass(stack, 'State2'); + + test.throws(() => { + new stepfunctions.Parallel(stack, 'Parallel') + .branch(state1.next(state2)) + .branch(state2); + }); + + test.done(); + }, + }, }; class ReusableStateMachine extends stepfunctions.StateMachineFragment { + public readonly startState: stepfunctions.State; + public readonly endStates: stepfunctions.INextable[]; constructor(parent: cdk.Construct, id: string) { super(parent, id); - this.start( - new stepfunctions.Choice(this, 'Choice') - .on(stepfunctions.Condition.stringEquals('$.branch', 'left'), new stepfunctions.Pass(this, 'Left Branch')) - .on(stepfunctions.Condition.stringEquals('$.branch', 'right'), new stepfunctions.Pass(this, 'Right Branch'))); - } -} - -class ReusableStateMachineWithImplicitStartState extends stepfunctions.StateMachineFragment { - constructor(parent: cdk.Construct, id: string, props: stepfunctions.StateMachineFragmentProps = {}) { - super(parent, id, props); + const choice = new stepfunctions.Choice(this, 'Choice') + .on(stepfunctions.Condition.stringEquals('$.branch', 'left'), new stepfunctions.Pass(this, 'Left Branch')) + .on(stepfunctions.Condition.stringEquals('$.branch', 'right'), new stepfunctions.Pass(this, 'Right Branch')); - const choice = new stepfunctions.Choice(this, 'Choice'); - choice.on(stepfunctions.Condition.stringEquals('$.branch', 'left'), new stepfunctions.Pass(this, 'Left Branch')); - choice.on(stepfunctions.Condition.stringEquals('$.branch', 'right'), new stepfunctions.Pass(this, 'Right Branch')); + this.startState = choice; + this.endStates = choice.afterwards().endStates; } } class SimpleChain extends stepfunctions.StateMachineFragment { + public readonly startState: stepfunctions.State; + public readonly endStates: stepfunctions.INextable[]; + private readonly task2: stepfunctions.Task; constructor(parent: cdk.Construct, id: string) { super(parent, id); @@ -758,9 +709,12 @@ class SimpleChain extends stepfunctions.StateMachineFragment { this.task2 = new stepfunctions.Task(this, 'Task2', { resource: new FakeResource() }); task1.next(this.task2); + + this.startState = task1; + this.endStates = [this.task2]; } - public onError(state: stepfunctions.IChainable, props?: stepfunctions.CatchProps): SimpleChain { + public catch(state: stepfunctions.IChainable, props?: stepfunctions.CatchProps): SimpleChain { this.task2.onError(state, props); return this; } @@ -774,6 +728,6 @@ class FakeResource implements stepfunctions.IStepFunctionsTaskResource { } } -function render(sm: IChainable) { - return cdk.resolve(sm.toStateChain().renderStateMachine().stateMachineDefinition); -} \ No newline at end of file +function render(sm: stepfunctions.IChainable) { + return cdk.resolve(new stepfunctions.StateGraph(sm.startState, 'Test Graph').toGraphJson()); +} From beebb71233c4c411d516247f6f04df21bce3629f Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 9 Oct 2018 11:18:58 +0200 Subject: [PATCH 25/29] Review comments --- packages/@aws-cdk/aws-stepfunctions/README.md | 60 +++++++++++++++-- .../@aws-cdk/aws-stepfunctions/lib/chain.ts | 2 +- .../lib/state-machine-fragment.ts | 6 +- .../aws-stepfunctions/lib/state-machine.ts | 67 +++++++++++++++++-- .../lib/state-transition-metrics.ts | 8 +-- .../aws-stepfunctions/lib/states/parallel.ts | 10 +-- .../test/integ.job-poller.ts | 2 +- .../test/test.states-language.ts | 4 +- 8 files changed, 133 insertions(+), 26 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions/README.md b/packages/@aws-cdk/aws-stepfunctions/README.md index 147dda337a904..f5b38b8e8bbec 100644 --- a/packages/@aws-cdk/aws-stepfunctions/README.md +++ b/packages/@aws-cdk/aws-stepfunctions/README.md @@ -1,7 +1,9 @@ ## AWS Step Functions Construct Library The `@aws-cdk/aws-stepfunctions` package contains constructs for building -serverless workflows. Using objects. Defining a workflow looks like this: +serverless workflows. Using objects. Defining a workflow looks like this +(for the [Step Functions Job Poller +example](https://docs.aws.amazon.com/step-functions/latest/dg/job-status-poller-sample.html)): ```ts const submitLambda = new lambda.Function(this, 'SubmitLambda', { ... }); @@ -9,6 +11,7 @@ const getStatusLambda = new lambda.Function(this, 'CheckLambda', { ... }); const submitJob = new stepfunctions.Task(this, 'Submit Job', { resource: submitLambda, + // Put Lambda's result here in the execution's state object resultPath: '$.guid', }); @@ -16,6 +19,8 @@ const waitX = new stepfunctions.Wait(this, 'Wait X Seconds', { secondsPath: '$.w const getStatus = new stepfunctions.Task(this, 'Get Job Status', { resource: getStatusLambda, + // Pass just the field named "guid" into the Lambda, put the + // Lambda's result in a field called "status" inputPath: '$.guid', resultPath: '$.status', }); @@ -27,6 +32,8 @@ const jobFailed = new stepfunctions.Fail(this, 'Job Failed', { const finalStatus = new stepfunctions.Task(this, 'Get Final Job Status', { resource: getStatusLambda, + // Use "guid" field as input, output of the Lambda becomes the + // entire state machine output. inputPath: '$.guid', }); @@ -34,13 +41,14 @@ const definition = submitJob .next(waitX) .next(getStatus) .next(new stepfunctions.Choice(this, 'Job Complete?') + // Look at the "status" field .on(stepfunctions.Condition.stringEquals('$.status', 'FAILED'), jobFailed) .on(stepfunctions.Condition.stringEquals('$.status', 'SUCCEEDED'), finalStatus) .otherwise(waitX)); new stepfunctions.StateMachine(this, 'StateMachine', { definition, - timeoutSeconds: 300 + timeoutSec: 300 }); ``` @@ -85,7 +93,9 @@ information, see the States Language spec. ### Task A `Task` represents some work that needs to be done. It takes a `resource` -property that is either a Lambda `Function` or a Step Functions `Activity`. +property that is either a Lambda `Function` or a Step Functions `Activity` +(A Lambda Function runs your task's code on AWS Lambda, whereas an `Activity` +is used to run your task's code on an arbitrary compute fleet you manage). ```ts const task = new stepfunctions.Task(this, 'Invoke The Lambda', { @@ -310,7 +320,49 @@ that you run yourself, probably on EC2, will pull jobs from the Activity and submit the results of individual jobs back. You need the ARN to do so, so if you use Activities be sure to pass the Activity -ARN into your worker pool. +ARN into your worker pool: + +```ts +const activity = new stepfunctions.Activity(this, 'Activity'); + +// Read this Output from your application and use it to poll for work on +// the activity. +new cdk.Output(this, 'ActivityArn', { value: activity.activityArn }); +``` + +## Metrics + +`Task` object expose various metrics on the execution of that particular task. For example, +to create an alarm on a particular task failing: + +```ts +new cloudwatch.Alarm(this, 'TaskAlarm', { + metric: task.metricFailed(), + threshold: 1, + evaluationPeriods: 1, +}); +``` + +There are also metrics on the complete state machine: + +```ts +new cloudwatch.Alarm(this, 'StateMachineAlarm', { + metric: stateMachine.metricFailed(), + threshold: 1, + evaluationPeriods: 1, +}); +``` + +And there are metrics on the capacity of all state machines in your account: + +```ts +new cloudwatch.Alarm(this, 'ThrottledAlarm', { + metric: StateTransitionMetrics.metricThrottledEvents(), + threshold: 10, + evaluationPeriods: 2, +}); +``` + ## Future work diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/chain.ts b/packages/@aws-cdk/aws-stepfunctions/lib/chain.ts index 957f67ed384e5..1f762686e49ea 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/chain.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/chain.ts @@ -64,7 +64,7 @@ export class Chain implements IChainable { * your paths accordingly. For example, change 'outputPath' to * '$[0]'. */ - public asSingleState(id: string, props: ParallelProps = {}): Parallel { + public toSingleState(id: string, props: ParallelProps = {}): Parallel { return new Parallel(this.startState, id, props).branch(this); } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine-fragment.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine-fragment.ts index b6b8d37062d4e..56177dcbbb57c 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine-fragment.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine-fragment.ts @@ -18,10 +18,6 @@ export abstract class StateMachineFragment extends cdk.Construct implements ICha */ public abstract readonly endStates: INextable[]; - constructor(parent: cdk.Construct, id: string) { - super(parent, id); - } - /** * Prefix the IDs of all states in this state machine fragment * @@ -46,7 +42,7 @@ export abstract class StateMachineFragment extends cdk.Construct implements ICha * your paths accordingly. For example, change 'outputPath' to * '$[0]'. */ - public asSingleState(options: SingleStateOptions = {}): Parallel { + public toSingleState(options: SingleStateOptions = {}): Parallel { const stateId = options.stateId || this.id; this.prefixStates(options.prefixStates || `${stateId}: `); diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts index 020a4e21a5a8e..515e20b8a547e 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts @@ -34,29 +34,49 @@ export interface StateMachineProps { * * @default No timeout */ - timeoutSeconds?: number; + timeoutSec?: number; } /** * Define a StepFunctions State Machine */ -export class StateMachine extends cdk.Construct { +export class StateMachine extends cdk.Construct implements IStateMachine { + /** + * Import a state machine + */ + public static import(parent: cdk.Construct, id: string, props: ImportedStateMachineProps) { + return new ImportedStateMachine(parent, id, props); + } + + /** + * Execution role of this state machine + */ public readonly role: iam.Role; + + /** + * The name of the state machine + */ public readonly stateMachineName: string; + + /** + * The ARN of the state machine + */ public readonly stateMachineArn: string; - /** A role used by CloudWatch events to trigger a build */ + /** + * A role used by CloudWatch events to start the State Machine + */ private eventsRole?: iam.Role; constructor(parent: cdk.Construct, id: string, props: StateMachineProps) { super(parent, id); this.role = props.role || new iam.Role(this, 'Role', { - assumedBy: new cdk.ServicePrincipal(new cdk.FnConcat('states.', new cdk.AwsRegion(), '.amazonaws.com').toString()), + assumedBy: new cdk.ServicePrincipal(`states.${new cdk.AwsRegion()}.amazonaws.com`), }); const graph = new StateGraph(props.definition.startState, `State Machine ${id} definition`); - graph.timeoutSeconds = props.timeoutSeconds; + graph.timeoutSeconds = props.timeoutSec; const resource = new cloudformation.StateMachineResource(this, 'Resource', { stateMachineName: props.stateMachineName, @@ -168,4 +188,41 @@ export class StateMachine extends cdk.Construct { public metricStarted(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { return this.metric('ExecutionsStarted', props); } + + /** + * Export this state machine + */ + public export(): ImportedStateMachineProps { + return { + stateMachineArn: new cdk.Output(this, 'StateMachineArn', { value: this.stateMachineArn }).makeImportValue().toString(), + }; + } +} + +/** + * A State Machine + */ +export interface IStateMachine { + /** + * The ARN of the state machine + */ + readonly stateMachineArn: string; +} + +/** + * Properties for an imported state machine + */ +export interface ImportedStateMachineProps { + /** + * The ARN of the state machine + */ + stateMachineArn: string; +} + +class ImportedStateMachine extends cdk.Construct implements IStateMachine { + public readonly stateMachineArn: string; + constructor(parent: cdk.Construct, id: string, props: ImportedStateMachineProps) { + super(parent, id); + this.stateMachineArn = props.stateMachineArn; + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-transition-metrics.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-transition-metrics.ts index 4dd642c722c8f..d29817a305c89 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-transition-metrics.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-transition-metrics.ts @@ -25,7 +25,7 @@ export class StateTransitionMetric { * * @default average over 5 minutes */ - public static provisionedBucketSize(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + public static metricProvisionedBucketSize(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { return StateTransitionMetric.metric("ProvisionedBucketSize", props); } @@ -34,7 +34,7 @@ export class StateTransitionMetric { * * @default average over 5 minutes */ - public static provisionedRefillRate(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + public static metricProvisionedRefillRate(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { return StateTransitionMetric.metric("ProvisionedRefillRate", props); } @@ -43,7 +43,7 @@ export class StateTransitionMetric { * * @default average over 5 minutes */ - public static consumedCapacity(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + public static metricConsumedCapacity(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { return StateTransitionMetric.metric("ConsumedCapacity", props); } @@ -52,7 +52,7 @@ export class StateTransitionMetric { * * @default sum over 5 minutes */ - public static throttledEvents(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { + public static metricThrottledEvents(props?: cloudwatch.MetricCustomization): cloudwatch.Metric { return StateTransitionMetric.metric("ThrottledEvents", { statistic: 'sum', ...props }); } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts index b79ab314a8f52..73cc10b468bf3 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts @@ -94,11 +94,13 @@ export class Parallel extends State implements INextable { } /** - * Define a branch to run along all other branches + * Define one or more branches to run in parallel */ - public branch(branch: IChainable): Parallel { - const name = `Parallel '${this.stateId}' branch ${this.branches.length + 1}`; - super.addBranch(new StateGraph(branch.startState, name)); + public branch(...branches: IChainable[]): Parallel { + for (const branch of branches) { + const name = `Parallel '${this.stateId}' branch ${this.branches.length + 1}`; + super.addBranch(new StateGraph(branch.startState, name)); + } return this; } diff --git a/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.ts b/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.ts index c0006ab537f1d..8daf9b96632ac 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.ts @@ -39,7 +39,7 @@ class JobPollerStack extends cdk.Stack { new stepfunctions.StateMachine(this, 'StateMachine', { definition: chain, - timeoutSeconds: 30 + timeoutSec: 30 }); } } diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts index 32312e2d0699b..3ea449332240d 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts @@ -280,7 +280,7 @@ export = { const stack = new cdk.Stack(); const reusable = new SimpleChain(stack, 'Hello'); - const state = reusable.asSingleState(); + const state = reusable.toSingleState(); test.deepEqual(render(state), { StartAt: 'Hello', @@ -467,7 +467,7 @@ export = { const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); // WHEN - const chain = task1.next(task2).asSingleState('Wrapped').onError(errorHandler); + const chain = task1.next(task2).toSingleState('Wrapped').onError(errorHandler); // THEN test.deepEqual(render(chain), { From 8307cd39a71b9a428a2e4e8b5e923c8bab3bf02f Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 9 Oct 2018 11:38:01 +0200 Subject: [PATCH 26/29] Update to new API --- packages/@aws-cdk/aws-stepfunctions/lib/state-graph.ts | 6 +++--- .../@aws-cdk/aws-stepfunctions/lib/state-machine.ts | 10 +++++----- packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts | 3 ++- .../aws-stepfunctions/test/integ.job-poller.ts | 4 ++-- .../test/test.state-machine-resources.ts | 3 ++- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-graph.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-graph.ts index 62b8cd4654a5a..50d368493ce63 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-graph.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-graph.ts @@ -1,4 +1,4 @@ -import cdk = require('@aws-cdk/cdk'); +import iam = require('@aws-cdk/aws-iam'); import { State } from "./states/state"; /** @@ -37,7 +37,7 @@ export class StateGraph { /** * The accumulated policy statements */ - public readonly policyStatements = new Array(); + public readonly policyStatements = new Array(); /** * All states in this graph @@ -72,7 +72,7 @@ export class StateGraph { /** * Register a Policy Statement used by states in this graph */ - public registerPolicyStatement(statement: cdk.PolicyStatement) { + public registerPolicyStatement(statement: iam.PolicyStatement) { if (this.superGraph) { this.superGraph.registerPolicyStatement(statement); } else { diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts index 515e20b8a547e..9e7b552126e52 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts @@ -44,7 +44,7 @@ export class StateMachine extends cdk.Construct implements IStateMachine { /** * Import a state machine */ - public static import(parent: cdk.Construct, id: string, props: ImportedStateMachineProps) { + public static import(parent: cdk.Construct, id: string, props: ImportedStateMachineProps): IStateMachine { return new ImportedStateMachine(parent, id, props); } @@ -72,7 +72,7 @@ export class StateMachine extends cdk.Construct implements IStateMachine { super(parent, id); this.role = props.role || new iam.Role(this, 'Role', { - assumedBy: new cdk.ServicePrincipal(`states.${new cdk.AwsRegion()}.amazonaws.com`), + assumedBy: new iam.ServicePrincipal(`states.${new cdk.AwsRegion()}.amazonaws.com`), }); const graph = new StateGraph(props.definition.startState, `State Machine ${id} definition`); @@ -95,7 +95,7 @@ export class StateMachine extends cdk.Construct implements IStateMachine { /** * Add the given statement to the role's policy */ - public addToRolePolicy(statement: cdk.PolicyStatement) { + public addToRolePolicy(statement: iam.PolicyStatement) { this.role.addToPolicy(statement); } @@ -105,10 +105,10 @@ export class StateMachine extends cdk.Construct implements IStateMachine { public asEventRuleTarget(_ruleArn: string, _ruleId: string): events.EventRuleTargetProps { if (!this.eventsRole) { this.eventsRole = new iam.Role(this, 'EventsRole', { - assumedBy: new cdk.ServicePrincipal('events.amazonaws.com') + assumedBy: new iam.ServicePrincipal('events.amazonaws.com') }); - this.eventsRole.addToPolicy(new cdk.PolicyStatement() + this.eventsRole.addToPolicy(new iam.PolicyStatement() .addAction('states:StartExecution') .addResource(this.stateMachineArn)); } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts index 84c15b9ef12f4..f2b5ced70014a 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts @@ -1,4 +1,5 @@ import cloudwatch = require('@aws-cdk/aws-cloudwatch'); +import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { Chain } from '../chain'; import { StateGraph } from '../state-graph'; @@ -277,7 +278,7 @@ export interface StepFunctionsTaskResourceProps { * * @default No policy roles */ - policyStatements?: cdk.PolicyStatement[]; + policyStatements?: iam.PolicyStatement[]; /** * Prefix for singular metric names of activity actions diff --git a/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.ts b/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.ts index 8daf9b96632ac..560dc5121bd83 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.ts @@ -44,6 +44,6 @@ class JobPollerStack extends cdk.Stack { } } -const app = new cdk.App(process.argv); +const app = new cdk.App(); new JobPollerStack(app, 'aws-stepfunctions-integ'); -process.stdout.write(app.run()); \ No newline at end of file +app.run(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts index d6c0379a70cb1..d3482e31d960b 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts @@ -1,4 +1,5 @@ import { expect, haveResource } from '@aws-cdk/assert'; +import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; import stepfunctions = require('../lib'); @@ -98,7 +99,7 @@ class FakeResource implements stepfunctions.IStepFunctionsTaskResource { return { resourceArn, - policyStatements: [new cdk.PolicyStatement() + policyStatements: [new iam.PolicyStatement() .addAction('resource:Everything') .addResource('resource') ], From 584645d0897951f3315c876463f466bc4db78136 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 9 Oct 2018 12:01:52 +0200 Subject: [PATCH 27/29] Work around jsii bug that does not register implicitly created properties --- packages/@aws-cdk/aws-stepfunctions/lib/chain.ts | 14 +++++++++++++- packages/@aws-cdk/aws-stepfunctions/lib/types.ts | 11 +++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/chain.ts b/packages/@aws-cdk/aws-stepfunctions/lib/chain.ts index 1f762686e49ea..f5db908bab42c 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/chain.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/chain.ts @@ -35,8 +35,20 @@ export class Chain implements IChainable { */ public readonly id: string; - private constructor(public readonly startState: State, public readonly endStates: INextable[], private readonly lastAdded: IChainable) { + /** + * The start state of this chain + */ + public readonly startState: State; + + /** + * The chainable end state(s) of this chain + */ + public readonly endStates: INextable[]; + + private constructor(startState: State, endStates: INextable[], private readonly lastAdded: IChainable) { this.id = lastAdded.id; + this.startState = startState; + this.endStates = endStates; } /** diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/types.ts b/packages/@aws-cdk/aws-stepfunctions/lib/types.ts index 4f01885379d8c..8af16b439bbff 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/types.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/types.ts @@ -12,8 +12,19 @@ export interface INextable { * Interface for objects that can be used in a Chain */ export interface IChainable { + /** + * Descriptive identifier for this chainable + */ readonly id: string; + + /** + * The start state of this chainable + */ readonly startState: State; + + /** + * The chainable end state(s) of this chainable + */ readonly endStates: INextable[]; } From fe43fbdebe5b0ce895745d2fad05839544e530dc Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 10 Oct 2018 09:56:54 +0200 Subject: [PATCH 28/29] Removing obsolete DESIGN_NOTES --- .../aws-stepfunctions/DESIGN_NOTES.md | 216 ------------------ 1 file changed, 216 deletions(-) delete mode 100644 packages/@aws-cdk/aws-stepfunctions/DESIGN_NOTES.md diff --git a/packages/@aws-cdk/aws-stepfunctions/DESIGN_NOTES.md b/packages/@aws-cdk/aws-stepfunctions/DESIGN_NOTES.md deleted file mode 100644 index a32f4fd31d97f..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions/DESIGN_NOTES.md +++ /dev/null @@ -1,216 +0,0 @@ -- Goal: should be possible to define structures such as IfThenElse() by users. - We'd like the usage of these constructs to look more or less like they would look - in regular programming languages. It should look roughly like this: - - task - .then(new IfThenElse( - new Condition(), - task.then(task).then(task), - task.then(task)) - .then(task) - -- Goal: should be possible to define reusable-recurring pieces with parameters, - and then reuse them. - - new Parallel([ - new DoSomeWork({ quality: 'high' }), - new DoSomeWork({ quality: 'medium' }), - new DoSomeWork({ quality: 'low' }), - ]) - -- Goal: States defined in the same StateMachineDefinition share a scope and cannot refer - to states outside that scope. StateMachineDefinition can be exploded into other - StateMachineDefinitions. - -- Goal: you shouldn't HAVE to define a StateMachineDefinition to use in a Parallel - (even though should render as such when expanding to ASL). The following should also work: - - new Parallel([ - task.then(task).then(task), - task.then(task), - task - ]); - - Regardless of how the states get into the Parallel, it should not be possible for them - to jump outside the Parallel branch. - -- Other kind of syntax: - - task1.then(task2).then(task3).goto(task1); // <--- ends chain - -- Interface we need from State: - - interface IState { - next(chainable): Chainable; - goto(state): void; // Use this for terminators as well? - - first(): State; - allReachable(): State[]; - } - - StateMachineDefinition - - All targeted states must be part of the same StateMachineDefinition - enclosure. - - States can be "lifted" into a separate enclosure by being a target of the - Parallel branch. - - Must be able to enumerate all reachable states, so that it can error on - them. Every task can only be the target of a .next() once, but it can - be the target of multiple goto()s. - -- Contraints: Because of JSII: - - - No overloading (Golang) - - No generics (Golang) - - No return type covariance (C#) - - Lack of overloading means that in order to not have a different function call for - every state type, we'll have to do runtime checks (for example, not all states - can be next()'ed onto, and we cannot use the type system to detect and appropriately - constrain this). - - Bonus: runtime checks are going to help dynamically-typed users. - -- Constraint: Trying to next() onto a Chainable that doens't have a next(), runtime error; - Lack of overloads make it impossible to do otherwise. - -- State machine definition should be frozen (and complete) after being chained to. - -Nextable: Pass, Task, Wait, Parallel -Terminating: Succeed, Fail -Weird: Choice - -class Blah extends StateMachine { - constructor() { - // define statemachine here - } -} - -Use as: - - new StateMachine(..., { - definintion: new Blah() - }); - -But also: - - const def = new StateMachineDefinition(); - const task = new Task(def, ...); - task.then(new Blah())// <-- expand (does that introduce naming? It should!) - .then(new Task(...); // <-- appends transition to every end state! - -And also: - - const p = new Parallel(); - p.parallel(new Blah()); // <-- leave as state machine - -But not: - - const p = new Parallel(); - blah = new Blah(); - p.parallel(blah); - - blah.then(...); // <-- should be immutable!! - -And also: - - const task1 = new Task1(); - const task2 = new Task1(); - - const p = new Parallel(); - p.parallel(task1); // <-- convert to state machine - p.parallel(task2); - -class FrozenStateMachine { - render(); -} - -TO CHECK -- Can SMDef be mutable? - -QUESTION -- Do branches in Parallel allow Timeout/Version? - -PROBLEMS ENCOUNTERED --------------------- - - task1.catch(handler).then(task2) - -Does this mean: - - (a) task1 -> task2 - | - +---> handler - -Or does this mean: - - (b) task1 -------> task2 - | ^ - +--> handler --+ - -In the case of simply writing this, you probably want (a), but -in the case of a larger composition: - - someStateMachine.then(task2) - -You want behavior (b) in case someStateMachine = task1.catch(handler). - -How to distinguish the two? The problem is in the .then() operator, but the -only way to solve it is with more operators (.close(), or otherwise) which is -going to confuse people 99% of the time. - -If we make everything mutable, we can simply do the narrow .then() definition -(open transitions only include task1), and people can manually add more -transitions after handler if they want to (in an immutable system, there's no -good way to "grab" that handler object and add to the transitions later). -Also, in the mutable representation, goto's are easier to represent (as in: -need no special representation, we can simply use .then()). - -Next complication however: if we do the mutable thing, and we can add to the -state machine in fragments, there's no object that represents the ENTIRE -state machine with all states and transitions. How do we get the full state -machine? Either we enumerate all children of a StateMachineDefinition object, -or we begin at the start state and crawl all accessible states. In the former -case, we can warn/error if not all states are used in the SM. - -Then there is the construct model API. I would like to allow: - -```ts -class SomeConstruct extends cdk.Construct { - constructor(parent: cdk.Construct) { - const task1 = new Task(this, ...); - const task2 = new Task(this, ...); - - // ALLOW THIS - new StateMachine(..., { - definition: task1.then(task2) - }); - - // ALLOW THIS - task1.then(task2); - new StateMachine(..., { - definition: task1 - }); - - new StateMachineDefinition(this, { - }); - } -} -``` - -It's desirable to just have a StateMachineDefinition own and list everything, -but that kind of requires that for Parallel you need a StateMachineDefinition -as well (but probably without appending names). - ---- - -Notes: use a complicated object model to hide ugly parts of the API from users -(by using "internal details" classes as capabilities). - -Decouple state chainer from states. - -Decouple rendering from states to allow elidable states. - -'then' is a reserved word in Ruby -'catch' is a reserved word in many languages - -https://halyph.com/blog/2016/11/28/prog-lang-reserved-words.html \ No newline at end of file From ae693abddbbf7000d471408a49d8e16bcef36162 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 16 Oct 2018 15:00:20 +0200 Subject: [PATCH 29/29] Method renames --- packages/@aws-cdk/aws-stepfunctions/README.md | 24 +++++++------- .../aws-stepfunctions/lib/states/choice.ts | 2 +- .../aws-stepfunctions/lib/states/parallel.ts | 8 ++--- .../aws-stepfunctions/lib/states/state.ts | 4 +-- .../aws-stepfunctions/lib/states/task.ts | 8 ++--- .../test/integ.job-poller.ts | 4 +-- .../test/test.states-language.ts | 32 +++++++++---------- 7 files changed, 41 insertions(+), 41 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions/README.md b/packages/@aws-cdk/aws-stepfunctions/README.md index f5b38b8e8bbec..a0527c883b83c 100644 --- a/packages/@aws-cdk/aws-stepfunctions/README.md +++ b/packages/@aws-cdk/aws-stepfunctions/README.md @@ -42,8 +42,8 @@ const definition = submitJob .next(getStatus) .next(new stepfunctions.Choice(this, 'Job Complete?') // Look at the "status" field - .on(stepfunctions.Condition.stringEquals('$.status', 'FAILED'), jobFailed) - .on(stepfunctions.Condition.stringEquals('$.status', 'SUCCEEDED'), finalStatus) + .when(stepfunctions.Condition.stringEquals('$.status', 'FAILED'), jobFailed) + .when(stepfunctions.Condition.stringEquals('$.status', 'SUCCEEDED'), finalStatus) .otherwise(waitX)); new stepfunctions.StateMachine(this, 'StateMachine', { @@ -105,13 +105,13 @@ const task = new stepfunctions.Task(this, 'Invoke The Lambda', { }); // Add a retry policy -task.retry({ +task.addRetry({ intervalSeconds: 5, maxAttempts: 10 }); // Add an error handler -task.onError(errorHandlerState); +task.addCatch(errorHandlerState); // Set the next state task.next(nextState); @@ -158,9 +158,9 @@ values in the execution's JSON state: ```ts const choice = new stepfunctions.Choice(this, 'Did it work?'); -// Add conditions with .on() -choice.on(stepfunctions.Condition.stringEqual('$.status', 'SUCCESS'), successState); -choice.on(stepfunctions.Condition.numberGreaterThan('$.attempts', 5), failureState); +// Add conditions with .when() +choice.when(stepfunctions.Condition.stringEqual('$.status', 'SUCCESS'), successState); +choice.when(stepfunctions.Condition.numberGreaterThan('$.attempts', 5), failureState); // Use .otherwise() to indicate what should be done if none of the conditions match choice.otherwise(tryAgainState); @@ -172,8 +172,8 @@ then ... else` works in a programming language), use the `.afterwards()` method: ```ts const choice = new stepfunctions.Choice(this, 'What color is it?'); -choice.on(stepfunctions.Condition.stringEqual('$.color', 'BLUE'), handleBlueItem); -choice.on(stepfunctions.Condition.stringEqual('$.color', 'RED'), handleRedItem); +choice.when(stepfunctions.Condition.stringEqual('$.color', 'BLUE'), handleBlueItem); +choice.when(stepfunctions.Condition.stringEqual('$.color', 'RED'), handleRedItem); choice.otherwise(handleOtherItemColor); // Use .afterwards() to join all possible paths back together and continue @@ -198,10 +198,10 @@ parallel.branch(sendInvoice); parallel.branch(restock); // Retry the whole workflow if something goes wrong -parallel.retry({ maxAttempts: 1 }); +parallel.addRetry({ maxAttempts: 1 }); // How to recover from errors -parallel.onError(sendFailureNotification); +parallel.addCatch(sendFailureNotification); // What to do in case everything succeeded parallel.next(closeOrder); @@ -241,7 +241,7 @@ targets of `Choice.on` or `Parallel.branch`: const definition = step1 .next(step2) .next(choice - .on(condition1, step3.next(step4).next(step5)) + .when(condition1, step3.next(step4).next(step5)) .otherwise(step6) .afterwards()) .next(parallel diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts index 29c7a8910c46e..54cbf54cb4919 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/choice.ts @@ -52,7 +52,7 @@ export class Choice extends State { /** * If the given condition matches, continue execution with the given state */ - public on(condition: Condition, next: IChainable): Choice { + public when(condition: Condition, next: IChainable): Choice { super.addChoice(condition, next.startState); return this; } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts index 73cc10b468bf3..5891fa0033b32 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts @@ -69,8 +69,8 @@ export class Parallel extends State implements INextable { * This controls if and how the execution will be retried if a particular * error occurs. */ - public retry(props: RetryProps = {}): Parallel { - super.addRetry(props); + public addRetry(props: RetryProps = {}): Parallel { + super._addRetry(props); return this; } @@ -80,8 +80,8 @@ export class Parallel extends State implements INextable { * When a particular error occurs, execution will continue at the error * handler instead of failing the state machine execution. */ - public onError(handler: IChainable, props: CatchProps = {}): Parallel { - super.addCatch(handler.startState, props); + public addCatch(handler: IChainable, props: CatchProps = {}): Parallel { + super._addCatch(handler.startState, props); return this; } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts index 19d957147ba42..3ef8b28d27746 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts @@ -200,7 +200,7 @@ export abstract class State extends cdk.Construct implements IChainable { /** * Add a retrier to the retry list of this state */ - protected addRetry(props: RetryProps = {}) { + protected _addRetry(props: RetryProps = {}) { this.retries.push({ ...props, errors: props.errors ? props.errors : [Errors.All], @@ -210,7 +210,7 @@ export abstract class State extends cdk.Construct implements IChainable { /** * Add an error handler to the catch list of this state */ - protected addCatch(handler: State, props: CatchProps = {}) { + protected _addCatch(handler: State, props: CatchProps = {}) { this.catches.push({ next: handler, props: { diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts index f2b5ced70014a..2612ce33b305b 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts @@ -102,8 +102,8 @@ export class Task extends State implements INextable { * This controls if and how the execution will be retried if a particular * error occurs. */ - public retry(props: RetryProps = {}): Task { - super.addRetry(props); + public addRetry(props: RetryProps = {}): Task { + super._addRetry(props); return this; } @@ -113,8 +113,8 @@ export class Task extends State implements INextable { * When a particular error occurs, execution will continue at the error * handler instead of failing the state machine execution. */ - public onError(handler: IChainable, props: CatchProps = {}): Task { - super.addCatch(handler.startState, props); + public addCatch(handler: IChainable, props: CatchProps = {}): Task { + super._addCatch(handler.startState, props); return this; } diff --git a/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.ts b/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.ts index 560dc5121bd83..8110a39431c17 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/integ.job-poller.ts @@ -33,8 +33,8 @@ class JobPollerStack extends cdk.Stack { .next(waitX) .next(getStatus) .next(isComplete - .on(stepfunctions.Condition.stringEquals('$.status', 'FAILED'), jobFailed) - .on(stepfunctions.Condition.stringEquals('$.status', 'SUCCEEDED'), finalStatus) + .when(stepfunctions.Condition.stringEquals('$.status', 'FAILED'), jobFailed) + .when(stepfunctions.Condition.stringEquals('$.status', 'SUCCEEDED'), finalStatus) .otherwise(waitX)); new stepfunctions.StateMachine(this, 'StateMachine', { diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts index 3ea449332240d..beeb6dc04d857 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts @@ -312,7 +312,7 @@ export = { const no = new stepfunctions.Fail(stack, 'No', { error: 'Failure', cause: 'Wrong branch' }); const enfin = new stepfunctions.Pass(stack, 'Finally'); const choice = new stepfunctions.Choice(stack, 'Choice') - .on(stepfunctions.Condition.stringEquals('$.foo', 'bar'), yes) + .when(stepfunctions.Condition.stringEquals('$.foo', 'bar'), yes) .otherwise(no); // WHEN @@ -344,7 +344,7 @@ export = { // WHEN const chain = new stepfunctions.Choice(stack, 'Choice') - .on(stepfunctions.Condition.stringEquals('$.foo', 'bar'), + .when(stepfunctions.Condition.stringEquals('$.foo', 'bar'), new stepfunctions.Pass(stack, 'Yes')) .afterwards({ includeOtherwise: true }) .next(new stepfunctions.Pass(stack, 'Finally')); @@ -401,7 +401,7 @@ export = { const failure = new stepfunctions.Fail(stack, 'Failed', { error: 'DidNotWork', cause: 'We got stuck' }); // WHEN - const chain = task1.onError(failure); + const chain = task1.addCatch(failure); // THEN test.deepEqual(render(chain), { @@ -433,7 +433,7 @@ export = { const failure = new stepfunctions.Fail(stack, 'Failed', { error: 'DidNotWork', cause: 'We got stuck' }); // WHEN - const chain = task1.retry({ errors: ['HTTPError'], maxAttempts: 2 }).onError(failure, { resultPath: '$.some_error' }).next(failure); + const chain = task1.addRetry({ errors: ['HTTPError'], maxAttempts: 2 }).addCatch(failure, { resultPath: '$.some_error' }).next(failure); // THEN test.deepEqual(render(chain), { @@ -467,7 +467,7 @@ export = { const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); // WHEN - const chain = task1.next(task2).toSingleState('Wrapped').onError(errorHandler); + const chain = task1.next(task2).toSingleState('Wrapped').addCatch(errorHandler); // THEN test.deepEqual(render(chain), { @@ -513,7 +513,7 @@ export = { const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); // WHEN - const chain = task1.onError(errorHandler).next(task2); + const chain = task1.addCatch(errorHandler).next(task2); // THEN test.deepEqual(render(chain), { @@ -545,9 +545,9 @@ export = { const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); // WHEN - const chain = task1.onError(errorHandler) - .next(task2.onError(errorHandler)) - .next(task3.onError(errorHandler)); + const chain = task1.addCatch(errorHandler) + .next(task2.addCatch(errorHandler)) + .next(task3.addCatch(errorHandler)); // THEN const sharedTaskProps = { Type: 'Task', Resource: 'resource', Catch: [ { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler' } ] }; @@ -573,9 +573,9 @@ export = { const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); // WHEN - task1.onError(errorHandler) + task1.addCatch(errorHandler) .next(new SimpleChain(stack, 'Chain').catch(errorHandler)) - .next(task2.onError(errorHandler)); + .next(task2.addCatch(errorHandler)); test.done(); }, @@ -589,8 +589,8 @@ export = { const failure = new stepfunctions.Fail(stack, 'Failed', { error: 'DidNotWork', cause: 'We got stuck' }); // WHEN - task1.onError(failure); - task2.onError(failure); + task1.addCatch(failure); + task2.addCatch(failure); task1.next(task2); @@ -689,8 +689,8 @@ class ReusableStateMachine extends stepfunctions.StateMachineFragment { super(parent, id); const choice = new stepfunctions.Choice(this, 'Choice') - .on(stepfunctions.Condition.stringEquals('$.branch', 'left'), new stepfunctions.Pass(this, 'Left Branch')) - .on(stepfunctions.Condition.stringEquals('$.branch', 'right'), new stepfunctions.Pass(this, 'Right Branch')); + .when(stepfunctions.Condition.stringEquals('$.branch', 'left'), new stepfunctions.Pass(this, 'Left Branch')) + .when(stepfunctions.Condition.stringEquals('$.branch', 'right'), new stepfunctions.Pass(this, 'Right Branch')); this.startState = choice; this.endStates = choice.afterwards().endStates; @@ -715,7 +715,7 @@ class SimpleChain extends stepfunctions.StateMachineFragment { } public catch(state: stepfunctions.IChainable, props?: stepfunctions.CatchProps): SimpleChain { - this.task2.onError(state, props); + this.task2.addCatch(state, props); return this; } }