├── settings.gradle ├── etc ├── fibonacci.png └── helloworld.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ ├── java │ │ └── com │ │ │ └── eclecticlogic │ │ │ └── stepper │ │ │ ├── state │ │ │ ├── State.java │ │ │ ├── Fail.java │ │ │ ├── Wait.java │ │ │ ├── Pass.java │ │ │ ├── Succeed.java │ │ │ ├── Parallel.java │ │ │ ├── Task.java │ │ │ ├── StateType.java │ │ │ ├── NamingScheme.java │ │ │ ├── NameProvider.java │ │ │ ├── observer │ │ │ │ ├── NotificationReceiver.java │ │ │ │ └── StateObserver.java │ │ │ ├── Choice.java │ │ │ ├── AbstractState.java │ │ │ └── AttributableState.java │ │ │ ├── etc │ │ │ ├── WeaveContext.java │ │ │ ├── LambdaHelper.java │ │ │ ├── Constants.java │ │ │ ├── LambdaBranch.java │ │ │ └── Etc.java │ │ │ ├── StepperParseException.java │ │ │ ├── construct │ │ │ ├── SuccessConstruct.java │ │ │ ├── WaitConstruct.java │ │ │ ├── FailConstruct.java │ │ │ ├── GotoConstruct.java │ │ │ ├── StateConstruct.java │ │ │ ├── ExpressionConstruct.java │ │ │ ├── CatchClause.java │ │ │ ├── WhenCase.java │ │ │ ├── ProgramConstruct.java │ │ │ ├── WhileConstruct.java │ │ │ ├── Construct.java │ │ │ ├── WhenConstruct.java │ │ │ ├── IfConstruct.java │ │ │ ├── ParallelConstruct.java │ │ │ ├── TryCatchConstruct.java │ │ │ ├── ForIterationConstruct.java │ │ │ └── ForLoopConstruct.java │ │ │ ├── visitor │ │ │ ├── DereferencingVisitor.java │ │ │ ├── AbstractVisitor.java │ │ │ ├── DereferenceListener.java │ │ │ ├── StatementBlockVisitor.java │ │ │ ├── RetryVisitor.java │ │ │ ├── ForVisitor.java │ │ │ ├── JsonObjectVisitor.java │ │ │ ├── StepperVisitor.java │ │ │ ├── AssignmentVisitor.java │ │ │ └── StatementVisitor.java │ │ │ ├── install │ │ │ ├── InstallConfig.java │ │ │ ├── StepFunctionInstaller.java │ │ │ └── LambdaInstaller.java │ │ │ ├── StateMachine.java │ │ │ └── Stepper.java │ ├── resources │ │ └── stepper │ │ │ └── template │ │ │ └── lambda.stg │ └── antlr │ │ └── Stepper.g4 └── test │ ├── resources │ ├── stepper │ │ ├── fail.stg │ │ ├── goto.stg │ │ ├── parallel.stg │ │ ├── while.stg │ │ ├── forloop.stg │ │ ├── when.stg │ │ ├── if.stg │ │ ├── trycatch.stg │ │ └── basic.stg │ └── antlr │ │ └── grammar │ │ └── grammar.stg │ ├── groovy │ └── com │ │ └── eclecticlogic │ │ └── stepper │ │ ├── etc │ │ └── TestEtc.groovy │ │ ├── construct │ │ └── TestConstruct.groovy │ │ └── asl │ │ ├── AbstractStateMachineTester.groovy │ │ ├── TestFail.groovy │ │ ├── TestParallel.groovy │ │ ├── TestGoto.groovy │ │ ├── TestWhile.groovy │ │ ├── TestTryCatch.groovy │ │ ├── TestWhen.groovy │ │ └── TestBasic.groovy │ └── java │ └── com │ └── eclecticlogic │ └── stepper │ └── antlr │ ├── AbstractGrammarTester.java │ └── TestGrammar.java ├── .gitignore ├── gradlew.bat ├── gradlew ├── README.md └── LICENSE /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'stepper' 2 | 3 | -------------------------------------------------------------------------------- /etc/fibonacci.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eclecticlogic/stepper/HEAD/etc/fibonacci.png -------------------------------------------------------------------------------- /etc/helloworld.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eclecticlogic/stepper/HEAD/etc/helloworld.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eclecticlogic/stepper/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.5.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/state/State.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.state; 2 | 3 | import com.google.gson.JsonObject; 4 | 5 | public interface State { 6 | 7 | String getName(); 8 | 9 | void setNextState(String value); 10 | 11 | JsonObject toJson(); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/etc/WeaveContext.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.etc; 2 | 3 | public class WeaveContext { 4 | 5 | private final LambdaHelper lambdaHelper = new LambdaHelper(); 6 | 7 | 8 | public LambdaHelper getLambdaHelper() { 9 | return lambdaHelper; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/state/Fail.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.state; 2 | 3 | public class Fail extends AttributableState { 4 | 5 | public Fail() { 6 | this(null); 7 | } 8 | 9 | 10 | public Fail(String label) { 11 | super(label); 12 | setType(StateType.FAIL); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/state/Wait.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.state; 2 | 3 | public class Wait extends AttributableState { 4 | 5 | public Wait() { 6 | this(null); 7 | } 8 | 9 | 10 | public Wait(String label) { 11 | super(label); 12 | setType(StateType.WAIT); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/state/Pass.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.state; 2 | 3 | public class Pass extends AttributableState { 4 | 5 | public Pass() { 6 | this(null); 7 | } 8 | 9 | 10 | public Pass(String stateName) { 11 | super(stateName); 12 | setType(StateType.PASS); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/state/Succeed.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.state; 2 | 3 | public class Succeed extends AbstractState { 4 | 5 | public Succeed() { 6 | this(null); 7 | } 8 | 9 | 10 | public Succeed(String label) { 11 | super(label); 12 | setType(StateType.SUCCEED); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/state/Parallel.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.state; 2 | 3 | public class Parallel extends AttributableState { 4 | 5 | public Parallel() { 6 | this(null); 7 | } 8 | 9 | 10 | public Parallel(String stateName) { 11 | super(stateName); 12 | setType(StateType.PARALLEL); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/state/Task.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.state; 2 | 3 | 4 | public class Task extends AttributableState { 5 | 6 | public Task() { 7 | this(null); 8 | } 9 | 10 | 11 | public Task(String stateName) { 12 | super(stateName); 13 | setType(StateType.TASK); 14 | } 15 | 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/etc/LambdaHelper.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.etc; 2 | 3 | import com.google.common.collect.Lists; 4 | 5 | import java.util.List; 6 | 7 | public class LambdaHelper { 8 | 9 | private final List branches = Lists.newArrayList(); 10 | 11 | 12 | public List getBranches() { 13 | return branches; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/test/resources/stepper/fail.stg: -------------------------------------------------------------------------------- 1 | 2 | simple() ::= << 3 | stepper Fail { 4 | a = 5; 5 | fail; 6 | b = 7; 7 | d = 5; 8 | } 9 | >> 10 | 11 | failWithAttributes() ::= << 12 | stepper Fail { 13 | a = 5; 14 | @Label("a") fail { 15 | "Cause": "blah1", 16 | "Error": "blah2" 17 | } 18 | b = 7; 19 | d = 5; 20 | } 21 | >> -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | !gradle/wrapper/gradle-wrapper.jar 16 | *.war 17 | *.nar 18 | *.ear 19 | *.zip 20 | *.tar.gz 21 | *.rar 22 | 23 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 24 | hs_err_pid* 25 | build/ 26 | .idea/ 27 | .gradle/ 28 | out/ -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/StepperParseException.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper; 2 | 3 | import org.antlr.v4.runtime.RecognitionException; 4 | 5 | public class StepperParseException extends RuntimeException { 6 | 7 | public StepperParseException(Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) { 8 | super("Line: " + line + ", char: " + charPositionInLine + ", " + msg, e); 9 | 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/construct/SuccessConstruct.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.construct; 2 | 3 | import com.eclecticlogic.stepper.state.Succeed; 4 | 5 | public class SuccessConstruct extends StateConstruct { 6 | 7 | public SuccessConstruct() { 8 | super(new Succeed()); 9 | } 10 | 11 | 12 | public SuccessConstruct(String programName) { 13 | super(new Succeed(programName + ".Success")); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/construct/WaitConstruct.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.construct; 2 | 3 | import com.eclecticlogic.stepper.state.Wait; 4 | 5 | public class WaitConstruct extends StateConstruct { 6 | 7 | public WaitConstruct(String label) { 8 | super(new Wait(label)); 9 | } 10 | 11 | 12 | @Override 13 | // Making public. 14 | public Wait getState() { 15 | return super.getState(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/resources/stepper/goto.stg: -------------------------------------------------------------------------------- 1 | 2 | goto1() ::= << 3 | stepper Goto { 4 | a = 5; 5 | if (a == 5) { 6 | goto "a" 7 | } else fail; 8 | @Label("a") b = 7; 9 | d = 5; 10 | } 11 | >> 12 | 13 | goto2() ::= << 14 | stepper Goto { 15 | a = 5; 16 | if (a == 5) goto "a" 17 | else goto "b" 18 | @Label("a") b = 7; 19 | goto "Goto.Success" 20 | @Label("b") d = 5; 21 | } 22 | >> -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/state/StateType.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.state; 2 | 3 | public enum StateType { 4 | PASS("Pass"), 5 | TASK("Task"), 6 | CHOICE("Choice"), 7 | WAIT("Wait"), 8 | SUCCEED("Succeed"), 9 | FAIL("Fail"), 10 | PARALLEL("Parallel"), 11 | ; 12 | 13 | 14 | private String name; 15 | 16 | StateType(String name) { 17 | this.name = name; 18 | } 19 | 20 | public String getName() { 21 | return name; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/resources/stepper/parallel.stg: -------------------------------------------------------------------------------- 1 | 2 | parallel1() ::= << 3 | stepper Para { 4 | a = 5 * 2; 5 | @Label("a") 6 | @RetryOnError("pqr") { "IntervalSeconds": 6 } 7 | b = parallel("stg://para1@stepper/parallel.stg", "stg://para2@stepper/parallel.stg") 8 | } 9 | >> 10 | 11 | para1() ::= << 12 | stepper First { 13 | a = 5 * 5; 14 | b = 10; 15 | } 16 | >> 17 | 18 | para2() ::= << 19 | stepper Second { 20 | a = 15 * 5; 21 | b = 20; 22 | } 23 | >> -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/state/NamingScheme.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.state; 2 | 3 | public class NamingScheme { 4 | 5 | private String prefix; 6 | private int index; 7 | private int varIndex; 8 | 9 | 10 | public NamingScheme(String prefix) { 11 | this.prefix = prefix; 12 | } 13 | 14 | 15 | public String getNextName() { 16 | return String.format("%s%03d", prefix, index++); 17 | } 18 | 19 | 20 | public String getVar() { 21 | return String.format("%svar__%03d", prefix, varIndex++); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/resources/stepper/while.stg: -------------------------------------------------------------------------------- 1 | 2 | simple() ::= << 3 | stepper Simple { 4 | a = 5; 5 | @Label("abc") 6 | while (a \< 15) { 7 | b = a + 1; 8 | a = a + 1; 9 | } 10 | c = a; 11 | } 12 | >> 13 | 14 | nested() ::= << 15 | stepper Nested { 16 | a = 5; 17 | @Label("abc") 18 | while (a \< 15) { 19 | b = a + 1; 20 | @Label("def") 21 | while (b > 3) { 22 | c = b + a; 23 | b-=1; 24 | } 25 | } 26 | } 27 | >> -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/visitor/DereferencingVisitor.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.visitor; 2 | 3 | import com.eclecticlogic.stepper.antlr.StepperBaseVisitor; 4 | import com.eclecticlogic.stepper.antlr.StepperParser; 5 | import com.google.common.collect.Sets; 6 | import org.antlr.v4.runtime.tree.ParseTreeWalker; 7 | 8 | import java.util.Set; 9 | 10 | public class DereferencingVisitor extends StepperBaseVisitor> { 11 | 12 | @Override 13 | public Set visitExpr(StepperParser.ExprContext ctx) { 14 | ParseTreeWalker walker = new ParseTreeWalker(); 15 | DereferenceListener listener = new DereferenceListener(); 16 | walker.walk(listener, ctx); 17 | return Sets.newHashSet(listener.getReferences()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/construct/FailConstruct.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.construct; 2 | 3 | import com.eclecticlogic.stepper.etc.WeaveContext; 4 | import com.eclecticlogic.stepper.state.Fail; 5 | 6 | public class FailConstruct extends StateConstruct { 7 | 8 | public FailConstruct(String label) { 9 | super(new Fail(label)); 10 | } 11 | 12 | 13 | @Override 14 | public Fail getState() { 15 | return super.getState(); 16 | } 17 | 18 | 19 | @Override 20 | protected void setNextStateName(String name) { 21 | // noop 22 | } 23 | 24 | 25 | @Override 26 | public void weave(WeaveContext context) { 27 | if (getNext() != null) { 28 | getNext().weave(context); 29 | } 30 | } 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/construct/GotoConstruct.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.construct; 2 | 3 | import com.eclecticlogic.stepper.etc.WeaveContext; 4 | import com.eclecticlogic.stepper.state.Pass; 5 | 6 | public class GotoConstruct extends StateConstruct { 7 | 8 | public GotoConstruct(String nextTarget) { 9 | super(new Pass()); 10 | super.setNextStateName(nextTarget); 11 | } 12 | 13 | 14 | @Override 15 | public Pass getState() { 16 | return super.getState(); 17 | } 18 | 19 | 20 | @Override 21 | protected void setNextStateName(String name) { 22 | // noop 23 | } 24 | 25 | 26 | @Override 27 | public void weave(WeaveContext context) { 28 | if (getNext() != null) { 29 | getNext().weave(context); 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/state/NameProvider.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.state; 2 | 3 | import java.util.Stack; 4 | 5 | public class NameProvider { 6 | 7 | private static ThreadLocal> namingSchemeStack = new ThreadLocal<>(); 8 | 9 | 10 | public static void usingName(String name, Runnable code) { 11 | if (namingSchemeStack.get() == null) { 12 | namingSchemeStack.set(new Stack<>()); 13 | } 14 | namingSchemeStack.get().push(new NamingScheme(name)); 15 | code.run(); 16 | namingSchemeStack.get().pop(); 17 | } 18 | 19 | 20 | public static String getName() { 21 | return namingSchemeStack.get().peek().getNextName(); 22 | } 23 | 24 | 25 | public static String getVar() { 26 | return namingSchemeStack.get().peek().getVar(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/etc/Constants.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015-2019 KR Abram 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.eclecticlogic.stepper.etc; 18 | 19 | public interface Constants { 20 | 21 | String COMMAND_VAR = "cmd__sm"; 22 | 23 | String LAMBDA_ARN_PLACEHOLDER = "@@@lambda_helper_arn@@@"; 24 | } 25 | -------------------------------------------------------------------------------- /src/test/resources/stepper/forloop.stg: -------------------------------------------------------------------------------- 1 | 2 | simpleFor() ::= << 3 | stepper For { 4 | @Label("abc") 5 | for (i = 0 to 10 step 2) { 6 | a += i; 7 | } 8 | } 9 | >> 10 | 11 | doubleFor() ::= << 12 | stepper For { 13 | for (i = 0 to 10 step 2) 14 | for (j = 2 to 12) { 15 | a = i; 16 | b = j; 17 | c = a * b; 18 | } 19 | } 20 | 21 | >> 22 | 23 | simpleIter() ::= << 24 | stepper For { 25 | @Label("abc") 26 | for (a in b.custom().delta(e)) { 27 | j = a; 28 | } 29 | } 30 | >> 31 | 32 | doubleIter() ::= << 33 | stepper Double { 34 | @Label("abc") 35 | for (a in b) { 36 | @Label("def") 37 | for (c in d) { 38 | j = a; 39 | } 40 | } 41 | } 42 | >> -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/visitor/AbstractVisitor.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015-2019 KR Abram 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.eclecticlogic.stepper.visitor; 18 | 19 | import com.eclecticlogic.stepper.antlr.StepperBaseVisitor; 20 | 21 | public abstract class AbstractVisitor extends StepperBaseVisitor { 22 | 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/state/observer/NotificationReceiver.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015-2019 KR Abram 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.eclecticlogic.stepper.state.observer; 18 | 19 | import com.eclecticlogic.stepper.state.State; 20 | 21 | public interface NotificationReceiver { 22 | 23 | void receive(State state); 24 | } 25 | -------------------------------------------------------------------------------- /src/test/groovy/com/eclecticlogic/stepper/etc/TestEtc.groovy: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.etc 2 | 3 | 4 | import org.antlr.v4.runtime.CommonToken 5 | import org.antlr.v4.runtime.Token 6 | import org.antlr.v4.runtime.tree.TerminalNode 7 | import org.antlr.v4.runtime.tree.TerminalNodeImpl 8 | import spock.lang.Specification 9 | 10 | class TestEtc extends Specification { 11 | 12 | def "string from token"() { 13 | given: 14 | Token t1 = new CommonToken(1, "test") 15 | 16 | expect: 17 | Etc.from(t1) == "test" 18 | } 19 | 20 | 21 | def "string from terminal node"() { 22 | given: 23 | TerminalNode t = new TerminalNodeImpl(new CommonToken(1, "test")) 24 | 25 | expect: 26 | Etc.from(t) == "test" 27 | } 28 | 29 | 30 | def "strip quotes"() { 31 | expect: 32 | Etc.strip('"abc"') == 'abc' 33 | Etc.strip('"ab"c"') == 'ab"c' 34 | Etc.strip('""ab"c"') == '"ab"c' 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/visitor/DereferenceListener.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.visitor; 2 | 3 | import com.eclecticlogic.stepper.antlr.StepperBaseListener; 4 | import com.eclecticlogic.stepper.antlr.StepperParser; 5 | import com.google.common.collect.Lists; 6 | import com.google.common.collect.Sets; 7 | 8 | import java.util.List; 9 | import java.util.Set; 10 | 11 | public class DereferenceListener extends StepperBaseListener { 12 | 13 | private final Set references = Sets.newHashSet(); 14 | 15 | private final Set exclusions = Sets.newHashSet("Math", "Number"); 16 | 17 | List getReferences() { 18 | return Lists.newArrayList(references); 19 | } 20 | 21 | 22 | @Override 23 | public void enterDereference(StepperParser.DereferenceContext ctx) { 24 | String ref = ctx.ID(0).getText(); 25 | if (!exclusions.contains(ref)) { 26 | references.add(ctx.ID(0).getText()); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/resources/stepper/when.stg: -------------------------------------------------------------------------------- 1 | 2 | simple() ::= << 3 | stepper Simple { 4 | a = 5; 5 | when { 6 | case a \< 5: 7 | b = 1; 8 | @Label("abc") case a == 5: { 9 | b = 2; 10 | c = 1; 11 | } 12 | else { 13 | b = 3; 14 | c = 2; 15 | } 16 | } 17 | } 18 | >> 19 | 20 | nested() ::= << 21 | stepper Nested { 22 | when { 23 | case a \< 5: { 24 | c = 1; 25 | when { 26 | case c == 2: e = 1; 27 | @Label("x") case c == 3: e = 2; 28 | } 29 | } 30 | else { 31 | b = 3; 32 | when { 33 | @Label("a1") case b % 3 == 0: 34 | d = 1; 35 | else @Label("a2") d = 2; 36 | } 37 | } 38 | } 39 | } 40 | >> -------------------------------------------------------------------------------- /src/test/groovy/com/eclecticlogic/stepper/construct/TestConstruct.groovy: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.construct 2 | 3 | import com.eclecticlogic.stepper.etc.WeaveContext 4 | import com.eclecticlogic.stepper.state.State 5 | import spock.lang.Specification 6 | 7 | class TestConstruct extends Specification { 8 | 9 | class MockConstruct extends Construct { 10 | 11 | @Override 12 | List getStates() { 13 | return [] 14 | } 15 | 16 | @Override 17 | protected String getFirstStateName() { 18 | return null 19 | } 20 | 21 | @Override 22 | protected void setNextStateName(String name) { 23 | 24 | } 25 | 26 | @Override 27 | void weave(WeaveContext context) { 28 | 29 | } 30 | } 31 | 32 | def "get last in chain"() { 33 | given: 34 | Construct a = new MockConstruct() 35 | Construct b = new MockConstruct() 36 | Construct c = new MockConstruct() 37 | a.next = b 38 | b.next = c 39 | 40 | expect: 41 | a.getLastInChain(a) == c 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/construct/StateConstruct.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.construct; 2 | 3 | import com.eclecticlogic.stepper.etc.WeaveContext; 4 | import com.eclecticlogic.stepper.state.State; 5 | 6 | import java.util.List; 7 | 8 | public class StateConstruct extends Construct { 9 | 10 | private final T state; 11 | 12 | 13 | public StateConstruct(T state) { 14 | this.state = state; 15 | } 16 | 17 | 18 | protected T getState() { 19 | return state; 20 | } 21 | 22 | 23 | @Override 24 | protected String getFirstStateName() { 25 | return state.getName(); 26 | } 27 | 28 | 29 | @Override 30 | protected void setNextStateName(String name) { 31 | state.setNextState(name); 32 | } 33 | 34 | 35 | @Override 36 | public void weave(WeaveContext context) { 37 | if (getNext() != null) { 38 | state.setNextState(getNext().getFirstStateName()); 39 | getNext().weave(context); 40 | } 41 | } 42 | 43 | 44 | @Override 45 | public List getStates() { 46 | return getStates(state); 47 | } 48 | 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/etc/LambdaBranch.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.etc; 2 | 3 | import java.util.Collection; 4 | 5 | public class LambdaBranch { 6 | 7 | private String commandName; 8 | private Collection inputs; 9 | private String computation; 10 | private String outputExpression; 11 | 12 | public String getCommandName() { 13 | return commandName; 14 | } 15 | 16 | 17 | public void setCommandName(String commandName) { 18 | this.commandName = commandName; 19 | } 20 | 21 | 22 | public Collection getInputs() { 23 | return inputs; 24 | } 25 | 26 | 27 | public void setInputs(Collection inputs) { 28 | this.inputs = inputs; 29 | } 30 | 31 | 32 | public String getComputation() { 33 | return computation; 34 | } 35 | 36 | 37 | public void setComputation(String computation) { 38 | this.computation = computation; 39 | } 40 | 41 | 42 | public String getOutputExpression() { 43 | return outputExpression; 44 | } 45 | 46 | 47 | public void setOutputExpression(String outputExpression) { 48 | this.outputExpression = outputExpression; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/construct/ExpressionConstruct.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.construct; 2 | 3 | import com.eclecticlogic.stepper.etc.WeaveContext; 4 | import com.eclecticlogic.stepper.state.Task; 5 | 6 | import java.util.Collection; 7 | 8 | public class ExpressionConstruct extends StateConstruct { 9 | 10 | private String expression; 11 | private String variable; 12 | private Collection symbols; 13 | 14 | 15 | public ExpressionConstruct(String label) { 16 | super(new Task(label)); 17 | } 18 | 19 | 20 | public void setExpression(String expression) { 21 | this.expression = expression; 22 | } 23 | 24 | 25 | public void setVariable(String variable) { 26 | this.variable = variable; 27 | } 28 | 29 | 30 | public void setSymbols(Collection symbols) { 31 | this.symbols = symbols; 32 | } 33 | 34 | 35 | @Override 36 | public void weave(WeaveContext context) { 37 | constructLambda(context, getState(), expression, symbols); 38 | Task task = getState(); 39 | task.setResultPath("$." + variable); 40 | 41 | task.setupLambdaHelper(); 42 | super.weave(context); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/resources/stepper/if.stg: -------------------------------------------------------------------------------- 1 | 2 | simpleIf() ::= << 3 | stepper Simple { 4 | a = 5; 5 | @Label("abc") 6 | if (a \< 5) { 7 | b = 6; 8 | d = 10; 9 | } 10 | c = 7; 11 | } 12 | >> 13 | 14 | ifelse() ::= << 15 | stepper IfElse { 16 | a = 5; 17 | @Label("abc") 18 | if (a \< 5) { 19 | b = 6; 20 | d = 0; 21 | } else { 22 | b = 4; 23 | d = 1; 24 | } 25 | c = 7; 26 | } 27 | >> 28 | 29 | ifelseif() ::= << 30 | stepper IfElse { 31 | a = 5; 32 | @Label("abc") 33 | if (a \< 5) { 34 | b = 6; 35 | d = 0; 36 | } else @Label("def") if (a > 10) { 37 | b = 4; 38 | d = 1; 39 | } else { 40 | b = 10; 41 | d = 2; 42 | } 43 | c = 7; 44 | } 45 | >> 46 | 47 | nestedif() ::= << 48 | stepper IfElse { 49 | a = 5; 50 | @Label("abc") 51 | if (a \< 5) 52 | @Label("def") 53 | if (a \< 2) b = 3; 54 | else b = 5; 55 | else 56 | @Label("pqr") 57 | if (a > 12) b = 13; 58 | else b = 15; 59 | c = 7; 60 | } 61 | >> -------------------------------------------------------------------------------- /src/main/resources/stepper/template/lambda.stg: -------------------------------------------------------------------------------- 1 | 2 | scaffolding(code) ::= << 3 | 4 | exports.handler = async (event) => { 5 | 6 | 7 | 8 | else return {"error": "no branch matched"}; 9 | }; 10 | 11 | >> 12 | 13 | 14 | branch(call, other) ::= << 15 | 16 | if (event.cmd__sm == "") { 17 | 18 | 19 | 20 | 21 | 22 | const response = ; 23 | 24 | const response = { 25 | 26 | }; 27 | 28 | return response; 29 | } 30 | 31 | 32 | >> 33 | 34 | 35 | genInputs(input, other) ::= << 36 | var = event.; 37 | 38 | >> 39 | 40 | 41 | forIterationBody(expr, index) ::= << 42 | var i = .idx + 1; 43 | var r = null; 44 | if (i \< .length) { 45 | r = { "idx": i, "var": [i], "exists": true } 46 | } else { 47 | r = { "exists": false } 48 | } 49 | >> -------------------------------------------------------------------------------- /src/test/java/com/eclecticlogic/stepper/antlr/AbstractGrammarTester.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.antlr; 2 | 3 | import com.eclecticlogic.stepper.Stepper; 4 | import org.antlr.v4.runtime.CharStreams; 5 | import org.antlr.v4.runtime.ParserRuleContext; 6 | import org.antlr.v4.runtime.tree.ParseTreeWalker; 7 | import org.stringtemplate.v4.ST; 8 | import org.stringtemplate.v4.STGroup; 9 | import org.stringtemplate.v4.STGroupFile; 10 | 11 | import java.util.function.Function; 12 | 13 | public abstract class AbstractGrammarTester { 14 | 15 | public static class TestStepperListener extends StepperBaseListener { 16 | 17 | } 18 | 19 | 20 | protected void parse(String groupFile, String name, Function parserFunction) { 21 | STGroup group = new STGroupFile("antlr/grammar/" + groupFile); 22 | ST st = group.getInstanceOf(name); 23 | parse(st.render(), parserFunction); 24 | } 25 | 26 | 27 | protected void parse(String text, Function parserFunction) { 28 | StepperParser parser = Stepper.createParser(CharStreams.fromString(text)); 29 | TestStepperListener listener = new TestStepperListener(); 30 | ParseTreeWalker.DEFAULT.walk(listener, parserFunction.apply(parser)); 31 | } 32 | 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/test/resources/stepper/trycatch.stg: -------------------------------------------------------------------------------- 1 | 2 | simple0() ::= << 3 | stepper Simple { 4 | a = 5; 5 | try { 6 | b = 3; 7 | } catch ("e1", "e2") { errorInfo -> 8 | c = 3; 9 | fail; 10 | } 11 | } 12 | >> 13 | 14 | 15 | simple1() ::= << 16 | stepper Simple { 17 | a = 5; 18 | try { 19 | b = 3 * a; 20 | } catch ("e1", "e2") { errorInfo -> 21 | c = 3; 22 | fail; 23 | } 24 | } 25 | >> 26 | 27 | 28 | simple2() ::= << 29 | stepper Simple { 30 | a = 5; 31 | try { 32 | b = 3 * a; 33 | } catch ("e1", "e2") { errorInfo -> 34 | c = 3; 35 | goto "g1"; 36 | } catch ("e3", "e4") { 37 | fail; 38 | } 39 | @Label("g1") 40 | d = 10; 41 | } 42 | >> 43 | 44 | 45 | nested() ::= << 46 | stepper Nested { 47 | try { 48 | a = 5; 49 | try { 50 | b = 3 * a; 51 | } catch ("e1", "e2") { errorInfo -> 52 | c = 3; 53 | goto "g1"; 54 | } 55 | @Label("g1") 56 | d = 10; 57 | } catch ("e0") { 58 | x = 10; 59 | fail; 60 | } 61 | y = 5; 62 | } 63 | >> -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/state/Choice.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.state; 2 | 3 | import com.google.gson.JsonObject; 4 | 5 | public class Choice extends AttributableState { 6 | 7 | private JsonObject objIf, objElse; 8 | 9 | public Choice(String variable) { 10 | setType(StateType.CHOICE); 11 | captureAttribute("Choices"); 12 | handleArray(() -> { 13 | // if 14 | objIf = handleObject(() -> { 15 | captureAttribute("Variable"); 16 | setProperty("$." + variable); 17 | captureAttribute("BooleanEquals"); 18 | setProperty(true); 19 | }); 20 | 21 | // else 22 | objElse = handleObject(() -> { 23 | captureAttribute("Variable"); 24 | setProperty("$." + variable); 25 | captureAttribute("BooleanEquals"); 26 | setProperty(false); 27 | }); 28 | }); 29 | } 30 | 31 | 32 | public void setIfNextState(String value) { 33 | objIf.addProperty("Next", value); 34 | } 35 | 36 | 37 | public void setElseNextState(String value) { 38 | objElse.addProperty("Next", value); 39 | } 40 | 41 | 42 | public JsonObject getObjIf() { 43 | return objIf; 44 | } 45 | 46 | 47 | public JsonObject getObjElse() { 48 | return objElse; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/construct/CatchClause.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015-2019 KR Abram 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.eclecticlogic.stepper.construct; 18 | 19 | import java.util.List; 20 | 21 | public class CatchClause { 22 | 23 | private List errors; 24 | private Construct block; 25 | private String resultPath; 26 | 27 | 28 | public List getErrors() { 29 | return errors; 30 | } 31 | 32 | 33 | public void setErrors(List errors) { 34 | this.errors = errors; 35 | } 36 | 37 | 38 | public Construct getBlock() { 39 | return block; 40 | } 41 | 42 | 43 | public void setBlock(Construct block) { 44 | this.block = block; 45 | } 46 | 47 | 48 | public String getResultPath() { 49 | return resultPath; 50 | } 51 | 52 | 53 | public void setResultPath(String resultPath) { 54 | this.resultPath = resultPath; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/etc/Etc.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.etc; 2 | 3 | import com.eclecticlogic.stepper.antlr.StepperParser; 4 | import org.antlr.v4.runtime.Token; 5 | import org.antlr.v4.runtime.tree.TerminalNode; 6 | 7 | 8 | public class Etc { 9 | 10 | public static String from(Token token) { 11 | return token == null ? null : token.getText(); 12 | } 13 | 14 | 15 | public static String from(TerminalNode token) { 16 | return token == null ? null : token.getText(); 17 | } 18 | 19 | 20 | public static String strip(String input) { 21 | return input == null ? null : input.substring(1).substring(0, input.length() - 2); 22 | } 23 | 24 | 25 | public static String enhance(StepperParser.ComplexAssignContext cactx, String expression, String variable) { 26 | if (cactx.ASSIGN() != null) { 27 | return expression; 28 | } else { 29 | String symbol; 30 | if (cactx.PLUSASSIGN() != null) { 31 | symbol = "+"; 32 | } else if (cactx.MINUSASSIGN() != null) { 33 | symbol = "-"; 34 | } else if (cactx.MULTASSIGN() != null) { 35 | symbol = "*"; 36 | } else { 37 | symbol = "/"; 38 | } 39 | return variable + " " + symbol + " (" + expression + ")"; 40 | } 41 | } 42 | 43 | 44 | public static String toLabel(StepperParser.LabelContext ctx) { 45 | return ctx == null ? null : strip(ctx.STRING().getText()); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/construct/WhenCase.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.construct; 2 | 3 | import com.eclecticlogic.stepper.state.Choice; 4 | import com.eclecticlogic.stepper.state.Task; 5 | 6 | import java.util.Set; 7 | 8 | public class WhenCase { 9 | 10 | private Set symbols; 11 | private String expression; 12 | private Construct block; 13 | 14 | private final Task task; 15 | private final String choiceVariable; 16 | private final Choice choice; 17 | 18 | 19 | public WhenCase(String label, String variable) { 20 | task = new Task(label); 21 | choiceVariable = variable; 22 | choice = new Choice(choiceVariable); 23 | } 24 | 25 | 26 | public Task getTask() { 27 | return task; 28 | } 29 | 30 | 31 | public String getChoiceVariable() { 32 | return choiceVariable; 33 | } 34 | 35 | 36 | public Choice getChoice() { 37 | return choice; 38 | } 39 | 40 | 41 | public Set getSymbols() { 42 | return symbols; 43 | } 44 | 45 | 46 | public void setSymbols(Set symbols) { 47 | this.symbols = symbols; 48 | } 49 | 50 | 51 | public String getExpression() { 52 | return expression; 53 | } 54 | 55 | 56 | public void setExpression(String expression) { 57 | this.expression = expression; 58 | } 59 | 60 | 61 | public Construct getBlock() { 62 | return block; 63 | } 64 | 65 | 66 | public void setBlock(Construct block) { 67 | this.block = block; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/visitor/StatementBlockVisitor.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015-2019 KR Abram 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.eclecticlogic.stepper.visitor; 18 | 19 | import com.eclecticlogic.stepper.antlr.StepperParser; 20 | import com.eclecticlogic.stepper.construct.Construct; 21 | 22 | public class StatementBlockVisitor extends AbstractVisitor { 23 | 24 | 25 | @Override 26 | public Construct visitStatementBlock(StepperParser.StatementBlockContext ctx) { 27 | Construct first = null; 28 | Construct current = null; 29 | for (StepperParser.StatementContext stCtx : ctx.statement()) { 30 | StatementVisitor visitor = new StatementVisitor(); 31 | Construct c = visitor.visit(stCtx); 32 | if (first == null) { 33 | first = c; 34 | current = c; 35 | } else { 36 | current.setNext(c); 37 | current = c; 38 | } 39 | } 40 | return first; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/state/observer/StateObserver.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015-2019 KR Abram 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.eclecticlogic.stepper.state.observer; 18 | 19 | import com.eclecticlogic.stepper.state.State; 20 | 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | import java.util.Stack; 24 | 25 | public class StateObserver { 26 | 27 | private static final ThreadLocal> observers = new ThreadLocal<>(); 28 | 29 | private final List receiverList = new ArrayList<>(); 30 | 31 | static { 32 | observers.set(new Stack<>()); 33 | } 34 | 35 | 36 | public static void over(Runnable runnable) { 37 | observers.get().push(new StateObserver()); 38 | runnable.run(); 39 | observers.get().pop(); 40 | } 41 | 42 | 43 | public static void register(NotificationReceiver receiver, Runnable runnable) { 44 | observers.get().peek().receiverList.add(receiver); 45 | runnable.run(); 46 | observers.get().peek().receiverList.remove(receiver); 47 | } 48 | 49 | 50 | public static void event(State state) { 51 | observers.get().peek().receiverList.forEach(r -> r.receive(state)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/install/InstallConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015-2019 KR Abram 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.eclecticlogic.stepper.install; 18 | 19 | public class InstallConfig { 20 | 21 | private Lambda lambda; 22 | private StepFunction stepFunction; 23 | 24 | 25 | public StepFunction getStepFunction() { 26 | return stepFunction; 27 | } 28 | 29 | 30 | public void setStepFunction(StepFunction stepFunction) { 31 | this.stepFunction = stepFunction; 32 | } 33 | 34 | 35 | public Lambda getLambda() { 36 | return lambda; 37 | } 38 | 39 | 40 | public void setLambda(Lambda lambda) { 41 | this.lambda = lambda; 42 | } 43 | 44 | 45 | public static class StepFunction { 46 | private String executionRole; 47 | 48 | 49 | public String getExecutionRole() { 50 | return executionRole; 51 | } 52 | 53 | 54 | public void setExecutionRole(String executionRole) { 55 | this.executionRole = executionRole; 56 | } 57 | } 58 | 59 | public static class Lambda { 60 | private String executionRole; 61 | 62 | 63 | public String getExecutionRole() { 64 | return executionRole; 65 | } 66 | 67 | 68 | public void setExecutionRole(String executionRole) { 69 | this.executionRole = executionRole; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/visitor/RetryVisitor.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.visitor; 2 | 3 | import com.eclecticlogic.stepper.antlr.StepperBaseVisitor; 4 | import com.eclecticlogic.stepper.antlr.StepperParser; 5 | import com.eclecticlogic.stepper.etc.Etc; 6 | import com.eclecticlogic.stepper.state.AttributableState; 7 | import org.antlr.v4.runtime.tree.TerminalNode; 8 | 9 | import java.util.List; 10 | import java.util.function.Function; 11 | 12 | import static com.eclecticlogic.stepper.etc.Etc.strip; 13 | 14 | public class RetryVisitor extends StepperBaseVisitor { 15 | 16 | private T state; 17 | private final Function stateCreator; 18 | 19 | 20 | public RetryVisitor(Function stateCreator) { 21 | this.stateCreator = stateCreator; 22 | } 23 | 24 | 25 | @Override 26 | public T visitRetries(StepperParser.RetriesContext ctx) { 27 | String name = ctx.label() == null ? null : strip(ctx.label().STRING().getText()); 28 | state = stateCreator.apply(name); 29 | 30 | if (!ctx.retry().isEmpty()) { 31 | setupRetries(ctx.retry()); 32 | } 33 | return state; 34 | } 35 | 36 | 37 | void setupRetries(List ctx) { 38 | state.captureAttribute("Retry"); 39 | state.handleArray(() -> { 40 | for (StepperParser.RetryContext rc : ctx) { 41 | state.handleObject(() -> { 42 | state.captureAttribute("ErrorEquals"); 43 | state.handleArray(() -> rc.STRING().stream()// 44 | .map(TerminalNode::getText) // 45 | .map(Etc::strip) // 46 | .forEach(state::setProperty)); 47 | JsonObjectVisitor visitor = new JsonObjectVisitor(state); 48 | visitor.visit(rc.jsonObject()); 49 | }); 50 | } 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/StateMachine.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015-2019 KR Abram 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.eclecticlogic.stepper; 18 | 19 | import com.eclecticlogic.stepper.construct.ProgramConstruct; 20 | import com.google.gson.Gson; 21 | import com.google.gson.GsonBuilder; 22 | import com.google.gson.JsonObject; 23 | import org.stringtemplate.v4.ST; 24 | import org.stringtemplate.v4.STGroup; 25 | import org.stringtemplate.v4.STGroupFile; 26 | 27 | public class StateMachine { 28 | private final ProgramConstruct program; 29 | 30 | 31 | public StateMachine(ProgramConstruct program) { 32 | this.program = program; 33 | } 34 | 35 | 36 | public String getName() { 37 | return program.getProgramName(); 38 | } 39 | 40 | 41 | public String getAsl() { 42 | Gson gson = new GsonBuilder().setPrettyPrinting().create(); 43 | return gson.toJson(toJson()); 44 | } 45 | 46 | 47 | public boolean isLambdaRequired() { 48 | return !program.getContext().getLambdaHelper().getBranches().isEmpty(); 49 | } 50 | 51 | 52 | public String getLambda() { 53 | STGroup group = new STGroupFile("stepper/template/lambda.stg"); 54 | ST st = group.getInstanceOf("scaffolding"); 55 | st.add("code", program.getContext().getLambdaHelper()); 56 | return st.render(); 57 | } 58 | 59 | 60 | public JsonObject toJson() { 61 | return program.toJson(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/resources/stepper/basic.stg: -------------------------------------------------------------------------------- 1 | 2 | simple() ::= << 3 | stepper Simple { 4 | @Label("xyz") a = "Hello World"; 5 | } 6 | >> 7 | 8 | annotationName() ::= << 9 | @Comment("this is a comment") 10 | @TimeoutSeconds(3600) 11 | @Version("1.0") 12 | stepper AnnotationTest { 13 | c = 5; 14 | } 15 | >> 16 | 17 | assignmentPrimitive() ::= << 18 | stepper assignment { 19 | @Label("abc") a = 5.2; 20 | b = 10; 21 | c = "Hello World"; 22 | d.e.f = true; 23 | @Label("xyz") e.f.g = false; 24 | } 25 | >> 26 | 27 | assignmentComplex() ::= << 28 | stepper complex { 29 | value1 = a * b + c.calc(e) - d.length(); 30 | @Label("one") value2 += a * b + c.calc(e) - d.length(); 31 | value3 -= a * b + c.calc(e) - d.length(); 32 | value4 /= a * b + c.calc(e) - d.length(); 33 | @Label("two") value5 *= a * b + c.calc(e) - d.length(); 34 | } 35 | >> 36 | 37 | assignmentJson() ::= << 38 | stepper json { 39 | @Label("xyz") value = { 40 | "a": 5, 41 | "b": 10, 42 | "c": { 43 | "d": "x" 44 | }, 45 | "e": ["p", true, false] 46 | } 47 | } 48 | >> 49 | 50 | assignmentJsonArray() ::= << 51 | stepper array { 52 | @Label("xyz") value = ["p", true, false, 2.5, 10] 53 | } 54 | >> 55 | 56 | assignmentTask() ::= << 57 | stepper mytask { 58 | @Label("x") 59 | value = task { 60 | "a": "b" 61 | } 62 | } 63 | >> 64 | 65 | task() ::= << 66 | 67 | stepper mytask { 68 | @RetryOnError("abc", "def") { "IntervalSeconds": 3, "MaxAttempts": 4, "BackoffRate": 5 } 69 | @RetryOnError("pqr") { "IntervalSeconds": 6 } 70 | @Label("hello") 71 | task { 72 | "a": "b" 73 | } 74 | } 75 | >> 76 | 77 | wait() ::= << 78 | stepper waiter { 79 | @Label("abc") 80 | wait { 81 | "Seconds": 10 82 | } 83 | } 84 | >> 85 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/state/AbstractState.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015-2019 KR Abram 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.eclecticlogic.stepper.state; 18 | 19 | import com.eclecticlogic.stepper.state.observer.StateObserver; 20 | import com.google.gson.JsonObject; 21 | 22 | import static com.eclecticlogic.stepper.etc.Constants.LAMBDA_ARN_PLACEHOLDER; 23 | 24 | public abstract class AbstractState implements State { 25 | 26 | private String stateName; 27 | 28 | JsonObject json = new JsonObject(); 29 | 30 | 31 | AbstractState(String stateName) { 32 | this.stateName = stateName == null ? NameProvider.getName() : stateName; 33 | StateObserver.event(this); 34 | } 35 | 36 | 37 | @Override 38 | public String getName() { 39 | return stateName; 40 | } 41 | 42 | 43 | @Override 44 | public JsonObject toJson() { 45 | return json; 46 | } 47 | 48 | 49 | void setType(StateType type) { 50 | json.addProperty("Type", type.getName()); 51 | } 52 | 53 | 54 | public void setupLambdaHelper() { 55 | json.addProperty("Resource", LAMBDA_ARN_PLACEHOLDER); 56 | } 57 | 58 | 59 | public void setResultPath(String value) { 60 | setResultPath(value, true); 61 | } 62 | 63 | 64 | public void setResultPath(String value, boolean overwrite) { 65 | if (overwrite || json.get("ResultPath") == null) { 66 | json.addProperty("ResultPath", value); 67 | } 68 | } 69 | 70 | 71 | public void setNextState(String value) { 72 | json.addProperty("Next", value); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/construct/ProgramConstruct.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.construct; 2 | 3 | import com.eclecticlogic.stepper.etc.WeaveContext; 4 | import com.eclecticlogic.stepper.state.State; 5 | import com.google.gson.JsonObject; 6 | 7 | import java.util.List; 8 | 9 | import static com.eclecticlogic.stepper.etc.Etc.strip; 10 | 11 | public class ProgramConstruct extends Construct { 12 | 13 | private final JsonObject stepFunction = new JsonObject(); 14 | private String programName; 15 | private WeaveContext context; 16 | 17 | 18 | @Override 19 | public String getFirstStateName() { 20 | return getNext().getFirstStateName(); 21 | } 22 | 23 | 24 | @Override 25 | protected void setNextStateName(String name) { 26 | // noop 27 | } 28 | 29 | 30 | @Override 31 | public List getStates() { 32 | return getNext().getStates(); 33 | } 34 | 35 | 36 | @Override 37 | public void weave(WeaveContext context) { 38 | this.context = context; 39 | getNext().weave(context); 40 | stepFunction.addProperty("StartAt", getFirstStateName()); 41 | 42 | JsonObject states = new JsonObject(); 43 | getStates().stream().forEach(state -> states.add(state.getName(), state.toJson())); 44 | stepFunction.add("States", states); 45 | } 46 | 47 | 48 | public void setProgramName(String programName) { 49 | this.programName = programName; 50 | } 51 | 52 | 53 | public String getProgramName() { 54 | return programName; 55 | } 56 | 57 | 58 | public void addAnnotation(String name, String value) { 59 | stepFunction.addProperty(name, strip(value)); 60 | } 61 | 62 | 63 | public void addAnnotation(String name, Number value) { 64 | stepFunction.addProperty(name, value); 65 | } 66 | 67 | 68 | public void addAnnotation(String name, boolean value) { 69 | stepFunction.addProperty(name, value); 70 | } 71 | 72 | 73 | public WeaveContext getContext() { 74 | return context; 75 | } 76 | 77 | 78 | public JsonObject toJson() { 79 | return stepFunction; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/test/groovy/com/eclecticlogic/stepper/asl/AbstractStateMachineTester.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015-2019 KR Abram 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.eclecticlogic.stepper.asl 18 | 19 | import com.eclecticlogic.stepper.StateMachine 20 | import com.eclecticlogic.stepper.Stepper 21 | import com.eclecticlogic.stepper.antlr.StepperParser 22 | import com.eclecticlogic.stepper.visitor.StepperVisitor 23 | import com.jayway.jsonpath.JsonPath 24 | import com.jayway.jsonpath.ReadContext 25 | import org.antlr.v4.runtime.CharStreams 26 | import org.stringtemplate.v4.ST 27 | import org.stringtemplate.v4.STGroup 28 | import org.stringtemplate.v4.STGroupFile 29 | import spock.lang.Specification 30 | 31 | abstract class AbstractStateMachineTester extends Specification { 32 | 33 | 34 | StateMachine _run(String groupFile, String name) { 35 | STGroup group = new STGroupFile('stepper/' + groupFile) 36 | ST st = group.getInstanceOf(name) 37 | 38 | StepperParser parser = Stepper.createParser(CharStreams.fromString(st.render())); 39 | StepperVisitor visitor = new StepperVisitor() 40 | 41 | return visitor.visitProgram(parser.program()) 42 | } 43 | 44 | 45 | ReadContext run(String groupFile, String name) { 46 | StateMachine machine = _run(groupFile, name) 47 | String json = machine.asl 48 | println(json) 49 | return JsonPath.parse(json) 50 | } 51 | 52 | 53 | class TestOutput { 54 | ReadContext ctx 55 | String lambda 56 | } 57 | 58 | 59 | TestOutput runWithLambda(String groupFile, String name) { 60 | StateMachine machine = _run(groupFile, name) 61 | String json = machine.asl 62 | println(json) 63 | println(machine.lambda) 64 | return new TestOutput().with { 65 | ctx = JsonPath.parse(json) 66 | lambda = machine.lambda 67 | return it 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/construct/WhileConstruct.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.construct; 2 | 3 | import com.eclecticlogic.stepper.etc.WeaveContext; 4 | import com.eclecticlogic.stepper.state.Choice; 5 | import com.eclecticlogic.stepper.state.State; 6 | import com.eclecticlogic.stepper.state.Task; 7 | import com.google.common.collect.Lists; 8 | 9 | import java.util.List; 10 | import java.util.Set; 11 | 12 | public class WhileConstruct extends Construct { 13 | private String expression; 14 | private Set symbols; 15 | private Construct block; 16 | 17 | private final Task conditionLambda; 18 | private final String choiceVar; 19 | final Choice choice; 20 | 21 | 22 | public WhileConstruct(String label) { 23 | conditionLambda = new Task(label); 24 | choiceVar = getNextDynamicVariable(); 25 | choice = new Choice(choiceVar); 26 | } 27 | 28 | 29 | public void setExpression(String expression) { 30 | this.expression = expression; 31 | } 32 | 33 | 34 | public void setSymbols(Set symbols) { 35 | this.symbols = symbols; 36 | } 37 | 38 | 39 | public void setBlock(Construct block) { 40 | this.block = block; 41 | } 42 | 43 | 44 | void setupConditionLambda(WeaveContext context) { 45 | constructLambda(context, conditionLambda, expression, symbols); 46 | 47 | conditionLambda.setupLambdaHelper(); 48 | conditionLambda.setResultPath("$." + choiceVar); 49 | conditionLambda.setNextState(choice.getName()); 50 | } 51 | 52 | 53 | void setupChoice() { 54 | choice.setIfNextState(block.getFirstStateName()); 55 | getLastInChain(block).setNextStateName(conditionLambda.getName()); 56 | } 57 | 58 | 59 | @Override 60 | protected String getFirstStateName() { 61 | return conditionLambda.getName(); 62 | } 63 | 64 | 65 | @Override 66 | protected void setNextStateName(String name) { 67 | choice.setElseNextState(name); 68 | } 69 | 70 | 71 | @Override 72 | public void weave(WeaveContext context) { 73 | setupConditionLambda(context); 74 | setupChoice(); 75 | block.weave(context); 76 | if (getNext() != null) { 77 | getNext().weave(context); 78 | setNextStateName(getNext().getFirstStateName()); 79 | } 80 | } 81 | 82 | 83 | @Override 84 | public List getStates() { 85 | List result = Lists.newArrayList(conditionLambda, choice); 86 | result.addAll(block.getStates()); 87 | if (getNext() != null) { 88 | result.addAll(getNext().getStates()); 89 | } 90 | return result; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/test/groovy/com/eclecticlogic/stepper/asl/TestFail.groovy: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.asl 2 | 3 | import com.jayway.jsonpath.ReadContext 4 | 5 | 6 | class TestFail extends AbstractStateMachineTester { 7 | 8 | def "test simple fail"() { 9 | given: 10 | TestOutput output = runWithLambda('fail.stg', 'simple') 11 | ReadContext ctx = output.ctx 12 | 13 | when: 14 | Closure v = { name, param -> 15 | return ctx.read('$.States.' + name + '.' + param) 16 | } 17 | 18 | then: 19 | Object[] data = ctx.read('$.States.*') 20 | data.length == 5 21 | 22 | ctx.read('$.StartAt') == 'Fail000' 23 | 24 | with('Fail000') { 25 | v(it, 'Type') == 'Pass' 26 | v(it, 'Result') == 5 27 | v(it, 'ResultPath') == '$.a' 28 | v(it, 'Next') == 'Fail001' 29 | } 30 | 31 | with('Fail001') { 32 | v(it, 'Type') == 'Fail' 33 | } 34 | 35 | with('Fail002') { 36 | v(it, 'Type') == 'Pass' 37 | v(it, 'Result') == 7 38 | v(it, 'ResultPath') == '$.b' 39 | v(it, 'Next') == 'Fail003' 40 | } 41 | 42 | with('Fail003') { 43 | v(it, 'Type') == 'Pass' 44 | v(it, 'Result') == 5 45 | v(it, 'ResultPath') == '$.d' 46 | v(it, 'Next') == 'Fail.Success' 47 | } 48 | 49 | v("['Fail.Success']", 'Type') == 'Succeed' 50 | } 51 | 52 | 53 | def "test fail with attributes"() { 54 | given: 55 | TestOutput output = runWithLambda('fail.stg', 'failWithAttributes') 56 | ReadContext ctx = output.ctx 57 | 58 | when: 59 | Closure v = { name, param -> 60 | return ctx.read('$.States.' + name + '.' + param) 61 | } 62 | 63 | then: 64 | Object[] data = ctx.read('$.States.*') 65 | data.length == 5 66 | 67 | ctx.read('$.StartAt') == 'Fail000' 68 | 69 | with('Fail000') { 70 | v(it, 'Type') == 'Pass' 71 | v(it, 'Result') == 5 72 | v(it, 'ResultPath') == '$.a' 73 | v(it, 'Next') == 'a' 74 | } 75 | 76 | with('a') { 77 | v(it, 'Type') == 'Fail' 78 | v(it, 'Cause') == 'blah1' 79 | v(it, 'Error') == 'blah2' 80 | } 81 | 82 | with('Fail001') { 83 | v(it, 'Type') == 'Pass' 84 | v(it, 'Result') == 7 85 | v(it, 'ResultPath') == '$.b' 86 | v(it, 'Next') == 'Fail002' 87 | } 88 | 89 | with('Fail002') { 90 | v(it, 'Type') == 'Pass' 91 | v(it, 'Result') == 5 92 | v(it, 'ResultPath') == '$.d' 93 | v(it, 'Next') == 'Fail.Success' 94 | } 95 | 96 | v("['Fail.Success']", 'Type') == 'Succeed' 97 | } 98 | } -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/visitor/ForVisitor.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015-2019 KR Abram 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.eclecticlogic.stepper.visitor; 18 | 19 | import com.eclecticlogic.stepper.antlr.StepperParser; 20 | import com.eclecticlogic.stepper.construct.Construct; 21 | import com.eclecticlogic.stepper.construct.ForIterationConstruct; 22 | import com.eclecticlogic.stepper.construct.ForLoopConstruct; 23 | 24 | import static com.eclecticlogic.stepper.etc.Etc.toLabel; 25 | 26 | public class ForVisitor extends AbstractVisitor { 27 | 28 | 29 | @Override 30 | public Construct visitForIteration(StepperParser.ForIterationContext ctx) { 31 | ForIterationConstruct construct = new ForIterationConstruct(toLabel(ctx.label())); 32 | construct.setIterableVariable(ctx.ID().getText()); 33 | construct.setIterableExpression(ctx.iterable.getText()); 34 | 35 | DereferencingVisitor defVisitor = new DereferencingVisitor(); 36 | construct.setSymbols(defVisitor.visit(ctx.iterable)); 37 | 38 | StatementBlockVisitor visitor = new StatementBlockVisitor(); 39 | construct.setBlock(visitor.visit(ctx.statementBlock())); 40 | 41 | return construct; 42 | } 43 | 44 | 45 | @Override 46 | public Construct visitForLoop(StepperParser.ForLoopContext ctx) { 47 | ForLoopConstruct construct = new ForLoopConstruct(toLabel(ctx.label())); 48 | construct.setIterableVariable(ctx.ID().getText()); 49 | 50 | construct.setInitialExpression(ctx.init.getText()); 51 | { 52 | DereferencingVisitor defVisitor = new DereferencingVisitor(); 53 | construct.setInitialExpressionSymbols(defVisitor.visit(ctx.init)); 54 | } 55 | 56 | construct.setEndingExpression(ctx.end.getText()); 57 | { 58 | DereferencingVisitor defVisitor = new DereferencingVisitor(); 59 | construct.setEndingExpressionSymbols(defVisitor.visit(ctx.end)); 60 | } 61 | 62 | if (ctx.delta != null) { 63 | construct.setStepExpression(ctx.delta.getText()); 64 | DereferencingVisitor defVisitor = new DereferencingVisitor(); 65 | construct.setStepExpressionSymbols(defVisitor.visit(ctx.delta)); 66 | } 67 | 68 | StatementBlockVisitor visitor = new StatementBlockVisitor(); 69 | construct.setBlock(visitor.visit(ctx.statementBlock())); 70 | 71 | return construct; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/visitor/JsonObjectVisitor.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.visitor; 2 | 3 | import com.eclecticlogic.stepper.antlr.StepperBaseVisitor; 4 | import com.eclecticlogic.stepper.antlr.StepperParser; 5 | import com.eclecticlogic.stepper.state.AttributableState; 6 | import com.eclecticlogic.stepper.state.Task; 7 | 8 | import java.text.NumberFormat; 9 | import java.text.ParseException; 10 | 11 | import static com.eclecticlogic.stepper.etc.Etc.from; 12 | import static com.eclecticlogic.stepper.etc.Etc.strip; 13 | 14 | 15 | public class JsonObjectVisitor extends StepperBaseVisitor { 16 | 17 | private AttributableState state; 18 | 19 | 20 | public JsonObjectVisitor(AttributableState state) { 21 | this.state = state; 22 | } 23 | 24 | 25 | @Override 26 | public AttributableState visitJsonObject(StepperParser.JsonObjectContext ctx) { 27 | ctx.pair().forEach(this::visit); 28 | return state; 29 | } 30 | 31 | 32 | @Override 33 | public AttributableState visitPair(StepperParser.PairContext ctx) { 34 | state.captureAttribute(strip(from(ctx.STRING()))); 35 | visit(ctx.value()); 36 | return null; 37 | } 38 | 39 | 40 | @Override 41 | public Task visitValueString(StepperParser.ValueStringContext ctx) { 42 | state.setProperty(strip(from(ctx.STRING()))); 43 | return null; 44 | } 45 | 46 | 47 | @Override 48 | public Task visitValueNum(StepperParser.ValueNumContext ctx) { 49 | try { 50 | state.setProperty(NumberFormat.getInstance().parse(ctx.NUMBER().getText())); 51 | } catch (ParseException e) { 52 | throw new RuntimeException(e); 53 | } 54 | return null; 55 | } 56 | 57 | 58 | @Override 59 | public Task visitValueTrue(StepperParser.ValueTrueContext ctx) { 60 | state.setProperty(true); 61 | return null; 62 | } 63 | 64 | 65 | @Override 66 | public Task visitValueFalse(StepperParser.ValueFalseContext ctx) { 67 | state.setProperty(false); 68 | return null; 69 | } 70 | 71 | 72 | @Override 73 | public Task visitValueObj(StepperParser.ValueObjContext ctx) { 74 | state.handleObject(() -> ctx.pair().forEach(this::visit)); 75 | return null; 76 | } 77 | 78 | 79 | @Override 80 | public Task visitValueObjEmpty(StepperParser.ValueObjEmptyContext ctx) { 81 | state.handleObject(() -> { 82 | }); 83 | return null; 84 | } 85 | 86 | 87 | @Override 88 | public Task visitValueArr(StepperParser.ValueArrContext ctx) { 89 | state.handleArray(() -> ctx.value().forEach(this::visit)); 90 | return null; 91 | } 92 | 93 | 94 | @Override 95 | public Task visitValueArrEmpty(StepperParser.ValueArrEmptyContext ctx) { 96 | state.handleArray(() -> { 97 | }); 98 | return null; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/construct/Construct.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.construct; 2 | 3 | import com.eclecticlogic.stepper.etc.LambdaBranch; 4 | import com.eclecticlogic.stepper.etc.WeaveContext; 5 | import com.eclecticlogic.stepper.state.NameProvider; 6 | import com.eclecticlogic.stepper.state.State; 7 | import com.eclecticlogic.stepper.state.Task; 8 | import com.google.common.collect.Lists; 9 | 10 | import java.util.Collection; 11 | import java.util.List; 12 | 13 | import static com.eclecticlogic.stepper.etc.Constants.COMMAND_VAR; 14 | 15 | public abstract class Construct { 16 | 17 | private static int dynamicVariableCounter; 18 | private Construct next; 19 | 20 | 21 | /** 22 | * @return Should return all the states in the given construct and states in the construct that follows this. 23 | */ 24 | public abstract List getStates(); 25 | 26 | 27 | Construct getNext() { 28 | return next; 29 | } 30 | 31 | public void setNext(Construct value) { 32 | next = value; 33 | } 34 | 35 | 36 | String getNextDynamicVariable() { 37 | return NameProvider.getVar(); 38 | } 39 | 40 | 41 | List getStates(State... first) { 42 | List result = Lists.newArrayList(first); 43 | if (next != null) { 44 | result.addAll(next.getStates()); 45 | } 46 | return result; 47 | } 48 | 49 | 50 | protected abstract String getFirstStateName(); 51 | 52 | 53 | protected abstract void setNextStateName(String name); 54 | 55 | 56 | /** 57 | * @param context Implementors are expected to wire up the ASL json nodes and then call weave on any 58 | * constructs they directly manage and call weave on the next construct (getNext() != null). 59 | */ 60 | public abstract void weave(WeaveContext context); 61 | 62 | 63 | /** 64 | * @param input first construct in a chain 65 | * @return The last construct in a chain of constructs (useful in finding last construct in a block of constructs). 66 | */ 67 | Construct getLastInChain(Construct input) { 68 | Construct next = input; 69 | while (next.getNext() != null) { 70 | next = next.getNext(); 71 | } 72 | return next; 73 | } 74 | 75 | 76 | LambdaBranch constructLambda(WeaveContext context, Task lambda, String expression, Collection symbols) { 77 | final LambdaBranch branch = new LambdaBranch(); 78 | branch.setCommandName(lambda.getName()); 79 | branch.setInputs(symbols); 80 | branch.setOutputExpression(expression); 81 | context.getLambdaHelper().getBranches().add(branch); 82 | 83 | lambda.captureAttribute("Parameters"); 84 | lambda.handleObject(() -> { 85 | lambda.captureAttribute(COMMAND_VAR); 86 | lambda.setProperty(lambda.getName()); 87 | 88 | symbols.forEach(it -> { 89 | lambda.captureAttribute(it + ".$"); 90 | lambda.setProperty("$." + it); 91 | }); 92 | }); 93 | return branch; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/construct/WhenConstruct.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.construct; 2 | 3 | import com.eclecticlogic.stepper.etc.WeaveContext; 4 | import com.eclecticlogic.stepper.state.State; 5 | import com.google.common.collect.Lists; 6 | 7 | import java.util.List; 8 | 9 | public class WhenConstruct extends Construct { 10 | private final List cases = Lists.newArrayList(); 11 | private Construct elseBlock; 12 | 13 | 14 | public WhenCase addCase(String label) { 15 | WhenCase wcase = new WhenCase(label, getNextDynamicVariable()); 16 | cases.add(wcase); 17 | return wcase; 18 | } 19 | 20 | 21 | public void setElseBlock(Construct elseBlock) { 22 | this.elseBlock = elseBlock; 23 | } 24 | 25 | 26 | @Override 27 | protected String getFirstStateName() { 28 | return cases.get(0).getTask().getName(); 29 | } 30 | 31 | 32 | void setupBlocks(WeaveContext context) { 33 | for (WhenCase aCase : cases) { 34 | constructLambda(context, aCase.getTask(), aCase.getExpression(), aCase.getSymbols()); 35 | 36 | aCase.getTask().setupLambdaHelper(); 37 | aCase.getTask().setResultPath("$." + aCase.getChoiceVariable()); 38 | aCase.getTask().setNextState(aCase.getChoice().getName()); 39 | 40 | aCase.getChoice().setIfNextState(aCase.getBlock().getFirstStateName()); 41 | } 42 | // now wire up all the choice failure parts. 43 | for (int i = 0; i < cases.size() - 1; i++) { 44 | cases.get(i).getChoice().setElseNextState(cases.get(i + 1).getTask().getName()); 45 | } 46 | if (elseBlock != null) { 47 | cases.get(cases.size() - 1).getChoice().setElseNextState(elseBlock.getFirstStateName()); 48 | } 49 | } 50 | 51 | 52 | @Override 53 | public void weave(WeaveContext context) { 54 | setupBlocks(context); 55 | 56 | cases.forEach(c -> c.getBlock().weave(context)); 57 | if (elseBlock != null) { 58 | elseBlock.weave(context); 59 | } 60 | 61 | if (getNext() != null) { 62 | getNext().weave(context); 63 | setNextStateName(getNext().getFirstStateName()); 64 | } 65 | } 66 | 67 | 68 | @Override 69 | protected void setNextStateName(String name) { 70 | cases.forEach(c -> getLastInChain(c.getBlock()).setNextStateName(name)); 71 | if (elseBlock != null) { 72 | getLastInChain(elseBlock).setNextStateName(name); 73 | } else { 74 | cases.get(cases.size() - 1).getChoice().setElseNextState(name); 75 | } 76 | } 77 | 78 | 79 | @Override 80 | public List getStates() { 81 | List result = Lists.newArrayList(); 82 | for (WhenCase aCase : cases) { 83 | result.add(aCase.getTask()); 84 | result.add(aCase.getChoice()); 85 | result.addAll(aCase.getBlock().getStates()); 86 | } 87 | if (elseBlock != null) { 88 | result.addAll(elseBlock.getStates()); 89 | } 90 | if (getNext() != null) { 91 | result.addAll(getNext().getStates()); 92 | } 93 | return result; 94 | } 95 | 96 | 97 | } 98 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/install/StepFunctionInstaller.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015-2019 KR Abram 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.eclecticlogic.stepper.install; 18 | 19 | import com.eclecticlogic.stepper.StateMachine; 20 | import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; 21 | import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; 22 | import software.amazon.awssdk.services.sfn.SfnClient; 23 | import software.amazon.awssdk.services.sfn.model.CreateStateMachineResponse; 24 | import software.amazon.awssdk.services.sfn.model.ListStateMachinesResponse; 25 | import software.amazon.awssdk.services.sfn.model.StateMachineListItem; 26 | 27 | import java.util.Optional; 28 | 29 | import static com.eclecticlogic.stepper.etc.Constants.LAMBDA_ARN_PLACEHOLDER; 30 | 31 | public class StepFunctionInstaller { 32 | 33 | private final StateMachine machine; 34 | private final InstallConfig config; 35 | private final SfnClient client; 36 | 37 | 38 | public StepFunctionInstaller(StateMachine machine, InstallConfig config) { 39 | this.machine = machine; 40 | this.config = config; 41 | AwsCredentialsProvider credentialsProvider = DefaultCredentialsProvider.create(); 42 | client = SfnClient.builder().credentialsProvider(DefaultCredentialsProvider.create()).build(); 43 | } 44 | 45 | 46 | Optional getStateMachineMatch(ListStateMachinesResponse response) { 47 | return response.stateMachines() 48 | .stream() 49 | .filter(sm -> machine.getName().equals(sm.name())) 50 | .findFirst(); 51 | } 52 | 53 | 54 | public String install(String lambdaArn) { 55 | ListStateMachinesResponse response = client.listStateMachines(); 56 | Optional item = getStateMachineMatch(response); 57 | 58 | while (!item.isPresent() && response.nextToken() != null) { 59 | String token = response.nextToken(); 60 | response = client.listStateMachines(b -> b.nextToken(token)); 61 | item = getStateMachineMatch(response); 62 | } 63 | 64 | String code = lambdaArn == null ? 65 | machine.getAsl() : machine.getAsl().replaceAll(LAMBDA_ARN_PLACEHOLDER, lambdaArn); 66 | 67 | if (item.isPresent()) { 68 | String arn = item.get().stateMachineArn(); 69 | client.updateStateMachine(b -> b.stateMachineArn(arn).definition(code)); 70 | return arn; 71 | } else { 72 | CreateStateMachineResponse cr = client.createStateMachine(b -> b.name(machine.getName()) 73 | .definition(code) 74 | .roleArn(config.getStepFunction().getExecutionRole())); 75 | return cr.stateMachineArn(); 76 | } 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/test/groovy/com/eclecticlogic/stepper/asl/TestParallel.groovy: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.asl 2 | 3 | import com.jayway.jsonpath.ReadContext 4 | 5 | 6 | class TestParallel extends AbstractStateMachineTester { 7 | 8 | def "test parallel1"() { 9 | given: 10 | TestOutput output = runWithLambda('parallel.stg', 'parallel1') 11 | ReadContext ctx = output.ctx 12 | 13 | when: 14 | Closure v = { name, param -> 15 | return ctx.read('$.States.' + name + '.' + param) 16 | } 17 | Closure b = { name, branch, param -> 18 | return v(name + '.' + branch, param) 19 | } 20 | 21 | then: 22 | Object[] data = ctx.read('$.States.*') 23 | data.length == 3 24 | 25 | ctx.read('$.StartAt') == 'Para000' 26 | 27 | with('Para000') { 28 | v(it, 'Type') == 'Task' 29 | v(it, 'Parameters.cmd__sm') == 'Para000' 30 | v(it, 'ResultPath') == '$.a' 31 | v(it, 'Next') == 'a' 32 | } 33 | 34 | with('a') { 35 | v(it, 'Type') == 'Parallel' 36 | v(it, 'Retry[0].ErrorEquals[0]') == 'pqr' 37 | v(it, 'Retry[0].IntervalSeconds') == 6 38 | v(it, 'ResultPath') == '$.b' 39 | v(it, 'Next') == 'Para.Success' 40 | v(it, 'Branches[0].StartAt') == 'First000' 41 | def name = it 42 | with('Branches[0].States.First000') { 43 | b(name, it, 'Type') == 'Task' 44 | b(name, it, 'Parameters.cmd__sm') == 'First000' 45 | b(name, it, 'ResultPath') == '$.a' 46 | b(name, it, 'Next') == 'First001' 47 | } 48 | with('Branches[0].States.First001') { 49 | b(name, it, 'Type') == 'Pass' 50 | b(name, it, 'Result') == 10 51 | b(name, it, 'ResultPath') == '$.b' 52 | b(name, it, 'Next') == 'First.Success' 53 | } 54 | with("Branches[0].States.['First.Success']") { 55 | b(name, it, 'Type') == 'Succeed' 56 | } 57 | 58 | v(it, 'Branches[1].StartAt') == 'Second000' 59 | with('Branches[1].States.Second000') { 60 | b(name, it, 'Type') == 'Task' 61 | b(name, it, 'Parameters.cmd__sm') == 'Second000' 62 | b(name, it, 'ResultPath') == '$.a' 63 | b(name, it, 'Next') == 'Second001' 64 | } 65 | with('Branches[1].States.Second001') { 66 | b(name, it, 'Type') == 'Pass' 67 | b(name, it, 'Result') == 20 68 | b(name, it, 'ResultPath') == '$.b' 69 | b(name, it, 'Next') == 'Second.Success' 70 | } 71 | with("Branches[1].States.['Second.Success']") { 72 | b(name, it, 'Type') == 'Succeed' 73 | } 74 | } 75 | 76 | v("['Para.Success']", 'Type') == 'Succeed' 77 | 78 | output.lambda.contains('if (event.cmd__sm == "Para000") {') 79 | output.lambda.contains('if (event.cmd__sm == "First000") {') 80 | output.lambda.contains('if (event.cmd__sm == "Second000") {') 81 | output.lambda.contains('const response = 5*2;') 82 | output.lambda.contains('const response = 5*5;') 83 | output.lambda.contains('const response = 15*5;') 84 | 85 | } 86 | 87 | 88 | } -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/visitor/StepperVisitor.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015-2019 KR Abram 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.eclecticlogic.stepper.visitor; 18 | 19 | import com.eclecticlogic.stepper.StateMachine; 20 | import com.eclecticlogic.stepper.antlr.StepperParser; 21 | import com.eclecticlogic.stepper.construct.Construct; 22 | import com.eclecticlogic.stepper.construct.ProgramConstruct; 23 | import com.eclecticlogic.stepper.construct.SuccessConstruct; 24 | import com.eclecticlogic.stepper.etc.WeaveContext; 25 | import com.eclecticlogic.stepper.state.NameProvider; 26 | import com.eclecticlogic.stepper.state.observer.StateObserver; 27 | 28 | import java.math.BigDecimal; 29 | 30 | public class StepperVisitor extends AbstractVisitor { 31 | 32 | private final WeaveContext weaveContext; 33 | private boolean suppressAnnotations; 34 | 35 | 36 | public StepperVisitor() { 37 | this(new WeaveContext()); 38 | } 39 | 40 | 41 | public StepperVisitor(WeaveContext weaveContext) { 42 | this.weaveContext = weaveContext; 43 | } 44 | 45 | 46 | public void setSuppressAnnotations(boolean suppressAnnotations) { 47 | this.suppressAnnotations = suppressAnnotations; 48 | } 49 | 50 | 51 | @Override 52 | public StateMachine visitProgram(StepperParser.ProgramContext ctx) { 53 | ProgramConstruct program = new ProgramConstruct(); 54 | 55 | program.setProgramName(ctx.ID().getText()); 56 | if (!suppressAnnotations) { 57 | for (StepperParser.AnnotationContext anCtx : ctx.annotation()) { 58 | if (anCtx.scalar().STRING() != null) { 59 | program.addAnnotation(anCtx.ID().getText(), anCtx.scalar().getText()); 60 | } else if (anCtx.scalar().NUMBER() != null) { 61 | program.addAnnotation(anCtx.ID().getText(), new BigDecimal(anCtx.scalar().getText())); 62 | } else { 63 | program.addAnnotation(anCtx.ID().getText(), Boolean.parseBoolean(anCtx.scalar().getText())); 64 | } 65 | } 66 | } 67 | 68 | StateObserver.over(() -> { 69 | NameProvider.usingName(program.getProgramName(), () -> { 70 | Construct current = program; 71 | for (StepperParser.StatementContext stCtx : ctx.statement()) { 72 | StatementVisitor visitor = new StatementVisitor(); 73 | Construct c = visitor.visit(stCtx); 74 | current.setNext(c); 75 | current = c; 76 | } 77 | current.setNext(new SuccessConstruct(program.getProgramName())); 78 | 79 | program.weave(weaveContext); 80 | }); 81 | }); 82 | 83 | return new StateMachine(program); 84 | } 85 | 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/construct/IfConstruct.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.construct; 2 | 3 | import com.eclecticlogic.stepper.etc.WeaveContext; 4 | import com.eclecticlogic.stepper.state.Choice; 5 | import com.eclecticlogic.stepper.state.State; 6 | import com.eclecticlogic.stepper.state.Task; 7 | import com.google.common.collect.Lists; 8 | 9 | import java.util.List; 10 | import java.util.Set; 11 | 12 | public class IfConstruct extends Construct { 13 | 14 | private String conditionText; 15 | private Set symbols; 16 | private Construct firstIf, firstElse; 17 | 18 | private final Task conditionTask; 19 | private final String choiceVariable; 20 | private final Choice choice; 21 | 22 | 23 | public IfConstruct(String label) { 24 | conditionTask = new Task(label); 25 | choiceVariable = getNextDynamicVariable(); 26 | choice = new Choice(choiceVariable); 27 | } 28 | 29 | 30 | public void setCondition(String condition) { 31 | conditionText = condition; 32 | } 33 | 34 | 35 | public void setSymbols(Set conditionDeferences) { 36 | this.symbols = conditionDeferences; 37 | } 38 | 39 | 40 | public void setFirstIf(Construct firstIf) { 41 | this.firstIf = firstIf; 42 | } 43 | 44 | 45 | public void setFirstElse(Construct firstElse) { 46 | this.firstElse = firstElse; 47 | } 48 | 49 | 50 | void setupCondition(WeaveContext context) { 51 | constructLambda(context, conditionTask, conditionText, symbols); 52 | 53 | conditionTask.setupLambdaHelper(); 54 | conditionTask.setResultPath("$." + choiceVariable); 55 | conditionTask.setNextState(choice.getName()); 56 | 57 | choice.setIfNextState(firstIf.getFirstStateName()); 58 | String elseName = "none"; 59 | if (firstElse != null) { 60 | elseName = firstElse.getFirstStateName(); 61 | } else if (getNext() != null) { 62 | elseName = getNext().getFirstStateName(); 63 | } 64 | choice.setElseNextState(elseName); 65 | } 66 | 67 | 68 | @Override 69 | protected String getFirstStateName() { 70 | return conditionTask.getName(); 71 | } 72 | 73 | 74 | @Override 75 | protected void setNextStateName(String name) { 76 | getLastInChain(firstIf).setNextStateName(name); 77 | if (firstElse != null) { 78 | getLastInChain(firstElse).setNextStateName(name); 79 | } else { 80 | choice.setElseNextState(name); 81 | } 82 | } 83 | 84 | 85 | @Override 86 | public void weave(WeaveContext context) { 87 | setupCondition(context); 88 | firstIf.weave(context); 89 | if (firstElse != null) { 90 | firstElse.weave(context); 91 | } 92 | if (getNext() != null) { 93 | getNext().weave(context); 94 | setNextStateName(getNext().getFirstStateName()); 95 | } 96 | } 97 | 98 | 99 | @Override 100 | public List getStates() { 101 | List result = Lists.newArrayList(conditionTask, choice); 102 | result.addAll(firstIf.getStates()); 103 | if (firstElse != null) { 104 | result.addAll(firstElse.getStates()); 105 | } 106 | if (getNext() != null) { 107 | result.addAll(getNext().getStates()); 108 | } 109 | return result; 110 | } 111 | 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/construct/ParallelConstruct.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015-2019 KR Abram 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.eclecticlogic.stepper.construct; 18 | 19 | import com.eclecticlogic.stepper.StateMachine; 20 | import com.eclecticlogic.stepper.Stepper; 21 | import com.eclecticlogic.stepper.antlr.StepperParser; 22 | import com.eclecticlogic.stepper.etc.WeaveContext; 23 | import com.eclecticlogic.stepper.state.Parallel; 24 | import com.eclecticlogic.stepper.visitor.StepperVisitor; 25 | import org.antlr.v4.runtime.CharStreams; 26 | import org.stringtemplate.v4.ST; 27 | import org.stringtemplate.v4.STGroup; 28 | import org.stringtemplate.v4.STGroupFile; 29 | 30 | import java.io.IOException; 31 | import java.nio.file.Files; 32 | import java.nio.file.Path; 33 | import java.nio.file.Paths; 34 | import java.util.List; 35 | import java.util.stream.Stream; 36 | 37 | import static java.util.stream.Collectors.joining; 38 | 39 | public class ParallelConstruct extends StateConstruct { 40 | 41 | private List references; 42 | 43 | 44 | public ParallelConstruct(Parallel state) { 45 | super(state); 46 | } 47 | 48 | 49 | public void setReferences(List references) { 50 | this.references = references; 51 | } 52 | 53 | 54 | public void setResultPath(String path) { 55 | getState().setResultPath(path); 56 | } 57 | 58 | 59 | String getText(String reference) { 60 | if (reference.startsWith("stg://")) { 61 | String path = reference.substring(6); 62 | String[] parts = path.split("@"); 63 | STGroup group = new STGroupFile(parts[1]); 64 | ST st = group.getInstanceOf(parts[0]); 65 | return st.render(); 66 | } else { 67 | Path path; 68 | if (reference.startsWith("classpath://")) { 69 | path = Paths.get(getClass().getClassLoader() 70 | .getResource(reference.substring(12)).getFile()); 71 | } else { 72 | path = Paths.get(reference); 73 | } 74 | try (Stream lines = Files.lines(path)) { 75 | return lines.collect(joining("\n")); 76 | } catch (IOException e) { 77 | throw new RuntimeException(e); 78 | } 79 | } 80 | } 81 | 82 | 83 | @Override 84 | public void weave(WeaveContext context) { 85 | Parallel state = getState(); 86 | state.captureAttribute("Branches"); 87 | state.handleArray(() -> { 88 | for (String reference : references) { 89 | StepperParser parser = Stepper.createParser(CharStreams.fromString(getText(reference))); 90 | StepperVisitor visitor = new StepperVisitor(context); 91 | visitor.setSuppressAnnotations(true); 92 | StateMachine machine = visitor.visitProgram(parser.program()); 93 | state.setObject(machine.toJson()); 94 | } 95 | }); 96 | super.weave(context); 97 | } 98 | 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/construct/TryCatchConstruct.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015-2019 KR Abram 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.eclecticlogic.stepper.construct; 18 | 19 | import com.eclecticlogic.stepper.etc.WeaveContext; 20 | import com.eclecticlogic.stepper.state.AttributableState; 21 | import com.eclecticlogic.stepper.state.Parallel; 22 | import com.eclecticlogic.stepper.state.State; 23 | import com.eclecticlogic.stepper.state.Task; 24 | import com.eclecticlogic.stepper.state.observer.NotificationReceiver; 25 | import com.google.common.collect.Lists; 26 | 27 | import java.util.List; 28 | 29 | public class TryCatchConstruct extends Construct implements NotificationReceiver { 30 | 31 | private final List statesOfInterest = Lists.newArrayList(); 32 | private final List clauses = Lists.newArrayList(); 33 | private Construct tryBlock; 34 | 35 | 36 | public void setTryBlock(Construct tryBlock) { 37 | this.tryBlock = tryBlock; 38 | } 39 | 40 | 41 | @Override 42 | public void receive(State state) { 43 | if (state instanceof Task || state instanceof Parallel) { 44 | statesOfInterest.add((AttributableState) state); 45 | } 46 | } 47 | 48 | 49 | public void add(CatchClause cc) { 50 | clauses.add(cc); 51 | } 52 | 53 | 54 | @Override 55 | public void weave(WeaveContext context) { 56 | tryBlock.weave(context); 57 | 58 | for (CatchClause clause : clauses) { 59 | clause.getBlock().weave(context); 60 | for (AttributableState state : statesOfInterest) { 61 | state.addCatch(() -> 62 | state.handleObject(() -> { 63 | state.captureAttribute("ErrorEquals"); 64 | state.handleArray(() -> clause.getErrors().forEach(state::setProperty)); 65 | if (clause.getResultPath() != null) { 66 | state.captureAttribute("ResultPath"); 67 | state.setProperty("$." + clause.getResultPath()); 68 | } 69 | state.captureAttribute("Next"); 70 | state.setProperty(clause.getBlock().getFirstStateName()); 71 | }) 72 | ); 73 | } 74 | } 75 | 76 | getNext().weave(context); 77 | setNextStateName(getNext().getFirstStateName()); 78 | } 79 | 80 | 81 | @Override 82 | protected String getFirstStateName() { 83 | return tryBlock.getFirstStateName(); 84 | } 85 | 86 | 87 | @Override 88 | protected void setNextStateName(String name) { 89 | getLastInChain(tryBlock).setNextStateName(name); 90 | } 91 | 92 | 93 | @Override 94 | public List getStates() { 95 | List states = Lists.newArrayList(); 96 | states.addAll(tryBlock.getStates()); 97 | for (CatchClause clause : clauses) { 98 | states.addAll(clause.getBlock().getStates()); 99 | } 100 | states.addAll(getNext().getStates()); 101 | return states; 102 | } 103 | 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/state/AttributableState.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015-2019 KR Abram 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.eclecticlogic.stepper.state; 18 | 19 | import com.google.gson.JsonArray; 20 | import com.google.gson.JsonElement; 21 | import com.google.gson.JsonObject; 22 | 23 | import java.util.Stack; 24 | 25 | public class AttributableState extends AbstractState { 26 | 27 | public static final String ATTRIB_CATCH = "Catch"; 28 | 29 | private String currentAttribute; 30 | private JsonElement current; 31 | private final Stack currentStack = new Stack<>(); 32 | 33 | 34 | AttributableState() { 35 | this(null); 36 | } 37 | 38 | 39 | AttributableState(String stateName) { 40 | super(stateName); 41 | current = json; 42 | } 43 | 44 | 45 | public void captureAttribute(String attr) { 46 | currentAttribute = attr; 47 | } 48 | 49 | 50 | public void setProperty(boolean value) { 51 | if (current instanceof JsonObject) { 52 | ((JsonObject)current).addProperty(currentAttribute, value); 53 | } else { 54 | ((JsonArray)current).add(value); 55 | } 56 | } 57 | 58 | 59 | public void setProperty(String value) { 60 | if (current instanceof JsonObject) { 61 | ((JsonObject)current).addProperty(currentAttribute, value); 62 | } else { 63 | ((JsonArray)current).add(value); 64 | } 65 | } 66 | 67 | 68 | public void setProperty(Number value) { 69 | if (current instanceof JsonObject) { 70 | ((JsonObject)current).addProperty(currentAttribute, value); 71 | } else { 72 | ((JsonArray)current).add(value); 73 | } 74 | } 75 | 76 | 77 | public void setObject(JsonObject object) { 78 | if (current instanceof JsonObject) { 79 | ((JsonObject) current).add(currentAttribute, object); 80 | } else { 81 | ((JsonArray) current).add(object); 82 | } 83 | } 84 | 85 | 86 | public JsonObject handleObject(Runnable closure) { 87 | JsonObject obj = new JsonObject(); 88 | if (current instanceof JsonObject) { 89 | ((JsonObject)current).add(currentAttribute, obj); 90 | } else { 91 | ((JsonArray)current).add(obj); 92 | } 93 | currentStack.push(current); 94 | current = obj; 95 | closure.run(); 96 | current = currentStack.pop(); 97 | return obj; 98 | } 99 | 100 | 101 | public JsonArray handleArray(Runnable closure) { 102 | JsonArray array = new JsonArray(); 103 | if (current instanceof JsonObject) { 104 | ((JsonObject)current).add(currentAttribute, array); 105 | } else { 106 | ((JsonArray)current).add(array); 107 | } 108 | currentStack.push(current); 109 | current = array; 110 | closure.run(); 111 | current = currentStack.pop(); 112 | return array; 113 | } 114 | 115 | 116 | public void addCatch(Runnable closure) { 117 | if (json.get(ATTRIB_CATCH) == null) { 118 | json.add(ATTRIB_CATCH, new JsonArray()); 119 | } 120 | JsonArray array = (JsonArray) json.get(ATTRIB_CATCH); 121 | currentStack.push(current); 122 | current = array; 123 | closure.run(); 124 | current = currentStack.pop(); 125 | } 126 | 127 | 128 | } 129 | 130 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/construct/ForIterationConstruct.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.construct; 2 | 3 | import com.eclecticlogic.stepper.etc.LambdaBranch; 4 | import com.eclecticlogic.stepper.etc.WeaveContext; 5 | import com.eclecticlogic.stepper.state.Choice; 6 | import com.eclecticlogic.stepper.state.Pass; 7 | import com.eclecticlogic.stepper.state.State; 8 | import com.eclecticlogic.stepper.state.Task; 9 | import com.google.common.collect.Lists; 10 | import org.stringtemplate.v4.ST; 11 | import org.stringtemplate.v4.STGroup; 12 | import org.stringtemplate.v4.STGroupFile; 13 | 14 | import java.util.List; 15 | import java.util.Set; 16 | 17 | public class ForIterationConstruct extends Construct { 18 | 19 | private String iterableExpression; 20 | private String iterableVariable; 21 | private Set symbols; 22 | private Construct block; 23 | 24 | private final Pass indexInitializer; 25 | private final String index; 26 | private final Task iteratingLambda; 27 | private final Choice choice; 28 | private final Pass iterVariableSetter; 29 | 30 | 31 | public ForIterationConstruct(String label) { 32 | indexInitializer = new Pass(label); 33 | index = getNextDynamicVariable(); 34 | iteratingLambda = new Task(); 35 | choice = new Choice(index + ".exists"); 36 | iterVariableSetter = new Pass(); 37 | } 38 | 39 | 40 | public void setIterableExpression(String iterableExpression) { 41 | this.iterableExpression = iterableExpression; 42 | } 43 | 44 | 45 | public void setIterableVariable(String iterableVariable) { 46 | this.iterableVariable = iterableVariable; 47 | } 48 | 49 | 50 | public void setSymbols(Set symbols) { 51 | this.symbols = symbols; 52 | } 53 | 54 | 55 | public void setBlock(Construct block) { 56 | this.block = block; 57 | } 58 | 59 | 60 | void setupInitializer() { 61 | indexInitializer.captureAttribute("Result"); 62 | indexInitializer.handleObject(() -> { 63 | indexInitializer.captureAttribute("idx"); 64 | indexInitializer.setProperty(-1); 65 | }); 66 | 67 | indexInitializer.setResultPath("$." + index); 68 | indexInitializer.setNextState(iteratingLambda.getName()); 69 | } 70 | 71 | 72 | void setupIteratingLambda(WeaveContext context) { 73 | symbols.add(index); 74 | final LambdaBranch branch = constructLambda(context, iteratingLambda, "r", symbols); 75 | STGroup group = new STGroupFile("stepper/template/lambda.stg"); 76 | ST st = group.getInstanceOf("forIterationBody"); 77 | st.add("expr", iterableExpression); 78 | st.add("index", index); 79 | branch.setComputation(st.render()); 80 | 81 | iteratingLambda.setupLambdaHelper(); 82 | iteratingLambda.setResultPath("$." + index); 83 | iteratingLambda.setNextState(choice.getName()); 84 | } 85 | 86 | 87 | void setupChoice() { 88 | choice.setIfNextState(iterVariableSetter.getName()); 89 | iterVariableSetter.captureAttribute("InputPath"); 90 | iterVariableSetter.setProperty("$." + index + ".var"); 91 | iterVariableSetter.setResultPath("$." + iterableVariable); 92 | iterVariableSetter.setNextState(block.getFirstStateName()); 93 | getLastInChain(block).setNextStateName(iteratingLambda.getName()); 94 | } 95 | 96 | 97 | @Override 98 | protected String getFirstStateName() { 99 | return indexInitializer.getName(); 100 | } 101 | 102 | 103 | @Override 104 | protected void setNextStateName(String name) { 105 | choice.setElseNextState(name); 106 | } 107 | 108 | 109 | @Override 110 | public void weave(WeaveContext context) { 111 | setupInitializer(); 112 | setupIteratingLambda(context); 113 | setupChoice(); 114 | 115 | block.weave(context); 116 | 117 | if (getNext() != null) { 118 | getNext().weave(context); 119 | setNextStateName(getNext().getFirstStateName()); 120 | } 121 | } 122 | 123 | 124 | @Override 125 | public List getStates() { 126 | List result = Lists.newArrayList(indexInitializer, iteratingLambda, choice, iterVariableSetter); 127 | result.addAll(block.getStates()); 128 | if (getNext() != null) { 129 | result.addAll(getNext().getStates()); 130 | } 131 | return result; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/install/LambdaInstaller.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015-2019 KR Abram 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.eclecticlogic.stepper.install; 18 | 19 | import com.eclecticlogic.stepper.StateMachine; 20 | import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; 21 | import software.amazon.awssdk.core.SdkBytes; 22 | import software.amazon.awssdk.services.lambda.LambdaClient; 23 | import software.amazon.awssdk.services.lambda.model.Runtime; 24 | import software.amazon.awssdk.services.lambda.model.*; 25 | 26 | import java.io.FileOutputStream; 27 | import java.io.IOException; 28 | import java.nio.file.Files; 29 | import java.nio.file.Path; 30 | import java.util.zip.ZipEntry; 31 | import java.util.zip.ZipOutputStream; 32 | 33 | public class LambdaInstaller { 34 | 35 | private final static String LAMBDA_NAME_SUFFIX = "_stepperLambda"; 36 | 37 | private final StateMachine machine; 38 | private final InstallConfig config; 39 | private final LambdaClient lambdaClient; 40 | 41 | 42 | public LambdaInstaller(StateMachine machine, InstallConfig config) { 43 | this.machine = machine; 44 | this.config = config; 45 | lambdaClient = LambdaClient.builder().credentialsProvider(DefaultCredentialsProvider.create()).build(); 46 | } 47 | 48 | 49 | public String getLambdaName() { 50 | return machine.getName() + LAMBDA_NAME_SUFFIX; 51 | } 52 | 53 | 54 | /** 55 | * @return arn if lambda exists, otherwise null 56 | */ 57 | String getExistingLambdaArn() { 58 | try { 59 | return lambdaClient.getFunction(b -> b.functionName(getLambdaName())).configuration().functionArn(); 60 | } catch (ResourceNotFoundException e) { 61 | return null; 62 | } 63 | } 64 | 65 | 66 | void updateLambda(String arn) { 67 | lambdaClient.updateFunctionCode(b -> b.functionName(getLambdaName()) 68 | .zipFile(createSdkBytesForCode())); 69 | lambdaClient.updateFunctionConfiguration(b -> b.functionName(getLambdaName()) 70 | .role(config.getLambda().getExecutionRole())); 71 | } 72 | 73 | 74 | SdkBytes createSdkBytesForCode() { 75 | Path zipFile; 76 | try { 77 | zipFile = Files.createTempFile("stepper", ".zip"); 78 | } catch (IOException e) { 79 | throw new RuntimeException(e); 80 | } 81 | try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zipFile.toFile()))) { 82 | ZipEntry e = new ZipEntry("index.js"); 83 | out.putNextEntry(e); 84 | 85 | byte[] data = machine.getLambda().getBytes(); 86 | out.write(data, 0, data.length); 87 | out.closeEntry(); 88 | } catch (IOException e) { 89 | throw new RuntimeException(e); 90 | } 91 | try { 92 | return SdkBytes.fromByteArray(Files.readAllBytes(zipFile)); 93 | } catch (IOException e) { 94 | throw new RuntimeException(e); 95 | } 96 | } 97 | 98 | 99 | FunctionCode createZippedLambdaCode() { 100 | return FunctionCode.builder().zipFile(createSdkBytesForCode()).build(); 101 | 102 | } 103 | 104 | 105 | public String install() { 106 | String arn = getExistingLambdaArn(); 107 | if (arn != null) { 108 | updateLambda(arn); 109 | } else { 110 | CreateFunctionRequest.Builder lambdaBuilder = CreateFunctionRequest.builder() 111 | .functionName(machine.getName() + LAMBDA_NAME_SUFFIX) 112 | .code(createZippedLambdaCode()) 113 | .handler("index.handler") 114 | .runtime(Runtime.NODEJS10_X) 115 | .role(config.getLambda().getExecutionRole()); 116 | CreateFunctionResponse response = lambdaClient.createFunction(lambdaBuilder.build()); 117 | arn = response.functionArn(); 118 | } 119 | 120 | return arn; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/test/groovy/com/eclecticlogic/stepper/asl/TestGoto.groovy: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.asl 2 | 3 | import com.jayway.jsonpath.ReadContext 4 | 5 | 6 | class TestGoto extends AbstractStateMachineTester { 7 | 8 | def "test goto1"() { 9 | given: 10 | TestOutput output = runWithLambda('goto.stg', 'goto1') 11 | ReadContext ctx = output.ctx 12 | 13 | when: 14 | Closure v = { name, param -> 15 | return ctx.read('$.States.' + name + '.' + param) 16 | } 17 | 18 | then: 19 | Object[] data = ctx.read('$.States.*') 20 | data.length == 8 21 | 22 | ctx.read('$.StartAt') == 'Goto000' 23 | 24 | with('Goto000') { 25 | v(it, 'Type') == 'Pass' 26 | v(it, 'Result') == 5 27 | v(it, 'ResultPath') == '$.a' 28 | v(it, 'Next') == 'Goto001' 29 | } 30 | 31 | with('Goto001') { 32 | v(it, 'Type') == 'Task' 33 | v(it, 'Parameters.cmd__sm') == 'Goto001' 34 | v(it, "Parameters.['a.\$']") == '$.a' 35 | v(it, 'ResultPath') == '$.Gotovar__000' 36 | v(it, 'Next') == 'Goto002' 37 | } 38 | output.lambda.contains('const response = a==5;') 39 | 40 | with('Goto002') { 41 | v(it, 'Type') == 'Choice' 42 | v(it, 'Choices[0].Variable') == '$.Gotovar__000' 43 | v(it, 'Choices[0].BooleanEquals') == true 44 | v(it, 'Choices[0].Next') == 'Goto003' 45 | 46 | v(it, 'Choices[1].Variable') == '$.Gotovar__000' 47 | v(it, 'Choices[1].BooleanEquals') == false 48 | v(it, 'Choices[1].Next') == 'Goto004' 49 | } 50 | 51 | with('Goto003') { 52 | v(it, 'Type') == 'Pass' 53 | v(it, 'Next') == 'a' 54 | } 55 | 56 | with('Goto004') { 57 | v(it, 'Type') == 'Fail' 58 | } 59 | 60 | with('a') { 61 | v(it, 'Type') == 'Pass' 62 | v(it, 'Result') == 7 63 | v(it, 'ResultPath') == '$.b' 64 | v(it, 'Next') == 'Goto005' 65 | } 66 | 67 | with('Goto005') { 68 | v(it, 'Type') == 'Pass' 69 | v(it, 'Result') == 5 70 | v(it, 'ResultPath') == '$.d' 71 | v(it, 'Next') == 'Goto.Success' 72 | } 73 | 74 | v("['Goto.Success']", 'Type') == 'Succeed' 75 | } 76 | 77 | 78 | def "test goto2"() { 79 | given: 80 | TestOutput output = runWithLambda('goto.stg', 'goto2') 81 | ReadContext ctx = output.ctx 82 | 83 | when: 84 | Closure v = { name, param -> 85 | return ctx.read('$.States.' + name + '.' + param) 86 | } 87 | 88 | then: 89 | Object[] data = ctx.read('$.States.*') 90 | data.length == 9 91 | 92 | ctx.read('$.StartAt') == 'Goto000' 93 | 94 | with('Goto000') { 95 | v(it, 'Type') == 'Pass' 96 | v(it, 'Result') == 5 97 | v(it, 'ResultPath') == '$.a' 98 | v(it, 'Next') == 'Goto001' 99 | } 100 | 101 | with('Goto001') { 102 | v(it, 'Type') == 'Task' 103 | v(it, 'Parameters.cmd__sm') == 'Goto001' 104 | v(it, "Parameters.['a.\$']") == '$.a' 105 | v(it, 'ResultPath') == '$.Gotovar__000' 106 | v(it, 'Next') == 'Goto002' 107 | } 108 | output.lambda.contains('const response = a==5;') 109 | 110 | with('Goto002') { 111 | v(it, 'Type') == 'Choice' 112 | v(it, 'Choices[0].Variable') == '$.Gotovar__000' 113 | v(it, 'Choices[0].BooleanEquals') == true 114 | v(it, 'Choices[0].Next') == 'Goto003' 115 | 116 | v(it, 'Choices[1].Variable') == '$.Gotovar__000' 117 | v(it, 'Choices[1].BooleanEquals') == false 118 | v(it, 'Choices[1].Next') == 'Goto004' 119 | } 120 | 121 | with('Goto003') { 122 | v(it, 'Type') == 'Pass' 123 | v(it, 'Next') == 'a' 124 | } 125 | 126 | with('Goto004') { 127 | v(it, 'Type') == 'Pass' 128 | v(it, 'Next') == 'b' 129 | } 130 | 131 | with('a') { 132 | v(it, 'Type') == 'Pass' 133 | v(it, 'Result') == 7 134 | v(it, 'ResultPath') == '$.b' 135 | v(it, 'Next') == 'Goto005' 136 | } 137 | 138 | with('Goto005') { 139 | v(it, 'Type') == 'Pass' 140 | v(it, 'Next') == 'Goto.Success' 141 | } 142 | 143 | with('b') { 144 | v(it, 'Type') == 'Pass' 145 | v(it, 'Result') == 5 146 | v(it, 'ResultPath') == '$.d' 147 | v(it, 'Next') == 'Goto.Success' 148 | } 149 | 150 | v("['Goto.Success']", 'Type') == 'Succeed' 151 | } 152 | 153 | } -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/Stepper.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015-2019 KR Abram 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.eclecticlogic.stepper; 18 | 19 | import com.beust.jcommander.JCommander; 20 | import com.beust.jcommander.Parameter; 21 | import com.beust.jcommander.ParameterException; 22 | import com.eclecticlogic.stepper.antlr.StepperLexer; 23 | import com.eclecticlogic.stepper.antlr.StepperParser; 24 | import com.eclecticlogic.stepper.install.InstallConfig; 25 | import com.eclecticlogic.stepper.install.LambdaInstaller; 26 | import com.eclecticlogic.stepper.install.StepFunctionInstaller; 27 | import com.eclecticlogic.stepper.visitor.StepperVisitor; 28 | import com.google.common.collect.Lists; 29 | import org.antlr.v4.runtime.*; 30 | import org.yaml.snakeyaml.Yaml; 31 | import org.yaml.snakeyaml.constructor.Constructor; 32 | 33 | import java.io.IOException; 34 | import java.nio.file.Files; 35 | import java.nio.file.Path; 36 | import java.nio.file.Paths; 37 | 38 | public class Stepper { 39 | 40 | @Parameter(names = {"-i", "--install"}, description = "Compiles and installs Step Function ASL and Lambda using information provided in yaml file.") 41 | String install; 42 | 43 | @Parameter(description = "Stepper program file", required = true) 44 | String stepperProgramFile; 45 | 46 | 47 | public static StepperParser createParser(CharStream input) { 48 | StepperLexer lexer = new StepperLexer(input); 49 | CommonTokenStream tokens = new CommonTokenStream(lexer); 50 | StepperParser parser = new StepperParser(tokens); 51 | parser.removeErrorListeners(); 52 | parser.addErrorListener(new BaseErrorListener() { 53 | @Override 54 | public void syntaxError(Recognizer recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) { 55 | throw new StepperParseException(offendingSymbol, line, charPositionInLine, msg, e); 56 | } 57 | }); 58 | return parser; 59 | } 60 | 61 | 62 | void run() throws IOException { 63 | StepperParser parser = createParser(CharStreams.fromFileName(stepperProgramFile)); 64 | 65 | StepperVisitor visitor = new StepperVisitor(); 66 | StateMachine machine = visitor.visitProgram(parser.program()); 67 | 68 | if (install == null) { 69 | manualSetup(machine); 70 | } else { 71 | autoSetup(machine); 72 | } 73 | } 74 | 75 | 76 | void autoSetup(StateMachine machine) throws IOException { 77 | Yaml yaml = new Yaml(new Constructor(InstallConfig.class)); 78 | String yamlData = String.join("\n", Files.readAllLines(Paths.get(install))); 79 | InstallConfig installConfig = yaml.load(yamlData); 80 | 81 | String lambdaArn = null; 82 | if (machine.isLambdaRequired()) { 83 | LambdaInstaller lambdaInstaller = new LambdaInstaller(machine, installConfig); 84 | lambdaArn = lambdaInstaller.install(); 85 | } 86 | 87 | StepFunctionInstaller sfInstaller = new StepFunctionInstaller(machine, installConfig); 88 | String arn = sfInstaller.install(lambdaArn); 89 | System.out.println("Lambda installed. Arn = " + arn); 90 | } 91 | 92 | 93 | void manualSetup(StateMachine machine) throws IOException { 94 | Path asl = Paths.get(stepperProgramFile).resolveSibling(machine.getName() + ".asl"); 95 | Files.write(asl, Lists.newArrayList(machine.getAsl())); 96 | 97 | if (machine.isLambdaRequired()) { 98 | Path lambda = Paths.get(stepperProgramFile).resolveSibling(machine.getName() + ".js"); 99 | Files.write(lambda, Lists.newArrayList(machine.getLambda())); 100 | System.out.println("Wrote asl to " + asl + ", lambda to " + lambda); 101 | } else { 102 | System.out.println("Wrote asl to " + asl); 103 | } 104 | } 105 | 106 | 107 | public static void main(String[] args) { 108 | Stepper stepper = new Stepper(); 109 | JCommander commander = JCommander 110 | .newBuilder() 111 | .addObject(stepper) 112 | .build(); 113 | commander.setProgramName("stepper"); 114 | try { 115 | commander.parse(args); 116 | stepper.run(); 117 | } catch (ParameterException e) { 118 | System.err.println(e.getMessage()); 119 | commander.usage(); 120 | } catch (IOException e) { 121 | e.printStackTrace(); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/visitor/AssignmentVisitor.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015-2019 KR Abram 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.eclecticlogic.stepper.visitor; 18 | 19 | import com.eclecticlogic.stepper.antlr.StepperParser; 20 | import com.eclecticlogic.stepper.construct.Construct; 21 | import com.eclecticlogic.stepper.construct.ExpressionConstruct; 22 | import com.eclecticlogic.stepper.construct.StateConstruct; 23 | import com.eclecticlogic.stepper.state.Pass; 24 | import com.eclecticlogic.stepper.state.Task; 25 | 26 | import java.math.BigDecimal; 27 | import java.util.Set; 28 | 29 | import static com.eclecticlogic.stepper.etc.Etc.*; 30 | 31 | 32 | public class AssignmentVisitor extends AbstractVisitor { 33 | 34 | 35 | @Override 36 | public Construct visitAssignmentTask(StepperParser.AssignmentTaskContext ctx) { 37 | RetryVisitor retryVisitor = new RetryVisitor<>(Task::new); 38 | Task task = retryVisitor.visit(ctx.retries()); 39 | 40 | JsonObjectVisitor jsonObjectVisitor = new JsonObjectVisitor(task); 41 | jsonObjectVisitor.visit(ctx.task().jsonObject()); 42 | 43 | task.setResultPath("$." + ctx.dereference().getText()); 44 | return new StateConstruct<>(task); 45 | } 46 | 47 | 48 | @Override 49 | public Construct visitAssignmentNumber(StepperParser.AssignmentNumberContext ctx) { 50 | Pass pass = new Pass(toLabel(ctx.label())); 51 | pass.captureAttribute("Result"); 52 | pass.setProperty(new BigDecimal(ctx.NUMBER().getText())); 53 | pass.setResultPath("$." + ctx.dereference().getText()); 54 | return new StateConstruct<>(pass); 55 | } 56 | 57 | 58 | @Override 59 | public Construct visitAssignmentString(StepperParser.AssignmentStringContext ctx) { 60 | Pass pass = new Pass(toLabel(ctx.label())); 61 | pass.captureAttribute("Result"); 62 | pass.setProperty(strip(ctx.STRING().getText())); 63 | pass.setResultPath("$." + ctx.dereference().getText()); 64 | return new StateConstruct<>(pass); 65 | } 66 | 67 | 68 | Construct visitAssignmentBoolean(String var, boolean value, StepperParser.LabelContext lbl) { 69 | Pass pass = new Pass(toLabel(lbl)); 70 | pass.captureAttribute("Result"); 71 | pass.setProperty(value); 72 | pass.setResultPath("$." + var); 73 | return new StateConstruct<>(pass); 74 | } 75 | 76 | 77 | @Override 78 | public Construct visitAssignmentTrue(StepperParser.AssignmentTrueContext ctx) { 79 | return visitAssignmentBoolean(ctx.dereference().getText(), true, ctx.label()); 80 | } 81 | 82 | 83 | @Override 84 | public Construct visitAssignmentFalse(StepperParser.AssignmentFalseContext ctx) { 85 | return visitAssignmentBoolean(ctx.dereference().getText(), false, ctx.label()); 86 | } 87 | 88 | 89 | @Override 90 | public Construct visitAssignmentJson(StepperParser.AssignmentJsonContext ctx) { 91 | Pass pass = new Pass(toLabel(ctx.label())); 92 | pass.captureAttribute("Parameters"); 93 | pass.handleObject(() -> { 94 | JsonObjectVisitor visitor = new JsonObjectVisitor(pass); 95 | visitor.visit(ctx.jsonObject()); 96 | }); 97 | pass.setResultPath("$." + ctx.dereference().getText()); 98 | return new StateConstruct<>(pass); 99 | } 100 | 101 | 102 | @Override 103 | public Construct visitAssignmentJsonArray(StepperParser.AssignmentJsonArrayContext ctx) { 104 | Pass pass = new Pass(toLabel(ctx.label())); 105 | pass.captureAttribute("Result"); 106 | pass.handleArray(() -> { 107 | JsonObjectVisitor visitor = new JsonObjectVisitor(pass); 108 | visitor.visit(ctx); 109 | }); 110 | pass.setResultPath("$." + ctx.dereference().getText()); 111 | return new StateConstruct<>(pass); 112 | } 113 | 114 | 115 | @Override 116 | public Construct visitAssignmentExpr(StepperParser.AssignmentExprContext ctx) { 117 | ExpressionConstruct construct = new ExpressionConstruct(toLabel(ctx.label())); 118 | String variable = ctx.dereference().getText(); 119 | String expression = enhance(ctx.complexAssign(), ctx.expr().getText(), variable); 120 | 121 | construct.setVariable(variable); 122 | construct.setExpression(expression); 123 | 124 | DereferencingVisitor defVisitor = new DereferencingVisitor(); 125 | Set symbols = defVisitor.visit(ctx.expr()); 126 | if (ctx.complexAssign().ASSIGN() == null) { 127 | symbols.add(variable); 128 | } 129 | construct.setSymbols(symbols); 130 | return construct; 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/construct/ForLoopConstruct.java: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.construct; 2 | 3 | import com.eclecticlogic.stepper.etc.WeaveContext; 4 | import com.eclecticlogic.stepper.state.Choice; 5 | import com.eclecticlogic.stepper.state.State; 6 | import com.eclecticlogic.stepper.state.Task; 7 | import com.google.common.collect.Lists; 8 | import com.google.common.collect.Sets; 9 | 10 | import java.util.List; 11 | import java.util.Set; 12 | 13 | public class ForLoopConstruct extends Construct { 14 | 15 | private String iterableVariable; 16 | 17 | private String initialExpression; 18 | private Set initialExpressionSymbols; 19 | 20 | private String endingExpression; 21 | private Set endingExpressionSymbols; 22 | 23 | private String stepExpression; 24 | private Set stepExpressionSymbols; 25 | 26 | private Construct block; 27 | 28 | private final Task initializingLambda; 29 | private final Task endingLambda; 30 | private final String choiceVar; 31 | private final Choice choice; 32 | private final Task incrementingLambda; 33 | 34 | 35 | public ForLoopConstruct(String label) { 36 | initializingLambda = new Task(label); 37 | endingLambda = new Task(); 38 | choiceVar = getNextDynamicVariable(); 39 | choice = new Choice(choiceVar); 40 | incrementingLambda = new Task(); 41 | } 42 | 43 | 44 | public void setIterableVariable(String iterableVariable) { 45 | this.iterableVariable = iterableVariable; 46 | } 47 | 48 | 49 | public void setInitialExpression(String initialExpression) { 50 | this.initialExpression = initialExpression; 51 | } 52 | 53 | 54 | public void setInitialExpressionSymbols(Set initialExpressionSymbols) { 55 | this.initialExpressionSymbols = initialExpressionSymbols; 56 | } 57 | 58 | 59 | public void setEndingExpression(String endingExpression) { 60 | this.endingExpression = endingExpression; 61 | } 62 | 63 | 64 | public void setEndingExpressionSymbols(Set endingExpressionSymbols) { 65 | this.endingExpressionSymbols = endingExpressionSymbols; 66 | } 67 | 68 | 69 | public void setStepExpression(String stepExpression) { 70 | this.stepExpression = stepExpression; 71 | } 72 | 73 | 74 | public void setStepExpressionSymbols(Set stepExpressionSymbols) { 75 | this.stepExpressionSymbols = stepExpressionSymbols; 76 | } 77 | 78 | 79 | public void setBlock(Construct block) { 80 | this.block = block; 81 | } 82 | 83 | 84 | void setupInitializer(WeaveContext context) { 85 | constructLambda(context, initializingLambda, initialExpression, initialExpressionSymbols); 86 | 87 | initializingLambda.setupLambdaHelper(); 88 | initializingLambda.setResultPath("$." + iterableVariable); 89 | initializingLambda.setNextState(endingLambda.getName()); 90 | } 91 | 92 | 93 | void setupEnding(WeaveContext context) { 94 | endingExpressionSymbols.add(iterableVariable); 95 | constructLambda(context, endingLambda, iterableVariable + " <= " + endingExpression, endingExpressionSymbols); 96 | 97 | endingLambda.setupLambdaHelper(); 98 | endingLambda.setResultPath("$." + choiceVar); 99 | endingLambda.setNextState(choice.getName()); 100 | } 101 | 102 | 103 | void setupChoice() { 104 | choice.setIfNextState(block.getFirstStateName()); 105 | getLastInChain(block).setNextStateName(incrementingLambda.getName()); 106 | } 107 | 108 | 109 | void setupIncrementer(WeaveContext context) { 110 | if (stepExpression == null) { 111 | // Use default 112 | stepExpression = "1"; 113 | stepExpressionSymbols = Sets.newHashSet(); 114 | } 115 | stepExpressionSymbols.add(iterableVariable); 116 | 117 | constructLambda(context, incrementingLambda, iterableVariable + " + (" + stepExpression + ")", 118 | stepExpressionSymbols); 119 | incrementingLambda.setupLambdaHelper(); 120 | incrementingLambda.setResultPath("$." + iterableVariable); 121 | incrementingLambda.setNextState(endingLambda.getName()); 122 | } 123 | 124 | 125 | @Override 126 | protected String getFirstStateName() { 127 | return initializingLambda.getName(); 128 | } 129 | 130 | 131 | @Override 132 | protected void setNextStateName(String name) { 133 | choice.setElseNextState(name); 134 | } 135 | 136 | 137 | @Override 138 | public void weave(WeaveContext context) { 139 | setupInitializer(context); 140 | setupEnding(context); 141 | setupChoice(); 142 | setupIncrementer(context); 143 | 144 | block.weave(context); 145 | 146 | if (getNext() != null) { 147 | getNext().weave(context); 148 | setNextStateName(getNext().getFirstStateName()); 149 | } 150 | } 151 | 152 | 153 | @Override 154 | public List getStates() { 155 | List result = Lists.newArrayList(initializingLambda, endingLambda, choice); 156 | result.addAll(block.getStates()); 157 | result.add(incrementingLambda); 158 | 159 | if (getNext() != null) { 160 | result.addAll(getNext().getStates()); 161 | } 162 | return result; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/test/groovy/com/eclecticlogic/stepper/asl/TestWhile.groovy: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.asl 2 | 3 | import com.jayway.jsonpath.ReadContext 4 | 5 | 6 | class TestWhile extends AbstractStateMachineTester { 7 | 8 | def "test simple while"() { 9 | given: 10 | TestOutput output = runWithLambda('while.stg', 'simple') 11 | ReadContext ctx = output.ctx 12 | 13 | when: 14 | Closure v = { name, param -> 15 | return ctx.read('$.States.' + name + '.' + param) 16 | } 17 | 18 | then: 19 | Object[] data = ctx.read('$.States.*') 20 | data.length == 7 21 | 22 | ctx.read('$.StartAt') == 'Simple000' 23 | 24 | with('Simple000') { 25 | v(it, 'Type') == 'Pass' 26 | v(it, 'Result') == 5 27 | v(it, 'ResultPath') == '$.a' 28 | v(it, 'Next') == 'abc' 29 | } 30 | 31 | with('abc') { 32 | v(it, 'Type') == 'Task' 33 | v(it, 'Parameters.cmd__sm') == 'abc' 34 | v(it, "Parameters.['a.\$']") == '$.a' 35 | v(it, 'ResultPath') == '$.Simplevar__000' 36 | v(it, 'Next') == 'Simple001' 37 | } 38 | output.lambda.contains('const response = a<15;') 39 | 40 | with('Simple001') { 41 | v(it, 'Type') == 'Choice' 42 | v(it, 'Choices[0].Variable') == '$.Simplevar__000' 43 | v(it, 'Choices[0].BooleanEquals') == true 44 | v(it, 'Choices[0].Next') == 'Simple002' 45 | 46 | v(it, 'Choices[1].Variable') == '$.Simplevar__000' 47 | v(it, 'Choices[1].BooleanEquals') == false 48 | v(it, 'Choices[1].Next') == 'Simple004' 49 | } 50 | 51 | with('Simple002') { 52 | v(it, 'Type') == 'Task' 53 | v(it, 'Parameters.cmd__sm') == 'Simple002' 54 | v(it, "Parameters.['a.\$']") == '$.a' 55 | v(it, 'ResultPath') == '$.b' 56 | v(it, 'Next') == 'Simple003' 57 | } 58 | output.lambda.contains('const response = a+1;') 59 | 60 | with('Simple003') { 61 | v(it, 'Type') == 'Task' 62 | v(it, 'Parameters.cmd__sm') == 'Simple003' 63 | v(it, "Parameters.['a.\$']") == '$.a' 64 | v(it, 'ResultPath') == '$.a' 65 | v(it, 'Next') == 'abc' 66 | } 67 | output.lambda.contains('const response = a+1;') 68 | 69 | with('Simple004') { 70 | v(it, 'Type') == 'Task' 71 | v(it, 'Parameters.cmd__sm') == 'Simple004' 72 | v(it, "Parameters.['a.\$']") == '$.a' 73 | v(it, 'ResultPath') == '$.c' 74 | v(it, 'Next') == 'Simple.Success' 75 | } 76 | output.lambda.contains('const response = a;') 77 | 78 | v("['Simple.Success']", 'Type') == 'Succeed' 79 | } 80 | 81 | 82 | def "test nested while"() { 83 | given: 84 | TestOutput output = runWithLambda('while.stg', 'nested') 85 | ReadContext ctx = output.ctx 86 | 87 | when: 88 | Closure v = { name, param -> 89 | return ctx.read('$.States.' + name + '.' + param) 90 | } 91 | 92 | then: 93 | Object[] data = ctx.read('$.States.*') 94 | data.length == 9 95 | 96 | ctx.read('$.StartAt') == 'Nested000' 97 | 98 | with('Nested000') { 99 | v(it, 'Type') == 'Pass' 100 | v(it, 'Result') == 5 101 | v(it, 'ResultPath') == '$.a' 102 | v(it, 'Next') == 'abc' 103 | } 104 | 105 | with('abc') { 106 | v(it, 'Type') == 'Task' 107 | v(it, 'Parameters.cmd__sm') == 'abc' 108 | v(it, "Parameters.['a.\$']") == '$.a' 109 | v(it, 'ResultPath') == '$.Nestedvar__000' 110 | v(it, 'Next') == 'Nested001' 111 | } 112 | output.lambda.contains('const response = a<15;') 113 | 114 | with('Nested001') { 115 | v(it, 'Type') == 'Choice' 116 | v(it, 'Choices[0].Variable') == '$.Nestedvar__000' 117 | v(it, 'Choices[0].BooleanEquals') == true 118 | v(it, 'Choices[0].Next') == 'Nested002' 119 | 120 | v(it, 'Choices[1].Variable') == '$.Nestedvar__000' 121 | v(it, 'Choices[1].BooleanEquals') == false 122 | v(it, 'Choices[1].Next') == 'Nested.Success' 123 | } 124 | 125 | with('Nested002') { 126 | v(it, 'Type') == 'Task' 127 | v(it, 'Parameters.cmd__sm') == 'Nested002' 128 | v(it, "Parameters.['a.\$']") == '$.a' 129 | v(it, 'ResultPath') == '$.b' 130 | v(it, 'Next') == 'def' 131 | } 132 | output.lambda.contains('const response = a+1;') 133 | 134 | with('def') { 135 | v(it, 'Type') == 'Task' 136 | v(it, 'Parameters.cmd__sm') == 'def' 137 | v(it, "Parameters.['b.\$']") == '$.b' 138 | v(it, 'ResultPath') == '$.Nestedvar__001' 139 | v(it, 'Next') == 'Nested003' 140 | } 141 | output.lambda.contains('const response = b>3;') 142 | 143 | with('Nested003') { 144 | v(it, 'Type') == 'Choice' 145 | v(it, 'Choices[0].Variable') == '$.Nestedvar__001' 146 | v(it, 'Choices[0].BooleanEquals') == true 147 | v(it, 'Choices[0].Next') == 'Nested004' 148 | 149 | v(it, 'Choices[1].Variable') == '$.Nestedvar__001' 150 | v(it, 'Choices[1].BooleanEquals') == false 151 | v(it, 'Choices[1].Next') == 'abc' 152 | } 153 | 154 | with('Nested004') { 155 | v(it, 'Type') == 'Task' 156 | v(it, 'Parameters.cmd__sm') == 'Nested004' 157 | v(it, "Parameters.['a.\$']") == '$.a' 158 | v(it, "Parameters.['b.\$']") == '$.b' 159 | v(it, 'ResultPath') == '$.c' 160 | v(it, 'Next') == 'Nested005' 161 | } 162 | output.lambda.contains('const response = b+a;') 163 | 164 | with('Nested005') { 165 | v(it, 'Type') == 'Task' 166 | v(it, 'Parameters.cmd__sm') == 'Nested005' 167 | v(it, "Parameters.['b.\$']") == '$.b' 168 | v(it, 'ResultPath') == '$.b' 169 | v(it, 'Next') == 'def' 170 | } 171 | output.lambda.contains('const response = b - (1);') 172 | 173 | v("['Nested.Success']", 'Type') == 'Succeed' 174 | } 175 | 176 | 177 | } -------------------------------------------------------------------------------- /src/main/antlr/Stepper.g4: -------------------------------------------------------------------------------- 1 | 2 | grammar Stepper; 3 | 4 | program 5 | : annotation* PROGRAM programName=ID '{' statement+ '}' 6 | ; 7 | 8 | // control statements 9 | 10 | annotation 11 | : '@' ID '(' scalar ')' 12 | ; 13 | 14 | forStatement 15 | : label? FOR '(' ID '=' init=expr TO end=expr (STEP delta=expr)? ')' statementBlock #forLoop 16 | | label? FOR '(' ID IN iterable=expr ')' statementBlock #forIteration 17 | ; 18 | 19 | ifStatement 20 | : label? IF '(' ifCondition=expr ')' ifBlock=statementBlock 21 | (ELSE elseBlock=statementBlock)? 22 | ; 23 | 24 | whileStatement 25 | : label? WHILE '(' expr ')' statementBlock 26 | ; 27 | 28 | whenStatement 29 | : WHEN '{' 30 | caseEntry+ 31 | (ELSE elseBlock=statementBlock)? 32 | '}' 33 | ; 34 | 35 | caseEntry 36 | : label? CASE expr ':' statementBlock 37 | ; 38 | 39 | waitStatement 40 | : label? WAIT jsonObject 41 | ; 42 | 43 | failStatement 44 | : label? FAIL jsonObject? ';'? 45 | ; 46 | 47 | gotoStatement 48 | : GOTO STRING ';'? 49 | ; 50 | 51 | parallelStatement 52 | : retries (dereference ASSIGN)? PARALLEL '(' STRING (',' STRING)* ')' 53 | ; 54 | 55 | tryCatchStatement 56 | : TRY statementBlock catchClause+ 57 | ; 58 | 59 | catchClause 60 | : CATCH '(' STRING (',' STRING)* ')' '{' (dereference CLOSURE)? statement+ '}' 61 | ; 62 | 63 | statementBlock 64 | : '{' statement+ '}' 65 | | statement 66 | ; 67 | 68 | statement 69 | : assignment #statementAssignment 70 | | retries task #statementTask 71 | | forStatement #statementFor 72 | | ifStatement #statementIf 73 | | whileStatement #statementWhile 74 | | whenStatement #statementWhen 75 | | waitStatement #statementWait 76 | | failStatement #statementFail 77 | | gotoStatement #statementGoto 78 | | parallelStatement #statementParallel 79 | | tryCatchStatement #statementTryCatch 80 | ; 81 | 82 | assignment 83 | : retries dereference ASSIGN task SEMICOLON? #assignmentTask 84 | | label? dereference ASSIGN NUMBER SEMICOLON #assignmentNumber 85 | | label? dereference ASSIGN TRUE SEMICOLON #assignmentTrue 86 | | label? dereference ASSIGN FALSE SEMICOLON #assignmentFalse 87 | | label? dereference ASSIGN STRING SEMICOLON #assignmentString 88 | | label? dereference complexAssign expr SEMICOLON #assignmentExpr 89 | | label? dereference ASSIGN jsonObject SEMICOLON? #assignmentJson 90 | | label? dereference ASSIGN '[' value (',' value)* ']' SEMICOLON? #assignmentJsonArray 91 | ; 92 | 93 | expr 94 | : scalar 95 | | NULL 96 | | expressionStatement 97 | | expr MUL expr 98 | | expr DIV expr 99 | | expr ADD expr 100 | | expr SUB expr 101 | | expr MOD expr 102 | | '(' expr ')' 103 | | expr AND expr 104 | | expr OR expr 105 | | NOT expr 106 | | expr EQUALITY expr 107 | | expr LE expr 108 | | expr LT expr 109 | | expr GE expr 110 | | expr GT expr 111 | ; 112 | 113 | expressionStatement 114 | : dereference indexingDereference? ('.' expressionStatement)? 115 | | methodCall indexingDereference? ('.' expressionStatement)? 116 | ; 117 | 118 | methodCall 119 | : ID '(' ')' 120 | | ID '(' expr (',' expr)* ')' 121 | ; 122 | 123 | indexingDereference 124 | : '[' expr ']' 125 | ; 126 | 127 | dereference 128 | : ID ('.' ID )* 129 | ; 130 | 131 | task 132 | : TASK jsonObject 133 | ; 134 | 135 | retries 136 | : retry* label? retry*; 137 | 138 | retry 139 | : '@RetryOnError' '(' STRING (',' STRING)* ')' jsonObject 140 | ; 141 | 142 | label 143 | : '@Label' '(' STRING ')' 144 | ; 145 | 146 | jsonObject 147 | : '{' pair (',' pair)* '}' 148 | ; 149 | 150 | pair 151 | : STRING ':' value 152 | ; 153 | 154 | value 155 | : STRING #valueString 156 | | NUMBER #valueNum 157 | | TRUE #valueTrue 158 | | FALSE #valueFalse 159 | | '{' pair (',' pair)* '}' #valueObj 160 | | '{' '}' #valueObjEmpty 161 | | '[' value (',' value)* ']' #valueArr 162 | | '[' ']' #valueArrEmpty 163 | ; 164 | 165 | scalar 166 | : STRING 167 | | NUMBER 168 | | TRUE 169 | | FALSE 170 | ; 171 | 172 | complexAssign 173 | : (ASSIGN | PLUSASSIGN | MINUSASSIGN | MULTASSIGN | DIVASSIGN) 174 | ; 175 | 176 | // lexer rules 177 | 178 | NUMBER 179 | : '-'? INT ('.' [0-9] +)? EXP? 180 | ; 181 | 182 | STRING 183 | : '"' (ESC | SAFECODEPOINT)* '"' 184 | ; 185 | 186 | // operators 187 | MUL: '*'; 188 | DIV: '/'; 189 | ADD: '+'; 190 | SUB: '-'; 191 | MOD: '%'; 192 | AND: '&&'; 193 | OR: '||'; 194 | NOT: '!'; 195 | EQUALITY: '=='; 196 | LE: '<='; 197 | LT: '<'; 198 | GE: '>='; 199 | GT: '>'; 200 | ASSIGN: '='; 201 | PLUSASSIGN: '+='; 202 | MINUSASSIGN: '-='; 203 | MULTASSIGN: '*='; 204 | DIVASSIGN: '/='; 205 | 206 | // keywords 207 | PROGRAM: 'stepper'; 208 | TASK: 'task'; 209 | PARALLEL: 'parallel'; 210 | WAIT: 'wait'; 211 | FAIL: 'fail'; 212 | GOTO: 'goto'; 213 | 214 | TRUE: 'true'; 215 | FALSE: 'false'; 216 | NULL: 'null'; 217 | IF: 'if'; 218 | ELSE: 'else'; 219 | FOR: 'for'; 220 | IN: 'in'; 221 | TO: 'to'; 222 | STEP: 'step'; 223 | WHILE: 'while'; 224 | WHEN: 'when'; 225 | CASE: 'case'; 226 | TRY: 'try'; 227 | CATCH: 'catch'; 228 | 229 | // symbols 230 | SEMICOLON: ';'; 231 | CLOSURE: '->'; 232 | 233 | ID 234 | : ALPHA (ALPHA | DIGIT)* 235 | ; 236 | 237 | fragment ESC 238 | : '\\' (["\\/bfnrt]) 239 | ; 240 | 241 | fragment SAFECODEPOINT 242 | : ~ ["\\\u0000-\u001F] 243 | ; 244 | 245 | fragment INT 246 | : '0' | [1-9] DIGIT* 247 | ; 248 | 249 | // no leading zeros 250 | 251 | fragment EXP 252 | : [Ee] [+\-]? INT 253 | ; 254 | 255 | fragment DIGIT 256 | : [0-9] 257 | ; 258 | 259 | fragment ALPHA 260 | : [a-zA-Z_] 261 | ; 262 | 263 | COMMENT 264 | : '/*' .*? '*/' -> skip 265 | ; 266 | 267 | LINE_COMMENT 268 | : '//' ~[\r\n]* -> skip 269 | ; 270 | 271 | WS 272 | : [ \t\n\r]+ -> skip 273 | ; -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin, switch paths to Windows format before running java 129 | if $cygwin ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Stepper 2 | ==== 3 | 4 | **Stepper** is to *Step Functions* as **High level language** is to *Assembly*. 5 | 6 | Stepper allows you to write AWS Step Functions using modern programming constructs such as `if else` branching, `for` 7 | and `while` loops, `try catch` for error handling and natural expressions such as `a = arr.length + 1`. 8 | To illustrate, let us create a step function that generates the first 10 Fibonnaci numbers and stores them into an SQS 9 | queue. 10 | 11 | ```Javascript 12 | 13 | @Comment("Generate Fibonnaci numbers") 14 | @TimeoutSeconds(120) 15 | @Version("1.0") 16 | stepper Fibonnaci { 17 | 18 | previous = 1; 19 | fib = 0; 20 | 21 | for (c = 1 to 10) { 22 | body = fib + ""; // queue message must be a string 23 | sqs = task { // write to queue 24 | "Resource": "arn:aws:states:::sqs:sendMessage", 25 | "Parameters": { 26 | "MessageBody.$": "$.body", 27 | "QueueUrl": "https://sqs.us-east-1.amazonaws.com/1570xxxx/fibo" 28 | } 29 | } 30 | temp = fib; 31 | fib += previous; 32 | previous = temp; 33 | } 34 | } 35 | 36 | ``` 37 | 38 | As you can see, the above code feels like a modern programming language (Stepper seeks compatibility with Javascript 39 | expression syntax and built-in functions). The Stepper compiler turns the variables used into ASL json attributes and 40 | then creates a supporting Lambda function to evaluate expressions used. Stepper will compile the code above into the 41 | following state machine. 42 | 43 |
44 | JSON for Fibonnaci step function ... (click to expand). 45 | 46 | ```json 47 | { 48 | "Comment": "Generate Fibonnaci numbers", 49 | "TimeoutSeconds": 120, 50 | "Version": "1.0", 51 | "StartAt": "Fibonnaci000", 52 | "States": { 53 | "Fibonnaci000": { 54 | "Type": "Pass", 55 | "Result": 1, 56 | "ResultPath": "$.previous", 57 | "Next": "Fibonnaci001" 58 | }, 59 | "Fibonnaci001": { 60 | "Type": "Pass", 61 | "Result": 0, 62 | "ResultPath": "$.fib", 63 | "Next": "Fibonnaci002" 64 | }, 65 | "Fibonnaci002": { 66 | "Type": "Task", 67 | "Parameters": { 68 | "cmd__sm": "Fibonnaci002" 69 | }, 70 | "Resource": "arn:aws:lambda:us-east-1:1570xxxx:function:Fibonnaci_stepperLambda", 71 | "ResultPath": "$.c", 72 | "Next": "Fibonnaci003" 73 | }, 74 | "Fibonnaci003": { 75 | "Type": "Task", 76 | "Parameters": { 77 | "cmd__sm": "Fibonnaci003", 78 | "c.$": "$.c" 79 | }, 80 | "Resource": "arn:aws:lambda:us-east-1:1570xxxx:function:Fibonnaci_stepperLambda", 81 | "ResultPath": "$.Fibonnacivar__000", 82 | "Next": "Fibonnaci004" 83 | }, 84 | "Fibonnaci004": { 85 | "Type": "Choice", 86 | "Choices": [ 87 | { 88 | "Variable": "$.Fibonnacivar__000", 89 | "BooleanEquals": true, 90 | "Next": "Fibonnaci006" 91 | }, 92 | { 93 | "Variable": "$.Fibonnacivar__000", 94 | "BooleanEquals": false, 95 | "Next": "Fibonnaci.Success" 96 | } 97 | ] 98 | }, 99 | "Fibonnaci006": { 100 | "Type": "Task", 101 | "Parameters": { 102 | "cmd__sm": "Fibonnaci006", 103 | "fib.$": "$.fib" 104 | }, 105 | "ResultPath": "$.body", 106 | "Resource": "arn:aws:lambda:us-east-1:1570xxxx:function:Fibonnaci_stepperLambda", 107 | "Next": "Fibonnaci007" 108 | }, 109 | "Fibonnaci007": { 110 | "Type": "Task", 111 | "Resource": "arn:aws:states:::sqs:sendMessage", 112 | "Parameters": { 113 | "MessageBody.$": "$.body", 114 | "QueueUrl": "https://sqs.us-east-1.amazonaws.com/1570xxxx/fibo" 115 | }, 116 | "ResultPath": "$.sqs", 117 | "Next": "Fibonnaci008" 118 | }, 119 | "Fibonnaci008": { 120 | "Type": "Task", 121 | "Parameters": { 122 | "cmd__sm": "Fibonnaci008", 123 | "fib.$": "$.fib" 124 | }, 125 | "ResultPath": "$.temp", 126 | "Resource": "arn:aws:lambda:us-east-1:1570xxxx:function:Fibonnaci_stepperLambda", 127 | "Next": "Fibonnaci009" 128 | }, 129 | "Fibonnaci009": { 130 | "Type": "Task", 131 | "Parameters": { 132 | "cmd__sm": "Fibonnaci009", 133 | "previous.$": "$.previous", 134 | "fib.$": "$.fib" 135 | }, 136 | "ResultPath": "$.fib", 137 | "Resource": "arn:aws:lambda:us-east-1:1570xxxx:function:Fibonnaci_stepperLambda", 138 | "Next": "Fibonnaci010" 139 | }, 140 | "Fibonnaci010": { 141 | "Type": "Task", 142 | "Next": "Fibonnaci005", 143 | "Parameters": { 144 | "cmd__sm": "Fibonnaci010", 145 | "temp.$": "$.temp" 146 | }, 147 | "ResultPath": "$.previous", 148 | "Resource": "arn:aws:lambda:us-east-1:1570xxxx:function:Fibonnaci_stepperLambda" 149 | }, 150 | "Fibonnaci005": { 151 | "Type": "Task", 152 | "Parameters": { 153 | "cmd__sm": "Fibonnaci005", 154 | "c.$": "$.c" 155 | }, 156 | "Resource": "arn:aws:lambda:us-east-1:1570xxxx:function:Fibonnaci_stepperLambda", 157 | "ResultPath": "$.c", 158 | "Next": "Fibonnaci003" 159 | }, 160 | "Fibonnaci.Success": { 161 | "Type": "Succeed" 162 | } 163 | } 164 | } 165 | ``` 166 |
167 | 168 | 169 | 170 | Stepper can automatically register the Lambda and create the Step Function or it can output the code for those pieces 171 | and you can manually register them. Stepper supports: 172 | 173 | - [variables, assignments and expressions](../../wiki/Language-Reference#variables) 174 | - [if/else](../../wiki/Language-Reference#branching) branching 175 | - for [loops](../../wiki/Language-Reference#loops) over range with step and looping over collections 176 | - [while loops](../../wiki/Language-Reference#while) 177 | - "[when](../../wiki/Language-Reference#when)" statement, a variation of the traditional switch statement for multi-predicate branching 178 | - [task](../../wiki/Language-Reference#tasks) ASL state for calling activities, accessing queues, etc. 179 | - [wait and fail](../../wiki/Language-Reference#errors) construct 180 | - annotation driven retry logic 181 | - control over state names 182 | - [parallel](../../wiki/Language-Reference#parallel) state that simply includes other stepper programs to be run concurrently 183 | - [goto](../../wiki/Language-Reference#goto) statement for complex logic 184 | - [try/catch](../../wiki/Language-Reference#trycatch) construct for error handling. 185 | 186 | To learn how to get started with Stepper, head over to the [Getting Started](../../wiki/Getting-Started) page of the wiki. The [language reference](../../wiki/Language-Reference) is 187 | also accessible from the wiki pages. 188 | -------------------------------------------------------------------------------- /src/test/resources/antlr/grammar/grammar.stg: -------------------------------------------------------------------------------- 1 | 2 | testValueNested() ::= << 3 | { 4 | "test": { 5 | "a": ["b", { 6 | "t": {}, 7 | "u": "v", 8 | "w": [false, 1] 9 | }, "c"], 10 | "d": { 11 | "e": true, 12 | "f": false 13 | } 14 | } 15 | } 16 | 17 | >> 18 | 19 | testTask() ::= << 20 | 21 | task { 22 | "Resource": "arn:aws:states:::sqs:sendMessage", 23 | "Parameters": { 24 | "MessageBody": "$.dynamo.Item.Message.S", 25 | "QueueUrl": "https://sqs.us-east-1.amazonaws.com/123456789012/sqsconnector-SQSQueue-QVGQBW134PWK" 26 | } 27 | } 28 | 29 | >> 30 | 31 | testStatementTask1() ::= << 32 | 33 | @Label("hello how are you") 34 | @RetryOnError("abc") { "IntervalSeconds": 3, "MaxAttempts": 4, "BackoffRate": 5} 35 | @RetryOnError("xyz", "pqr") { "IntervalSeconds": 3 } 36 | task { 37 | "Resource": "arn:aws:states:::sqs:sendMessage", 38 | "Parameters": { 39 | "MessageBody": "$.dynamo.Item.Message.S", 40 | "QueueUrl": "https://sqs.us-east-1.amazonaws.com/123456789012/sqsconnector-SQSQueue-QVGQBW134PWK" 41 | } 42 | } 43 | 44 | >> 45 | 46 | testStatementTask2() ::= << 47 | 48 | @RetryOnError("abc") { "IntervalSeconds": 3, "MaxAttempts": 4, "BackoffRate": 5} 49 | @Label("hello how are you") 50 | @RetryOnError("xyz", "pqr") { "IntervalSeconds": 3 } 51 | task { 52 | "Resource": "arn:aws:states:::sqs:sendMessage", 53 | "Parameters": { 54 | "MessageBody": "$.dynamo.Item.Message.S", 55 | "QueueUrl": "https://sqs.us-east-1.amazonaws.com/123456789012/sqsconnector-SQSQueue-QVGQBW134PWK" 56 | } 57 | } 58 | 59 | >> 60 | 61 | testStatementTask3() ::= << 62 | 63 | @RetryOnError("abc") { "IntervalSeconds": 3, "MaxAttempts": 4, "BackoffRate": 5} 64 | @RetryOnError("xyz", "pqr") { "IntervalSeconds": 3 } 65 | @Label("hello how are you") 66 | task { 67 | "Resource": "arn:aws:states:::sqs:sendMessage", 68 | "Parameters": { 69 | "MessageBody": "$.dynamo.Item.Message.S", 70 | "QueueUrl": "https://sqs.us-east-1.amazonaws.com/123456789012/sqsconnector-SQSQueue-QVGQBW134PWK" 71 | } 72 | } 73 | 74 | >> 75 | 76 | testStatementTask4() ::= << 77 | 78 | @Label("hello how are you") 79 | task { 80 | "Resource": "arn:aws:states:::sqs:sendMessage", 81 | "Parameters": { 82 | "MessageBody": "$.dynamo.Item.Message.S", 83 | "QueueUrl": "https://sqs.us-east-1.amazonaws.com/123456789012/sqsconnector-SQSQueue-QVGQBW134PWK" 84 | } 85 | } 86 | 87 | >> 88 | 89 | testAssignmentTask1() ::= << 90 | 91 | @RetryOnError("abc") { "IntervalSeconds": 3, "MaxAttempts": 4, "BackoffRate": 5} 92 | @RetryOnError("xyz", "pqr") { "IntervalSeconds": 3 } 93 | abc = task { 94 | "Resource": "arn:aws:states:::sqs:sendMessage", 95 | "Parameters": { 96 | "MessageBody": "$.dynamo.Item.Message.S", 97 | "QueueUrl": "https://sqs.us-east-1.amazonaws.com/123456789012/sqsconnector-SQSQueue-QVGQBW134PWK" 98 | } 99 | } 100 | 101 | >> 102 | 103 | testAssignmentTask2() ::= << 104 | 105 | @RetryOnError("abc") { "IntervalSeconds": 3, "MaxAttempts": 4, "BackoffRate": 5} 106 | @Label("hello how are you") 107 | @RetryOnError("xyz", "pqr") { "IntervalSeconds": 3 } 108 | abc = task { 109 | "Resource": "arn:aws:states:::sqs:sendMessage", 110 | "Parameters": { 111 | "MessageBody": "$.dynamo.Item.Message.S", 112 | "QueueUrl": "https://sqs.us-east-1.amazonaws.com/123456789012/sqsconnector-SQSQueue-QVGQBW134PWK" 113 | } 114 | } 115 | 116 | >> 117 | 118 | testAssignmentJson() ::= << 119 | @Label("xyz") result = { 120 | "test": { 121 | "a": ["b", { 122 | "t": {}, 123 | "u": "v", 124 | "w": [false, 1] 125 | }, "c"], 126 | "d": { 127 | "e": true, 128 | "f": false 129 | } 130 | } 131 | } 132 | 133 | >> 134 | 135 | testForLoop1() ::= << 136 | @Label("abc") for (i = 0 to c * 4) { 137 | a = b; 138 | } 139 | >> 140 | testForLoop2() ::= << 141 | for (i = 0 to c * 4 step 3) { 142 | a = b; 143 | } 144 | >> 145 | testForLoop3() ::= << 146 | @Label("abc") for (i = d to c * 4 step -53 * a) { 147 | a = b; 148 | } 149 | >> 150 | 151 | testForIteration() ::= << 152 | @Label("abc") for (i in json.array) { 153 | a = b; 154 | } 155 | >> 156 | 157 | testIf1() ::= << 158 | if (c * 3 > 4) 159 | a = b; 160 | >> 161 | testIf2() ::= << 162 | @Label("abc") if (c * 3 > 4) { 163 | a = b; 164 | } 165 | >> 166 | testIf3() ::= << 167 | if (c * 3 > 4) 168 | a = b; 169 | else if (d > 0) { 170 | b = 5; 171 | } else { 172 | c = 6 173 | } 174 | >> 175 | 176 | testWhile() ::= << 177 | @Label("abc") 178 | while (a > b) { 179 | c = task { 180 | "a": "b" 181 | } 182 | } 183 | >> 184 | 185 | testWhen1() ::= << 186 | when { 187 | case a > b: task { "a": "b" } 188 | } 189 | >> 190 | 191 | testWhen2() ::= << 192 | when { 193 | @Label("abc") case a > b: task { "a": "b" } 194 | @Label("def") case c > d: { 195 | a1 = 1; 196 | a2 = 2; 197 | } 198 | else d = 10; 199 | } 200 | >> 201 | 202 | testWhen3() ::= << 203 | when { 204 | case a > b: task { "a": "b" } 205 | case c > d: { 206 | a1 = 1; 207 | a2 = 2; 208 | } 209 | else { 210 | d1 = 10; 211 | d2 = 20; 212 | } 213 | } 214 | >> 215 | 216 | testWait1() ::= << 217 | wait { 218 | "a": 10 219 | } 220 | >> 221 | testWait2() ::= << 222 | @Label("abc") 223 | wait { 224 | "a": 10 225 | } 226 | >> 227 | 228 | testFail0() ::= << 229 | fail; 230 | >> 231 | 232 | testFail1() ::= << 233 | @Label("a") fail 234 | >> 235 | 236 | testFail2() ::= << 237 | @Label("a") fail { 238 | "Cause": "blah", 239 | "Error": "error" 240 | } 241 | >> 242 | 243 | parallel1() ::= << 244 | x = parallel("a", "b") 245 | >> 246 | 247 | parallel2() ::= << 248 | @Label("xyz") 249 | x = parallel("a", "b") 250 | >> 251 | 252 | parallel3() ::= << 253 | @RetryOnError("abc") { "IntervalSeconds": 3, "MaxAttempts": 4, "BackoffRate": 5} 254 | @Label("hello how are you") 255 | @RetryOnError("xyz", "pqr") { "IntervalSeconds": 3 } 256 | x = parallel("a", "b") 257 | >> 258 | 259 | trycatch1() ::= << 260 | try a = 5; catch("b") { d = 3; } 261 | >> 262 | 263 | trycatch2() ::= << 264 | try a = 5; catch("b") { output -> d = 3; } 265 | >> 266 | 267 | trycatch3() ::= << 268 | try a = 5; catch("b") { output -> d = 3; c = 10; } 269 | >> 270 | 271 | trycatch4() ::= << 272 | try { 273 | a = 5; 274 | b = 10; 275 | } catch("q", "r", "s") { output -> d = 3; c = 10; } 276 | >> -------------------------------------------------------------------------------- /src/test/java/com/eclecticlogic/stepper/antlr/TestGrammar.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015-2019 KR Abram 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.eclecticlogic.stepper.antlr; 18 | 19 | import org.antlr.v4.runtime.ParserRuleContext; 20 | import org.junit.Test; 21 | 22 | import java.util.function.Function; 23 | 24 | /** 25 | * Tests parsing rules. Simply tests that parsing doesn't fail with exceptions. 26 | */ 27 | public class TestGrammar extends AbstractGrammarTester { 28 | 29 | 30 | void testScalarTypeUse(Function parserFunction) { 31 | parse("\"hello\"", parserFunction); 32 | parse("\"he\"llo\"", parserFunction); 33 | parse("\"he\"\"llo\"", parserFunction); 34 | parse("123", parserFunction); 35 | parse("123.23", parserFunction); 36 | parse("123.23e+10", parserFunction); 37 | parse("123.23e-10", parserFunction); 38 | parse("-123.23e-10", parserFunction); 39 | parse("true", parserFunction); 40 | parse("false", parserFunction); 41 | } 42 | 43 | 44 | @Test 45 | public void testScalar() { 46 | testScalarTypeUse(StepperParser::scalar); 47 | } 48 | 49 | 50 | @Test 51 | public void testValue() { 52 | Function fn = StepperParser::value; 53 | testScalarTypeUse(fn); 54 | parse("{}", fn); 55 | parse("[]", fn); 56 | parse("[1, 2, true, false]", fn); 57 | parse("[\"a\", \"b\"]", fn); 58 | parse("{\"attrib\": 123}", fn); 59 | parse("[{\"attrib\": 123}]", fn); 60 | parse("[{\"attrib\": 123, \"c\": [\"a\", true]}]", fn); 61 | parse("grammar.stg", "testValueNested", fn); 62 | } 63 | 64 | 65 | @Test 66 | public void testJsonObject() { 67 | parse("grammar.stg", "testValueNested", StepperParser::jsonObject); 68 | } 69 | 70 | 71 | @Test 72 | public void testTask() { 73 | parse("grammar.stg", "testTask", StepperParser::task); 74 | parse("grammar.stg", "testStatementTask1", StepperParser::statement); 75 | parse("grammar.stg", "testStatementTask2", StepperParser::statement); 76 | parse("grammar.stg", "testStatementTask3", StepperParser::statement); 77 | parse("grammar.stg", "testStatementTask4", StepperParser::statement); 78 | } 79 | 80 | 81 | @Test 82 | public void testDereference() { 83 | Function fn = StepperParser::dereference; 84 | parse("abc.def", fn); 85 | parse("abc", fn); 86 | parse("abc.d.e.f", fn); 87 | } 88 | 89 | 90 | @Test 91 | public void testExpr() { 92 | Function fn = StepperParser::expr; 93 | testScalarTypeUse(fn); 94 | parse("null", fn); 95 | parse("12 * 3", fn); 96 | parse("12 / 3", fn); 97 | parse("12 + 3", fn); 98 | parse("12 - 3", fn); 99 | parse("a * b", fn); 100 | parse("a / b", fn); 101 | parse("a + b", fn); 102 | parse("a - b", fn); 103 | parse("(a - b)", fn); 104 | parse("c + (a - b)", fn); 105 | parse("c && (a || !b)", fn); 106 | parse("c == (a + b) * c", fn); 107 | parse("c < (a + b) * c", fn); 108 | parse("c <= (a + b) * c", fn); 109 | parse("c > (a + b) * c", fn); 110 | parse("c >= (a + b) * c", fn); 111 | parse("c.d.e", fn); 112 | parse("c.d.e[3].f", fn); 113 | parse("c.d.e[3].f().g", fn); 114 | parse("c().d().e[0][1]", fn); 115 | parse("c(a+b).d(r && f).e[0][w-q(4)]", fn); 116 | parse("c(a+b, r(z).d && f)", fn); 117 | } 118 | 119 | 120 | @Test 121 | public void testAssignment() { 122 | Function fn = StepperParser::assignment; 123 | parse("grammar.stg", "testAssignmentTask1", fn); 124 | parse("grammar.stg", "testAssignmentTask2", fn); 125 | parse("@Label(\"xyz\") a.b.c = d - g();", fn); 126 | parse("a.b.c -= d - g();", fn); 127 | parse("@Label(\"xyz\") a.b.c *= d - g();", fn); 128 | parse("a.b.c /= d - g();", fn); 129 | parse("grammar.stg", "testAssignmentJson", fn); 130 | parse("@Label(\"xyz\") ab.cd = [1, 2, 3]", fn); 131 | } 132 | 133 | 134 | @Test 135 | public void testFor() { 136 | Function fn = StepperParser::forStatement; 137 | parse("grammar.stg", "testForLoop1", fn); 138 | parse("grammar.stg", "testForLoop2", fn); 139 | parse("grammar.stg", "testForLoop3", fn); 140 | parse("grammar.stg", "testForIteration", fn); 141 | } 142 | 143 | 144 | @Test 145 | public void testIf() { 146 | Function fn = StepperParser::ifStatement; 147 | parse("grammar.stg", "testIf1", fn); 148 | parse("grammar.stg", "testIf2", fn); 149 | parse("grammar.stg", "testIf3", fn); 150 | } 151 | 152 | 153 | @Test 154 | public void testWhile() { 155 | Function fn = StepperParser::whileStatement; 156 | parse("grammar.stg", "testWhile", fn); 157 | } 158 | 159 | 160 | @Test 161 | public void testWhen() { 162 | Function fn = StepperParser::whenStatement; 163 | parse("grammar.stg", "testWhen1", fn); 164 | parse("grammar.stg", "testWhen2", fn); 165 | parse("grammar.stg", "testWhen3", fn); 166 | } 167 | 168 | 169 | @Test 170 | public void testWait() { 171 | Function fn = StepperParser::waitStatement; 172 | parse("grammar.stg", "testWait1", fn); 173 | parse("grammar.stg", "testWait2", fn); 174 | } 175 | 176 | 177 | @Test 178 | public void testFail() { 179 | Function fn = StepperParser::failStatement; 180 | parse("grammar.stg", "testFail0", fn); 181 | parse("grammar.stg", "testFail1", fn); 182 | parse("grammar.stg", "testFail2", fn); 183 | } 184 | 185 | 186 | @Test 187 | public void testGoto() { 188 | Function fn = StepperParser::gotoStatement; 189 | parse("goto \"abc\";", fn); 190 | parse("goto \"abc\"", fn); 191 | } 192 | 193 | 194 | @Test 195 | public void testParallel() { 196 | Function fn = StepperParser::parallelStatement; 197 | parse("grammar.stg", "parallel1", fn); 198 | parse("grammar.stg", "parallel2", fn); 199 | parse("grammar.stg", "parallel3", fn); 200 | } 201 | 202 | 203 | @Test 204 | public void testTryCatch() { 205 | Function fn = StepperParser::tryCatchStatement; 206 | parse("grammar.stg", "trycatch1", fn); 207 | parse("grammar.stg", "trycatch2", fn); 208 | parse("grammar.stg", "trycatch3", fn); 209 | parse("grammar.stg", "trycatch4", fn); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/main/java/com/eclecticlogic/stepper/visitor/StatementVisitor.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015-2019 KR Abram 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.eclecticlogic.stepper.visitor; 18 | 19 | import com.eclecticlogic.stepper.antlr.StepperParser; 20 | import com.eclecticlogic.stepper.construct.*; 21 | import com.eclecticlogic.stepper.etc.Etc; 22 | import com.eclecticlogic.stepper.state.Parallel; 23 | import com.eclecticlogic.stepper.state.Task; 24 | import com.eclecticlogic.stepper.state.observer.StateObserver; 25 | 26 | import java.util.List; 27 | 28 | import static com.eclecticlogic.stepper.etc.Etc.strip; 29 | import static com.eclecticlogic.stepper.etc.Etc.toLabel; 30 | import static java.util.stream.Collectors.toList; 31 | 32 | 33 | public class StatementVisitor extends AbstractVisitor { 34 | 35 | 36 | @Override 37 | public Construct visitStatementTask(StepperParser.StatementTaskContext ctx) { 38 | RetryVisitor retryVisitor = new RetryVisitor<>(Task::new); 39 | Task task = retryVisitor.visit(ctx.retries()); 40 | 41 | JsonObjectVisitor jsonObjectVisitor = new JsonObjectVisitor(task); 42 | jsonObjectVisitor.visit(ctx.task().jsonObject()); 43 | 44 | task.setResultPath("$." + task.getName(), false); 45 | return new StateConstruct<>(task); 46 | } 47 | 48 | 49 | @Override 50 | public Construct visitStatementParallel(StepperParser.StatementParallelContext stCtx) { 51 | StepperParser.ParallelStatementContext ctx = stCtx.parallelStatement(); 52 | RetryVisitor retryVisitor = new RetryVisitor<>(Parallel::new); 53 | Parallel parallel = retryVisitor.visit(ctx.retries()); 54 | 55 | ParallelConstruct construct = new ParallelConstruct(parallel); 56 | List references = ctx.STRING().stream().map(t -> strip(t.getText())).collect(toList()); 57 | construct.setReferences(references); 58 | 59 | if (ctx.dereference() != null) { 60 | construct.setResultPath("$." + ctx.dereference().getText()); 61 | } 62 | return construct; 63 | } 64 | 65 | 66 | @Override 67 | public Construct visitStatementAssignment(StepperParser.StatementAssignmentContext ctx) { 68 | AssignmentVisitor visitor = new AssignmentVisitor(); 69 | return visitor.visit(ctx.assignment()); 70 | } 71 | 72 | 73 | @Override 74 | public Construct visitIfStatement(StepperParser.IfStatementContext ctx) { 75 | IfConstruct construct = new IfConstruct(toLabel(ctx.label())); 76 | construct.setCondition(ctx.ifCondition.getText()); 77 | 78 | DereferencingVisitor defVisitor = new DereferencingVisitor(); 79 | construct.setSymbols(defVisitor.visit(ctx.ifCondition)); 80 | 81 | StatementBlockVisitor ifBlockVisitor = new StatementBlockVisitor(); 82 | construct.setFirstIf(ifBlockVisitor.visit(ctx.ifBlock)); 83 | 84 | if (ctx.ELSE() != null) { 85 | StatementBlockVisitor elseBlockVisitor = new StatementBlockVisitor(); 86 | construct.setFirstElse(elseBlockVisitor.visit(ctx.elseBlock)); 87 | } 88 | 89 | return construct; 90 | } 91 | 92 | 93 | @Override 94 | public Construct visitStatementFor(StepperParser.StatementForContext ctx) { 95 | ForVisitor visitor = new ForVisitor(); 96 | return visitor.visit(ctx.forStatement()); 97 | } 98 | 99 | 100 | @Override 101 | public Construct visitWhileStatement(StepperParser.WhileStatementContext ctx) { 102 | WhileConstruct construct = new WhileConstruct(toLabel(ctx.label())); 103 | construct.setExpression(ctx.expr().getText()); 104 | 105 | DereferencingVisitor defVisitor = new DereferencingVisitor(); 106 | construct.setSymbols(defVisitor.visit(ctx.expr())); 107 | 108 | StatementBlockVisitor visitor = new StatementBlockVisitor(); 109 | construct.setBlock(visitor.visit(ctx.statementBlock())); 110 | 111 | return construct; 112 | } 113 | 114 | 115 | @Override 116 | public Construct visitWhenStatement(StepperParser.WhenStatementContext ctx) { 117 | WhenConstruct construct = new WhenConstruct(); 118 | for (StepperParser.CaseEntryContext caseEntry : ctx.caseEntry()) { 119 | WhenCase wcase = construct.addCase(toLabel(caseEntry.label())); 120 | 121 | DereferencingVisitor defVisitor = new DereferencingVisitor(); 122 | StatementBlockVisitor statementBlockVisitor = new StatementBlockVisitor(); 123 | Construct blockConstruct = statementBlockVisitor.visit(caseEntry.statementBlock()); 124 | 125 | wcase.setSymbols(defVisitor.visit(caseEntry.expr())); 126 | wcase.setExpression(caseEntry.expr().getText()); 127 | wcase.setBlock(blockConstruct); 128 | } 129 | 130 | if (ctx.elseBlock != null) { 131 | StatementBlockVisitor statementBlockVisitor = new StatementBlockVisitor(); 132 | construct.setElseBlock(statementBlockVisitor.visit(ctx.elseBlock)); 133 | } 134 | return construct; 135 | } 136 | 137 | 138 | @Override 139 | public Construct visitStatementWait(StepperParser.StatementWaitContext ctx) { 140 | WaitConstruct construct = new WaitConstruct(toLabel(ctx.waitStatement().label())); 141 | 142 | JsonObjectVisitor jsonObjectVisitor = new JsonObjectVisitor(construct.getState()); 143 | jsonObjectVisitor.visit(ctx.waitStatement().jsonObject()); 144 | 145 | return construct; 146 | } 147 | 148 | 149 | @Override 150 | public Construct visitStatementFail(StepperParser.StatementFailContext ctx) { 151 | FailConstruct construct = new FailConstruct(toLabel(ctx.failStatement().label())); 152 | 153 | if (ctx.failStatement().jsonObject() != null) { 154 | JsonObjectVisitor jsonObjectVisitor = new JsonObjectVisitor(construct.getState()); 155 | jsonObjectVisitor.visit(ctx.failStatement().jsonObject()); 156 | } 157 | 158 | return construct; 159 | } 160 | 161 | 162 | @Override 163 | public Construct visitStatementGoto(StepperParser.StatementGotoContext ctx) { 164 | return new GotoConstruct(strip(ctx.gotoStatement().STRING().getText())); 165 | } 166 | 167 | 168 | @Override 169 | public Construct visitTryCatchStatement(StepperParser.TryCatchStatementContext ctx) { 170 | TryCatchConstruct construct = new TryCatchConstruct(); 171 | StateObserver.register(construct, () -> { 172 | StatementBlockVisitor statementBlockVisitor = new StatementBlockVisitor(); 173 | Construct blockConstruct = statementBlockVisitor.visit(ctx.statementBlock()); 174 | construct.setTryBlock(blockConstruct); 175 | }); 176 | ctx.catchClause().forEach(clause -> { 177 | CatchClause cc = new CatchClause(); 178 | cc.setErrors(clause.STRING().stream().map(it -> strip(it.getText())).collect(toList())); 179 | 180 | Construct current = null, first = null; 181 | for (StepperParser.StatementContext stCtx : clause.statement()) { 182 | StatementVisitor visitor = new StatementVisitor(); 183 | Construct c = visitor.visit(stCtx); 184 | if (current == null) { 185 | current = c; 186 | first = c; 187 | } else { 188 | current.setNext(c); 189 | current = c; 190 | } 191 | } 192 | cc.setBlock(first); 193 | 194 | if (clause.dereference() != null) { 195 | cc.setResultPath(clause.dereference().getText()); 196 | } 197 | construct.add(cc); 198 | }); 199 | return construct; 200 | } 201 | 202 | } 203 | -------------------------------------------------------------------------------- /src/test/groovy/com/eclecticlogic/stepper/asl/TestTryCatch.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015-2019 KR Abram 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package com.eclecticlogic.stepper.asl 18 | 19 | import com.jayway.jsonpath.ReadContext 20 | 21 | 22 | class TestTryCatch extends AbstractStateMachineTester { 23 | 24 | def "test trycatch none affected"() { 25 | given: 26 | TestOutput output = runWithLambda('trycatch.stg', 'simple0') 27 | ReadContext ctx = output.ctx 28 | 29 | when: 30 | Closure v = { name, param -> 31 | return ctx.read('$.States.' + name + '.' + param) 32 | } 33 | 34 | then: 35 | Object[] data = ctx.read('$.States.*') 36 | data.length == 5 37 | 38 | ctx.read('$.StartAt') == 'Simple000' 39 | 40 | with('Simple000') { 41 | v(it, 'Type') == 'Pass' 42 | v(it, 'Result') == 5 43 | v(it, 'ResultPath') == '$.a' 44 | v(it, 'Next') == 'Simple001' 45 | } 46 | 47 | with('Simple001') { 48 | v(it, 'Type') == 'Pass' 49 | v(it, 'Result') == 3 50 | v(it, 'ResultPath') == '$.b' 51 | v(it, 'Next') == 'Simple.Success' 52 | } 53 | 54 | with('Simple002') { 55 | v(it, 'Type') == 'Pass' 56 | v(it, 'Result') == 3 57 | v(it, 'ResultPath') == '$.c' 58 | v(it, 'Next') == 'Simple003' 59 | } 60 | 61 | with('Simple003') { 62 | v(it, 'Type') == 'Fail' 63 | } 64 | 65 | v("['Simple.Success']", 'Type') == 'Succeed' 66 | } 67 | 68 | 69 | def "test try single catch"() { 70 | given: 71 | TestOutput output = runWithLambda('trycatch.stg', 'simple1') 72 | ReadContext ctx = output.ctx 73 | 74 | when: 75 | Closure v = { name, param -> 76 | return ctx.read('$.States.' + name + '.' + param) 77 | } 78 | 79 | then: 80 | Object[] data = ctx.read('$.States.*') 81 | data.length == 5 82 | 83 | ctx.read('$.StartAt') == 'Simple000' 84 | 85 | with('Simple000') { 86 | v(it, 'Type') == 'Pass' 87 | v(it, 'Result') == 5 88 | v(it, 'ResultPath') == '$.a' 89 | v(it, 'Next') == 'Simple001' 90 | } 91 | 92 | with('Simple001') { 93 | v(it, 'Type') == 'Task' 94 | v(it, 'Parameters.cmd__sm') == 'Simple001' 95 | v(it, "Parameters.['a.\$']") == '$.a' 96 | v(it, 'ResultPath') == '$.b' 97 | v(it, 'Catch[0].ErrorEquals[0]') == 'e1' 98 | v(it, 'Catch[0].ErrorEquals[1]') == 'e2' 99 | v(it, 'Catch[0].ResultPath') == '$.errorInfo' 100 | v(it, 'Catch[0].Next') == 'Simple002' 101 | v(it, 'Next') == 'Simple.Success' 102 | } 103 | output.lambda.contains('const response = 3*a;') 104 | 105 | with('Simple002') { 106 | v(it, 'Type') == 'Pass' 107 | v(it, 'Result') == 3 108 | v(it, 'ResultPath') == '$.c' 109 | v(it, 'Next') == 'Simple003' 110 | } 111 | 112 | with('Simple003') { 113 | v(it, 'Type') == 'Fail' 114 | } 115 | 116 | v("['Simple.Success']", 'Type') == 'Succeed' 117 | } 118 | 119 | 120 | def "test try double catch"() { 121 | given: 122 | TestOutput output = runWithLambda('trycatch.stg', 'simple2') 123 | ReadContext ctx = output.ctx 124 | 125 | when: 126 | Closure v = { name, param -> 127 | return ctx.read('$.States.' + name + '.' + param) 128 | } 129 | 130 | then: 131 | Object[] data = ctx.read('$.States.*') 132 | data.length == 7 133 | 134 | ctx.read('$.StartAt') == 'Simple000' 135 | 136 | with('Simple000') { 137 | v(it, 'Type') == 'Pass' 138 | v(it, 'Result') == 5 139 | v(it, 'ResultPath') == '$.a' 140 | v(it, 'Next') == 'Simple001' 141 | } 142 | 143 | with('Simple001') { 144 | v(it, 'Type') == 'Task' 145 | v(it, 'Parameters.cmd__sm') == 'Simple001' 146 | v(it, "Parameters.['a.\$']") == '$.a' 147 | v(it, 'ResultPath') == '$.b' 148 | v(it, 'Catch[0].ErrorEquals[0]') == 'e1' 149 | v(it, 'Catch[0].ErrorEquals[1]') == 'e2' 150 | v(it, 'Catch[0].ResultPath') == '$.errorInfo' 151 | v(it, 'Catch[0].Next') == 'Simple002' 152 | v(it, 'Catch[1].ErrorEquals[0]') == 'e3' 153 | v(it, 'Catch[1].ErrorEquals[1]') == 'e4' 154 | v(it, 'Catch[1].Next') == 'Simple004' 155 | v(it, 'Next') == 'g1' 156 | } 157 | 158 | with('Simple002') { 159 | v(it, 'Type') == 'Pass' 160 | v(it, 'Result') == 3 161 | v(it, 'ResultPath') == '$.c' 162 | v(it, 'Next') == 'Simple003' 163 | } 164 | 165 | with('Simple003') { 166 | v(it, 'Type') == 'Pass' 167 | v(it, 'Next') == 'g1' 168 | } 169 | 170 | with('Simple004') { 171 | v(it, 'Type') == 'Fail' 172 | } 173 | 174 | with('g1') { 175 | v(it, 'Type') == 'Pass' 176 | v(it, 'Result') == 10 177 | v(it, 'ResultPath') == '$.d' 178 | v(it, 'Next') == 'Simple.Success' 179 | } 180 | 181 | v("['Simple.Success']", 'Type') == 'Succeed' 182 | } 183 | 184 | 185 | def "test try catch nested"() { 186 | given: 187 | TestOutput output = runWithLambda('trycatch.stg', 'nested') 188 | ReadContext ctx = output.ctx 189 | 190 | when: 191 | Closure v = { name, param -> 192 | return ctx.read('$.States.' + name + '.' + param) 193 | } 194 | 195 | then: 196 | Object[] data = ctx.read('$.States.*') 197 | data.length == 9 198 | 199 | ctx.read('$.StartAt') == 'Nested000' 200 | 201 | with('Nested000') { 202 | v(it, 'Type') == 'Pass' 203 | v(it, 'Result') == 5 204 | v(it, 'ResultPath') == '$.a' 205 | v(it, 'Next') == 'Nested001' 206 | } 207 | 208 | with('Nested001') { 209 | v(it, 'Type') == 'Task' 210 | v(it, 'Parameters.cmd__sm') == 'Nested001' 211 | v(it, "Parameters.['a.\$']") == '$.a' 212 | v(it, 'ResultPath') == '$.b' 213 | v(it, 'Catch[0].ErrorEquals[0]') == 'e1' 214 | v(it, 'Catch[0].ErrorEquals[1]') == 'e2' 215 | v(it, 'Catch[0].ResultPath') == '$.errorInfo' 216 | v(it, 'Catch[0].Next') == 'Nested002' 217 | v(it, 'Catch[1].ErrorEquals[0]') == 'e0' 218 | v(it, 'Catch[1].Next') == 'Nested004' 219 | v(it, 'Next') == 'g1' 220 | } 221 | 222 | with('Nested002') { 223 | v(it, 'Type') == 'Pass' 224 | v(it, 'Result') == 3 225 | v(it, 'ResultPath') == '$.c' 226 | v(it, 'Next') == 'Nested003' 227 | } 228 | 229 | with('Nested003') { 230 | v(it, 'Type') == 'Pass' 231 | v(it, 'Next') == 'g1' 232 | } 233 | 234 | with('g1') { 235 | v(it, 'Type') == 'Pass' 236 | v(it, 'Result') == 10 237 | v(it, 'ResultPath') == '$.d' 238 | v(it, 'Next') == 'Nested006' 239 | } 240 | 241 | with('Nested004') { 242 | v(it, 'Type') == 'Pass' 243 | v(it, 'Result') == 10 244 | v(it, 'ResultPath') == '$.x' 245 | v(it, 'Next') == 'Nested005' 246 | } 247 | v('Nested005', 'Type') == 'Fail' 248 | 249 | with('Nested006') { 250 | v(it, 'Type') == 'Pass' 251 | v(it, 'Result') == 5 252 | v(it, 'ResultPath') == '$.y' 253 | v(it, 'Next') == 'Nested.Success' 254 | } 255 | 256 | v("['Nested.Success']", 'Type') == 'Succeed' 257 | } 258 | } -------------------------------------------------------------------------------- /src/test/groovy/com/eclecticlogic/stepper/asl/TestWhen.groovy: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.asl 2 | 3 | import com.jayway.jsonpath.ReadContext 4 | 5 | 6 | class TestWhen extends AbstractStateMachineTester { 7 | 8 | def "test simple when"() { 9 | given: 10 | TestOutput output = runWithLambda('when.stg', 'simple') 11 | ReadContext ctx = output.ctx 12 | 13 | when: 14 | Closure v = { name, param -> 15 | return ctx.read('$.States.' + name + '.' + param) 16 | } 17 | 18 | then: 19 | Object[] data = ctx.read('$.States.*') 20 | data.length == 11 21 | 22 | ctx.read('$.StartAt') == 'Simple000' 23 | 24 | with('Simple000') { 25 | v(it, 'Type') == 'Pass' 26 | v(it, 'Result') == 5 27 | v(it, 'ResultPath') == '$.a' 28 | v(it, 'Next') == 'Simple001' 29 | } 30 | 31 | with('Simple001') { 32 | v(it, 'Type') == 'Task' 33 | v(it, 'Parameters.cmd__sm') == 'Simple001' 34 | v(it, "Parameters.['a.\$']") == '$.a' 35 | v(it, 'ResultPath') == '$.Simplevar__000' 36 | v(it, 'Next') == 'Simple002' 37 | } 38 | output.lambda.contains('const response = a<5;') 39 | 40 | with('Simple002') { 41 | v(it, 'Type') == 'Choice' 42 | v(it, 'Choices[0].Variable') == '$.Simplevar__000' 43 | v(it, 'Choices[0].BooleanEquals') == true 44 | v(it, 'Choices[0].Next') == 'Simple003' 45 | 46 | v(it, 'Choices[1].Variable') == '$.Simplevar__000' 47 | v(it, 'Choices[1].BooleanEquals') == false 48 | v(it, 'Choices[1].Next') == 'abc' 49 | } 50 | 51 | with('Simple003') { 52 | v(it, 'Type') == 'Pass' 53 | v(it, 'Result') == 1 54 | v(it, 'ResultPath') == '$.b' 55 | v(it, 'Next') == 'Simple.Success' 56 | } 57 | 58 | with('abc') { 59 | v(it, 'Type') == 'Task' 60 | v(it, 'Parameters.cmd__sm') == 'abc' 61 | v(it, "Parameters.['a.\$']") == '$.a' 62 | v(it, 'ResultPath') == '$.Simplevar__001' 63 | v(it, 'Next') == 'Simple004' 64 | } 65 | output.lambda.contains('const response = a==5;') 66 | 67 | with('Simple004') { 68 | v(it, 'Type') == 'Choice' 69 | v(it, 'Choices[0].Variable') == '$.Simplevar__001' 70 | v(it, 'Choices[0].BooleanEquals') == true 71 | v(it, 'Choices[0].Next') == 'Simple005' 72 | 73 | v(it, 'Choices[1].Variable') == '$.Simplevar__001' 74 | v(it, 'Choices[1].BooleanEquals') == false 75 | v(it, 'Choices[1].Next') == 'Simple007' 76 | } 77 | with('Simple005') { 78 | v(it, 'Type') == 'Pass' 79 | v(it, 'Result') == 2 80 | v(it, 'ResultPath') == '$.b' 81 | v(it, 'Next') == 'Simple006' 82 | } 83 | with('Simple006') { 84 | v(it, 'Type') == 'Pass' 85 | v(it, 'Result') == 1 86 | v(it, 'ResultPath') == '$.c' 87 | v(it, 'Next') == 'Simple.Success' 88 | } 89 | with('Simple007') { 90 | v(it, 'Type') == 'Pass' 91 | v(it, 'Result') == 3 92 | v(it, 'ResultPath') == '$.b' 93 | v(it, 'Next') == 'Simple008' 94 | } 95 | with('Simple008') { 96 | v(it, 'Type') == 'Pass' 97 | v(it, 'Result') == 2 98 | v(it, 'ResultPath') == '$.c' 99 | v(it, 'Next') == 'Simple.Success' 100 | } 101 | 102 | v("['Simple.Success']", 'Type') == 'Succeed' 103 | } 104 | 105 | 106 | def "test nested when"() { 107 | given: 108 | TestOutput output = runWithLambda('when.stg', 'nested') 109 | ReadContext ctx = output.ctx 110 | 111 | when: 112 | Closure v = { name, param -> 113 | return ctx.read('$.States.' + name + '.' + param) 114 | } 115 | 116 | then: 117 | Object[] data = ctx.read('$.States.*') 118 | data.length == 15 119 | 120 | ctx.read('$.StartAt') == 'Nested000' 121 | 122 | with('Nested000') { 123 | v(it, 'Type') == 'Task' 124 | v(it, 'Parameters.cmd__sm') == 'Nested000' 125 | v(it, "Parameters.['a.\$']") == '$.a' 126 | v(it, 'ResultPath') == '$.Nestedvar__000' 127 | v(it, 'Next') == 'Nested001' 128 | } 129 | output.lambda.contains('const response = a<5;') 130 | 131 | with('Nested001') { 132 | v(it, 'Type') == 'Choice' 133 | v(it, 'Choices[0].Variable') == '$.Nestedvar__000' 134 | v(it, 'Choices[0].BooleanEquals') == true 135 | v(it, 'Choices[0].Next') == 'Nested002' 136 | 137 | v(it, 'Choices[1].Variable') == '$.Nestedvar__000' 138 | v(it, 'Choices[1].BooleanEquals') == false 139 | v(it, 'Choices[1].Next') == 'Nested008' 140 | } 141 | 142 | with('Nested002') { 143 | v(it, 'Type') == 'Pass' 144 | v(it, 'Result') == 1 145 | v(it, 'ResultPath') == '$.c' 146 | v(it, 'Next') == 'Nested003' 147 | } 148 | 149 | with('Nested003') { 150 | v(it, 'Type') == 'Task' 151 | v(it, 'Parameters.cmd__sm') == 'Nested003' 152 | v(it, "Parameters.['c.\$']") == '$.c' 153 | v(it, 'ResultPath') == '$.Nestedvar__001' 154 | v(it, 'Next') == 'Nested004' 155 | } 156 | output.lambda.contains('const response = c==2;') 157 | 158 | with('Nested004') { 159 | v(it, 'Type') == 'Choice' 160 | v(it, 'Choices[0].Variable') == '$.Nestedvar__001' 161 | v(it, 'Choices[0].BooleanEquals') == true 162 | v(it, 'Choices[0].Next') == 'Nested005' 163 | 164 | v(it, 'Choices[1].Variable') == '$.Nestedvar__001' 165 | v(it, 'Choices[1].BooleanEquals') == false 166 | v(it, 'Choices[1].Next') == 'x' 167 | } 168 | 169 | with('Nested005') { 170 | v(it, 'Type') == 'Pass' 171 | v(it, 'Result') == 1 172 | v(it, 'ResultPath') == '$.e' 173 | v(it, 'Next') == 'Nested.Success' 174 | } 175 | 176 | with('x') { 177 | v(it, 'Type') == 'Task' 178 | v(it, 'Parameters.cmd__sm') == 'x' 179 | v(it, "Parameters.['c.\$']") == '$.c' 180 | v(it, 'ResultPath') == '$.Nestedvar__002' 181 | v(it, 'Next') == 'Nested006' 182 | } 183 | output.lambda.contains('const response = c==3;') 184 | 185 | with('Nested006') { 186 | v(it, 'Type') == 'Choice' 187 | v(it, 'Choices[0].Variable') == '$.Nestedvar__002' 188 | v(it, 'Choices[0].BooleanEquals') == true 189 | v(it, 'Choices[0].Next') == 'Nested007' 190 | 191 | v(it, 'Choices[1].Variable') == '$.Nestedvar__002' 192 | v(it, 'Choices[1].BooleanEquals') == false 193 | v(it, 'Choices[1].Next') == 'Nested.Success' 194 | } 195 | 196 | with('Nested007') { 197 | v(it, 'Type') == 'Pass' 198 | v(it, 'Result') == 2 199 | v(it, 'ResultPath') == '$.e' 200 | v(it, 'Next') == 'Nested.Success' 201 | } 202 | 203 | with('Nested008') { 204 | v(it, 'Type') == 'Pass' 205 | v(it, 'Result') == 3 206 | v(it, 'ResultPath') == '$.b' 207 | v(it, 'Next') == 'a1' 208 | } 209 | 210 | with('a1') { 211 | v(it, 'Type') == 'Task' 212 | v(it, 'Parameters.cmd__sm') == 'a1' 213 | v(it, "Parameters.['b.\$']") == '$.b' 214 | v(it, 'ResultPath') == '$.Nestedvar__003' 215 | v(it, 'Next') == 'Nested009' 216 | } 217 | output.lambda.contains('const response = b%3==0;') 218 | 219 | with('Nested009') { 220 | v(it, 'Type') == 'Choice' 221 | v(it, 'Choices[0].Variable') == '$.Nestedvar__003' 222 | v(it, 'Choices[0].BooleanEquals') == true 223 | v(it, 'Choices[0].Next') == 'Nested010' 224 | 225 | v(it, 'Choices[1].Variable') == '$.Nestedvar__003' 226 | v(it, 'Choices[1].BooleanEquals') == false 227 | v(it, 'Choices[1].Next') == 'a2' 228 | } 229 | 230 | with('Nested010') { 231 | v(it, 'Type') == 'Pass' 232 | v(it, 'Result') == 1 233 | v(it, 'ResultPath') == '$.d' 234 | v(it, 'Next') == 'Nested.Success' 235 | } 236 | 237 | with('a2') { 238 | v(it, 'Type') == 'Pass' 239 | v(it, 'Result') == 2 240 | v(it, 'ResultPath') == '$.d' 241 | v(it, 'Next') == 'Nested.Success' 242 | } 243 | 244 | v("['Nested.Success']", 'Type') == 'Succeed' 245 | } 246 | 247 | 248 | } -------------------------------------------------------------------------------- /src/test/groovy/com/eclecticlogic/stepper/asl/TestBasic.groovy: -------------------------------------------------------------------------------- 1 | package com.eclecticlogic.stepper.asl 2 | 3 | import com.jayway.jsonpath.ReadContext 4 | 5 | /** 6 | * General structure and assignment tests. 7 | */ 8 | class TestBasic extends AbstractStateMachineTester { 9 | 10 | def "test simple"() { 11 | given: 12 | ReadContext ctx = run('basic.stg', 'simple') 13 | 14 | expect: 15 | with(ctx) { 16 | read('$.StartAt') == 'xyz' 17 | read('$.States').size() == 2 18 | 19 | read('$..xyz.Type')[0] == 'Pass' 20 | read('$..xyz.Result')[0] == 'Hello World' 21 | read('$..xyz.ResultPath')[0] == '$.a' 22 | read('$..xyz.Next')[0] == 'Simple.Success' 23 | 24 | read("\$..['Simple.Success'].Type")[0] == 'Succeed' 25 | } 26 | } 27 | 28 | 29 | def "test annotation and state name"() { 30 | given: 31 | ReadContext ctx = run('basic.stg', 'annotationName') 32 | 33 | expect: 34 | with(ctx) { 35 | read('$.Comment') == 'this is a comment' 36 | read('$.TimeoutSeconds') == 3600 37 | read('$.Version') == '1.0' 38 | 39 | read('$.StartAt') == 'AnnotationTest000' 40 | read('$..AnnotationTest000.Type')[0] == 'Pass' 41 | read('$..AnnotationTest000.Result')[0] == 5 42 | read('$..AnnotationTest000.ResultPath')[0] == '$.c' 43 | read('$..AnnotationTest000.Next')[0] == 'AnnotationTest.Success' 44 | 45 | read("\$..['AnnotationTest.Success'].Type")[0] == 'Succeed' 46 | } 47 | } 48 | 49 | 50 | def "test primitive assignment"() { 51 | given: 52 | ReadContext ctx = run('basic.stg', 'assignmentPrimitive') 53 | 54 | when: 55 | Closure v = { key, value, resultVar -> 56 | verifyAll(ctx) { 57 | read('$..' + key + '.Type')[0] == 'Pass' 58 | read('$..' + key + '.Result')[0] == value 59 | read('$..' + key + '.ResultPath')[0] == '$.' + resultVar 60 | } 61 | return true 62 | } 63 | 64 | then: 65 | v('abc', 5.2, 'a') 66 | v('assignment000', 10, 'b') 67 | v('assignment001', 'Hello World', 'c') 68 | v('assignment002', true, 'd.e.f') 69 | v('xyz', false, 'e.f.g') 70 | } 71 | 72 | 73 | def "test complex assignment"() { 74 | given: 75 | TestOutput output = runWithLambda('basic.stg', 'assignmentComplex') 76 | ReadContext ctx = output.ctx 77 | def name1 = 'complex000' 78 | def name2 = 'one' 79 | def name3 = 'complex001' 80 | def name4 = 'complex002' 81 | def name5 = 'two' 82 | 83 | when: 84 | Closure v = { stateName, key, value -> 85 | verifyAll(ctx) { 86 | read('$.States.' + stateName + '.' + key) == value 87 | } 88 | return true 89 | } 90 | 91 | then: 92 | Object[] data = ctx.read('$.States.*') 93 | data.length == 6 94 | 95 | v(name1, 'Type', 'Task') 96 | v(name1, 'ResultPath', '$.value1') 97 | v(name1, 'Parameters.cmd__sm', name1) 98 | v(name1, "Parameters.['a.\$']", '$.a') 99 | v(name1, "Parameters.['b.\$']", '$.b') 100 | v(name1, "Parameters.['c.\$']", '$.c') 101 | v(name1, "Parameters.['d.\$']", '$.d') 102 | v(name1, "Parameters.['e.\$']", '$.e') 103 | v(name1, 'Next', name2) 104 | output.lambda.contains('const response = a*b+c.calc(e)-d.length();') 105 | 106 | v(name2, 'Type', 'Task') 107 | v(name2, 'ResultPath', '$.value2') 108 | v(name2, 'Parameters.cmd__sm', name2) 109 | v(name2, "Parameters.['a.\$']", '$.a') 110 | v(name2, "Parameters.['b.\$']", '$.b') 111 | v(name2, "Parameters.['c.\$']", '$.c') 112 | v(name2, "Parameters.['d.\$']", '$.d') 113 | v(name2, "Parameters.['e.\$']", '$.e') 114 | v(name2, 'Next', name3) 115 | output.lambda.contains('const response = value2 + (a*b+c.calc(e)-d.length());') 116 | 117 | v(name3, 'Type', 'Task') 118 | v(name3, 'ResultPath', '$.value3') 119 | v(name3, 'Parameters.cmd__sm', name3) 120 | v(name3, "Parameters.['a.\$']", '$.a') 121 | v(name3, "Parameters.['b.\$']", '$.b') 122 | v(name3, "Parameters.['c.\$']", '$.c') 123 | v(name3, "Parameters.['d.\$']", '$.d') 124 | v(name3, "Parameters.['e.\$']", '$.e') 125 | v(name3, 'Next', name4) 126 | output.lambda.contains('const response = value3 - (a*b+c.calc(e)-d.length());') 127 | 128 | v(name4, 'Type', 'Task') 129 | v(name4, 'ResultPath', '$.value4') 130 | v(name4, 'Parameters.cmd__sm', name4) 131 | v(name4, "Parameters.['a.\$']", '$.a') 132 | v(name4, "Parameters.['b.\$']", '$.b') 133 | v(name4, "Parameters.['c.\$']", '$.c') 134 | v(name4, "Parameters.['d.\$']", '$.d') 135 | v(name4, "Parameters.['e.\$']", '$.e') 136 | v(name4, 'Next', name5) 137 | output.lambda.contains('const response = value4 / (a*b+c.calc(e)-d.length());') 138 | 139 | v(name5, 'Type', 'Task') 140 | v(name5, 'ResultPath', '$.value5') 141 | v(name5, 'Parameters.cmd__sm', name5) 142 | v(name5, "Parameters.['a.\$']", '$.a') 143 | v(name5, "Parameters.['b.\$']", '$.b') 144 | v(name5, "Parameters.['c.\$']", '$.c') 145 | v(name5, "Parameters.['d.\$']", '$.d') 146 | v(name5, "Parameters.['e.\$']", '$.e') 147 | v(name5, 'Next', 'complex.Success') 148 | output.lambda.contains('const response = value5 * (a*b+c.calc(e)-d.length());') 149 | 150 | v("['complex.Success']", 'Type', 'Succeed') 151 | } 152 | 153 | 154 | def "test assignment json"() { 155 | given: 156 | TestOutput output = runWithLambda('basic.stg', 'assignmentJson') 157 | ReadContext ctx = output.ctx 158 | def name = 'xyz' 159 | 160 | when: 161 | Closure v = { stateName, key, value -> 162 | verifyAll(ctx) { 163 | read('$.States.' + stateName + '.' + key) == value 164 | } 165 | return true 166 | } 167 | 168 | then: 169 | Object[] data = ctx.read('$.States.*') 170 | data.length == 2 171 | 172 | v(name, 'Type', 'Pass') 173 | v(name, 'Parameters.a', 5) 174 | v(name, 'Parameters.b', 10) 175 | v(name, 'Parameters.c.d', 'x') 176 | v(name, 'Parameters.e[0]', 'p') 177 | v(name, 'Parameters.e[1]', true) 178 | v(name, 'Parameters.e[2]', false) 179 | v(name, 'ResultPath', '$.value') 180 | v(name, 'Next', 'json.Success') 181 | 182 | v("['json.Success']", 'Type', 'Succeed') 183 | } 184 | 185 | 186 | def "test assignment json array"() { 187 | given: 188 | TestOutput output = runWithLambda('basic.stg', 'assignmentJsonArray') 189 | ReadContext ctx = output.ctx 190 | def name = 'xyz' 191 | 192 | when: 193 | Closure v = { stateName, key, value -> 194 | verifyAll(ctx) { 195 | read('$.States.' + stateName + '.' + key) == value 196 | } 197 | return true 198 | } 199 | 200 | then: 201 | Object[] data = ctx.read('$.States.*') 202 | data.length == 2 203 | 204 | v(name, 'Type', 'Pass') 205 | v(name, 'Result[0]', 'p') 206 | v(name, 'Result[1]', true) 207 | v(name, 'Result[2]', false) 208 | v(name, 'Result[3]', 2.5) 209 | v(name, 'Result[4]', 10) 210 | v(name, 'ResultPath', '$.value') 211 | v(name, 'Next', 'array.Success') 212 | 213 | v("['array.Success']", 'Type', 'Succeed') 214 | } 215 | 216 | 217 | def "test assignment task"() { 218 | given: 219 | TestOutput output = runWithLambda('basic.stg', 'assignmentTask') 220 | ReadContext ctx = output.ctx 221 | 222 | expect: 223 | Object[] data = ctx.read('$.States.*') 224 | data.length == 2 225 | 226 | ctx.read('$..x.Type')[0] == 'Task' 227 | ctx.read('$..x.ResultPath')[0] == '$.value' 228 | ctx.read('$..x.a')[0] == 'b' 229 | } 230 | 231 | 232 | def "test task"() { 233 | given: 234 | TestOutput output = runWithLambda('basic.stg', 'task') 235 | ReadContext ctx = output.ctx 236 | 237 | expect: 238 | Object[] data = ctx.read('$.States.*') 239 | data.length == 2 240 | 241 | ctx.read('$..hello.Type')[0] == 'Task' 242 | ctx.read('$..hello.a')[0] == 'b' 243 | ctx.read('$..hello.ResultPath')[0] == '$.hello' 244 | ctx.read('$.States.hello.Retry[0].BackoffRate') == 5 245 | ctx.read('$.States.hello.Retry[0].IntervalSeconds') == 3 246 | ctx.read('$.States.hello.Retry[0].ErrorEquals')[0] == 'abc' 247 | ctx.read('$.States.hello.Retry[0].ErrorEquals')[1] == 'def' 248 | ctx.read('$.States.hello.Retry[0].MaxAttempts') == 4 249 | ctx.read('$.States.hello.Retry[1].IntervalSeconds') == 6 250 | ctx.read('$.States.hello.Retry[1].ErrorEquals')[0] == 'pqr' 251 | } 252 | 253 | 254 | def "test wait"() { 255 | given: 256 | TestOutput output = runWithLambda('basic.stg', 'wait') 257 | ReadContext ctx = output.ctx 258 | 259 | expect: 260 | Object[] data = ctx.read('$.States.*') 261 | data.length == 2 262 | 263 | ctx.read('$..abc.Type')[0] == 'Wait' 264 | ctx.read('$..abc.Seconds')[0] == 10 265 | ctx.read('$..abc.Next')[0] == 'waiter.Success' 266 | ctx.read("\$..['waiter.Success'].Type")[0] == 'Succeed' 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | --------------------------------------------------------------------------------