├── .gitignore ├── requirementsascode_logo.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .travis.yml ├── requirementsascodecore ├── src │ ├── test │ │ └── java │ │ │ └── org │ │ │ └── requirementsascode │ │ │ ├── testbehavior │ │ │ ├── TestAddTaskResponse.java │ │ │ ├── TestAddTaskRequest.java │ │ │ ├── TestCreateListRequest.java │ │ │ ├── TestCreateListResponse.java │ │ │ ├── TestResponse.java │ │ │ ├── EmptyBehavior.java │ │ │ ├── TestExceptionThrower.java │ │ │ ├── TestBehaviorModel.java │ │ │ └── TestCompleteTaskRequest.java │ │ │ ├── RunStopAndRestartTest.java │ │ │ ├── RecordingTest.java │ │ │ ├── FlowWithCaseStepTest.java │ │ │ ├── ExceptionHandlingTest.java │ │ │ ├── BehaviorTest.java │ │ │ ├── CanReactToTest.java │ │ │ ├── IncludesTest.java │ │ │ └── ReactToTypesTest.java │ └── main │ │ └── java │ │ └── org │ │ └── requirementsascode │ │ ├── package-info.java │ │ ├── systemreaction │ │ ├── IgnoresIt.java │ │ ├── AbstractContinues.java │ │ ├── ContinuesAfter.java │ │ ├── AbstractContinuesAfter.java │ │ └── ContinuesAt.java │ │ ├── exception │ │ ├── package-info.java │ │ ├── NestedCallOfReactTo.java │ │ ├── InfiniteRepetition.java │ │ ├── NoSuchElementInModel.java │ │ ├── ElementAlreadyInModel.java │ │ ├── MissingUseCaseStepPart.java │ │ └── MoreThanOneStepCanReact.java │ │ ├── Condition.java │ │ ├── BehaviorException.java │ │ ├── flowposition │ │ ├── Anytime.java │ │ ├── InsteadOf.java │ │ ├── FlowPosition.java │ │ ├── AfterSingleStep.java │ │ └── After.java │ │ ├── UserActor.java │ │ ├── SystemActor.java │ │ ├── FlowlessStep.java │ │ ├── Actor.java │ │ ├── BehaviorModel.java │ │ ├── ModelElement.java │ │ ├── Behavior.java │ │ ├── InterruptingFlowStep.java │ │ ├── builder │ │ ├── StepConditionPart.java │ │ ├── StepUseCasePart.java │ │ ├── FlowConditionPart.java │ │ ├── FlowPositionPart.java │ │ ├── FlowlessUseCasePart.java │ │ ├── StepToPart.java │ │ ├── FlowlessToPart.java │ │ ├── FlowlessUserPart.java │ │ └── FlowlessStepPart.java │ │ ├── FlowStep.java │ │ ├── SystemReaction.java │ │ ├── ModelElementContainer.java │ │ ├── StatelessBehavior.java │ │ ├── InterruptableFlowStep.java │ │ ├── RecordingActor.java │ │ ├── queue │ │ └── EventQueue.java │ │ ├── Flow.java │ │ ├── Step.java │ │ └── StepToBeRun.java └── build.gradle ├── requirementsascodeexamples ├── pizzavolumecalculator │ ├── src │ │ ├── main │ │ │ └── java │ │ │ │ └── pizzavolumecalculator │ │ │ │ ├── actor │ │ │ │ ├── command │ │ │ │ │ ├── CalculateVolume.java │ │ │ │ │ ├── EnterHeight.java │ │ │ │ │ └── EnterRadius.java │ │ │ │ └── PizzaVolumeCalculator.java │ │ │ │ └── console │ │ │ │ └── PizzaVolumeCalculatorConsole.java │ │ └── test │ │ │ └── java │ │ │ └── pizzavolumecalculator │ │ │ └── actor │ │ │ └── PizzaVolumeCalculatorTest.java │ └── build.gradle ├── creditcard_eventsourcing │ ├── src │ │ └── main │ │ │ └── java │ │ │ └── creditcard_eventsourcing │ │ │ ├── model │ │ │ ├── command │ │ │ │ ├── RequestToCloseCycle.java │ │ │ │ ├── RequestRepay.java │ │ │ │ ├── RequestWithdrawal.java │ │ │ │ └── RequestToAssignLimit.java │ │ │ ├── event │ │ │ │ ├── DomainEvent.java │ │ │ │ ├── CycleClosed.java │ │ │ │ ├── CardRepaid.java │ │ │ │ ├── CardWithdrawn.java │ │ │ │ └── LimitAssigned.java │ │ │ └── CreditCard.java │ │ │ ├── persistence │ │ │ └── EventStore.java │ │ │ ├── controller │ │ │ └── CreditCardController.java │ │ │ └── EventsourcingApplication.java │ └── build.gradle ├── helloworld │ ├── src │ │ ├── main │ │ │ └── java │ │ │ │ ├── helloworld │ │ │ │ ├── infrastructure │ │ │ │ │ └── OutputAdapter.java │ │ │ │ ├── command │ │ │ │ │ └── EnterText.java │ │ │ │ ├── domain │ │ │ │ │ ├── Greeting.java │ │ │ │ │ ├── Person.java │ │ │ │ │ └── Color.java │ │ │ │ ├── commandhandler │ │ │ │ │ ├── SaveName.java │ │ │ │ │ ├── SaveAge.java │ │ │ │ │ ├── GreetWithDefaultName.java │ │ │ │ │ ├── GreetPersonWithAge.java │ │ │ │ │ ├── GreetPersonWithName.java │ │ │ │ │ └── GreetWithEnteredName.java │ │ │ │ ├── actor │ │ │ │ │ ├── ValidUser.java │ │ │ │ │ ├── AnonymousUser.java │ │ │ │ │ └── NormalUser.java │ │ │ │ ├── HelloWorld01.java │ │ │ │ ├── HelloWorld03.java │ │ │ │ ├── HelloWorld02.java │ │ │ │ ├── HelloUser.java │ │ │ │ ├── HelloWorld03a.java │ │ │ │ ├── HelloWorld04.java │ │ │ │ ├── HelloWorld07.java │ │ │ │ ├── HelloWorld05.java │ │ │ │ └── HelloWorld06.java │ │ │ │ └── cleanarchitecture │ │ │ │ └── CleanArchitectureOutline.java │ │ └── test │ │ │ └── java │ │ │ ├── helloworld │ │ │ └── actor │ │ │ │ └── InvalidUser.java │ │ │ └── cleanarchitecture │ │ │ └── CleanArchitectureOutlineTest.java │ └── build.gradle ├── actor │ ├── build.gradle │ └── src │ │ └── main │ │ └── java │ │ ├── message │ │ ├── EnterName.java │ │ └── NameEntered.java │ │ └── actor │ │ ├── PublishingActorExample.java │ │ └── InteractingActorsExample.java ├── akka │ ├── build.gradle │ └── src │ │ └── main │ │ ├── resources │ │ └── application.conf │ │ └── java │ │ └── akka │ │ └── Akka.java └── crosscuttingconcerns │ ├── build.gradle │ ├── README.md │ └── src │ └── main │ └── java │ └── crosscuttingconcerns │ └── CrossCuttingConcerns01.java ├── requirementsascodeextract ├── src │ ├── test │ │ ├── java │ │ │ └── org │ │ │ │ └── requirementsascode │ │ │ │ └── extract │ │ │ │ └── freemarker │ │ │ │ ├── usercommand │ │ │ │ ├── DecidesToQuit.java │ │ │ │ └── EntersName.java │ │ │ │ ├── systemreaction │ │ │ │ ├── Quits.java │ │ │ │ ├── BlowsUp.java │ │ │ │ ├── PromptsUserToEnterName.java │ │ │ │ ├── LogsException.java │ │ │ │ ├── GreetsUser.java │ │ │ │ └── NameEntered.java │ │ │ │ ├── predicate │ │ │ │ ├── ThereIsNoAlternative.java │ │ │ │ └── SomeConditionIsFulfilled.java │ │ │ │ └── methodmodel │ │ │ │ └── WordsTest.java │ │ └── resources │ │ │ └── org │ │ │ └── requirementsascode │ │ │ └── extract │ │ │ └── freemarker │ │ │ ├── testextract_flowless.ftl │ │ │ └── testextract.ftl │ └── main │ │ └── java │ │ └── org │ │ └── requirementsascode │ │ └── extract │ │ └── freemarker │ │ ├── methodmodel │ │ ├── util │ │ │ ├── Words.java │ │ │ └── Steps.java │ │ ├── ActorPartOfStep.java │ │ ├── ReactWhileOfStep.java │ │ ├── InCasePartOfStep.java │ │ ├── FlowlessCondition.java │ │ ├── UserPartOfStep.java │ │ ├── FlowCondition.java │ │ └── SystemPartOfStep.java │ │ └── FreeMarkerEngine.java └── build.gradle ├── settings.gradle ├── .gitattributes ├── CONTRIBUTING.md ├── gradlew.bat └── CODE_OF_CONDUCT.md /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .project 3 | .settings 4 | .classpath 5 | build 6 | bin 7 | -------------------------------------------------------------------------------- /requirementsascode_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertilmuth/requirementsascode/HEAD/requirementsascode_logo.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertilmuth/requirementsascode/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk8 4 | - openjdk14 5 | - oraclejdk14 6 | before_install: 7 | - chmod +x gradlew 8 | -------------------------------------------------------------------------------- /requirementsascodecore/src/test/java/org/requirementsascode/testbehavior/TestAddTaskResponse.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.testbehavior; 2 | 3 | public class TestAddTaskResponse { 4 | } 5 | -------------------------------------------------------------------------------- /requirementsascodecore/src/test/java/org/requirementsascode/testbehavior/TestAddTaskRequest.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.testbehavior; 2 | 3 | public class TestAddTaskRequest { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /requirementsascodecore/src/test/java/org/requirementsascode/testbehavior/TestCreateListRequest.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.testbehavior; 2 | 3 | public class TestCreateListRequest { 4 | } 5 | -------------------------------------------------------------------------------- /requirementsascodecore/src/test/java/org/requirementsascode/testbehavior/TestCreateListResponse.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.testbehavior; 2 | 3 | public class TestCreateListResponse { 4 | } 5 | -------------------------------------------------------------------------------- /requirementsascodecore/build.gradle: -------------------------------------------------------------------------------- 1 | jar { 2 | manifest { 3 | attributes 'Implementation-Title': 'requirements as code - core', 4 | 'Implementation-Version':archiveVersion 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /requirementsascodeexamples/pizzavolumecalculator/src/main/java/pizzavolumecalculator/actor/command/CalculateVolume.java: -------------------------------------------------------------------------------- 1 | package pizzavolumecalculator.actor.command; 2 | 3 | public class CalculateVolume { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /requirementsascodeextract/src/test/java/org/requirementsascode/extract/freemarker/usercommand/DecidesToQuit.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.extract.freemarker.usercommand; 2 | 3 | public class DecidesToQuit { 4 | } 5 | -------------------------------------------------------------------------------- /requirementsascodeexamples/creditcard_eventsourcing/src/main/java/creditcard_eventsourcing/model/command/RequestToCloseCycle.java: -------------------------------------------------------------------------------- 1 | package creditcard_eventsourcing.model.command; 2 | 3 | public class RequestToCloseCycle { 4 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/main/java/helloworld/infrastructure/OutputAdapter.java: -------------------------------------------------------------------------------- 1 | package helloworld.infrastructure; 2 | 3 | public class OutputAdapter { 4 | public void showMessage(String message) { 5 | System.out.println(message); 6 | } 7 | } -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Core package of requirementsascode, containing everything you need to create 3 | * a model and run it. 4 | * 5 | * @author b_muth 6 | */ 7 | package org.requirementsascode; 8 | -------------------------------------------------------------------------------- /requirementsascodeextract/src/test/java/org/requirementsascode/extract/freemarker/systemreaction/Quits.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.extract.freemarker.systemreaction; 2 | 3 | public class Quits implements Runnable { 4 | @Override 5 | public void run() { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /requirementsascodeextract/src/test/java/org/requirementsascode/extract/freemarker/systemreaction/BlowsUp.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.extract.freemarker.systemreaction; 2 | 3 | public class BlowsUp implements Runnable { 4 | @Override 5 | public void run() { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/systemreaction/IgnoresIt.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.systemreaction; 2 | 3 | import java.util.function.Consumer; 4 | 5 | public class IgnoresIt implements Consumer{ 6 | @Override 7 | public void accept(T command) { 8 | } 9 | } -------------------------------------------------------------------------------- /requirementsascodeexamples/actor/build.gradle: -------------------------------------------------------------------------------- 1 | jar { 2 | manifest { 3 | attributes 'Implementation-Title': 'requirements as code - actor example', 'Implementation-Version':archiveVersion 4 | } 5 | } 6 | 7 | dependencies { 8 | implementation project(':requirementsascodecore') 9 | } 10 | 11 | -------------------------------------------------------------------------------- /requirementsascodeextract/src/test/java/org/requirementsascode/extract/freemarker/systemreaction/PromptsUserToEnterName.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.extract.freemarker.systemreaction; 2 | 3 | public class PromptsUserToEnterName implements Runnable { 4 | @Override 5 | public void run() { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /requirementsascodeexamples/akka/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | } 4 | 5 | dependencies { 6 | implementation project(':requirementsascodecore') 7 | implementation 'com.typesafe.akka:akka-actor_2.11:2.5.4' 8 | implementation 'com.typesafe.akka:akka-remote_2.11:2.5.4' 9 | } 10 | 11 | -------------------------------------------------------------------------------- /requirementsascodeexamples/actor/src/main/java/message/EnterName.java: -------------------------------------------------------------------------------- 1 | package message; 2 | 3 | public class EnterName { 4 | private String userName; 5 | 6 | public EnterName(String userName) { 7 | this.userName = userName; 8 | } 9 | 10 | public String getUserName() { 11 | return userName; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/exception/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Exception package of requirementsascode, containing all the exceptions that 3 | * are thrown by requirementsascode during model creation and running. 4 | * 5 | * @author b_muth 6 | */ 7 | package org.requirementsascode.exception; 8 | -------------------------------------------------------------------------------- /requirementsascodeexamples/actor/src/main/java/message/NameEntered.java: -------------------------------------------------------------------------------- 1 | package message; 2 | 3 | public class NameEntered { 4 | private String userName; 5 | 6 | public NameEntered(String userName) { 7 | this.userName = userName; 8 | } 9 | 10 | public String getUserName() { 11 | return userName; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/main/java/helloworld/command/EnterText.java: -------------------------------------------------------------------------------- 1 | package helloworld.command; 2 | 3 | public class EnterText { 4 | private final String text; 5 | 6 | public EnterText(String text) { 7 | this.text = text; 8 | } 9 | 10 | public String getText() { 11 | return text; 12 | } 13 | } -------------------------------------------------------------------------------- /requirementsascodeexamples/crosscuttingconcerns/build.gradle: -------------------------------------------------------------------------------- 1 | jar { 2 | manifest { 3 | attributes 'Implementation-Title': 'requirements as code - cross cutting concerns', 4 | 'Implementation-Version':archiveVersion 5 | } 6 | } 7 | 8 | dependencies { 9 | implementation project(':requirementsascodecore') 10 | } 11 | 12 | -------------------------------------------------------------------------------- /requirementsascodeexamples/pizzavolumecalculator/build.gradle: -------------------------------------------------------------------------------- 1 | jar { 2 | manifest { 3 | attributes 'Implementation-Title': 'requirements as code - pizza volume calculator', 4 | 'Implementation-Version':archiveVersion 5 | } 6 | } 7 | 8 | dependencies { 9 | implementation project(':requirementsascodecore') 10 | } 11 | 12 | -------------------------------------------------------------------------------- /requirementsascodecore/src/test/java/org/requirementsascode/testbehavior/TestResponse.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.testbehavior; 2 | 3 | public class TestResponse { 4 | private final String taskName; 5 | 6 | TestResponse(String taskName) { 7 | this.taskName = taskName; 8 | } 9 | 10 | public String getTaskName() { 11 | return taskName; 12 | } 13 | } -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/main/java/helloworld/domain/Greeting.java: -------------------------------------------------------------------------------- 1 | package helloworld.domain; 2 | 3 | public class Greeting { 4 | public static String forUserWithName(String userName) { 5 | return "Hello, " + userName + "."; 6 | } 7 | 8 | public static String forUserWithAge(int age) { 9 | return "You are " + age + " years old."; 10 | } 11 | } -------------------------------------------------------------------------------- /requirementsascodeextract/src/test/resources/org/requirementsascode/extract/freemarker/testextract_flowless.ftl: -------------------------------------------------------------------------------- 1 | <@compress single_line=true> 2 | <#list model.useCases as useCase> 3 | Use case: ${useCase}. 4 | <#list useCase.steps as s> 5 | Step: ${s}. ${flowlessCondition(s)}${actorPartOfStep(s)}${userPartOfStep(s)}${systemPartOfStep(s)} 6 | 7 | 8 | -------------------------------------------------------------------------------- /requirementsascodecore/src/test/java/org/requirementsascode/testbehavior/EmptyBehavior.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.testbehavior; 2 | 3 | import org.requirementsascode.BehaviorModel; 4 | import org.requirementsascode.Model; 5 | 6 | public class EmptyBehavior implements BehaviorModel { 7 | @Override 8 | public Model model() { 9 | return Model.builder().build(); 10 | } 11 | } -------------------------------------------------------------------------------- /requirementsascodeexamples/pizzavolumecalculator/src/main/java/pizzavolumecalculator/actor/command/EnterHeight.java: -------------------------------------------------------------------------------- 1 | package pizzavolumecalculator.actor.command; 2 | 3 | public class EnterHeight { 4 | private int height; 5 | 6 | public EnterHeight(int height) { 7 | this.height = height; 8 | } 9 | 10 | public int getHeight() { 11 | return height; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /requirementsascodeexamples/pizzavolumecalculator/src/main/java/pizzavolumecalculator/actor/command/EnterRadius.java: -------------------------------------------------------------------------------- 1 | package pizzavolumecalculator.actor.command; 2 | 3 | public class EnterRadius { 4 | private int radius; 5 | 6 | public EnterRadius(int radius) { 7 | this.radius = radius; 8 | } 9 | 10 | public int getRadius() { 11 | return radius; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /requirementsascodeextract/src/test/java/org/requirementsascode/extract/freemarker/predicate/ThereIsNoAlternative.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.extract.freemarker.predicate; 2 | 3 | import org.requirementsascode.Condition; 4 | 5 | public class ThereIsNoAlternative implements Condition { 6 | @Override 7 | public boolean evaluate() { 8 | return true; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/build.gradle: -------------------------------------------------------------------------------- 1 | jar { 2 | manifest { 3 | attributes 'Implementation-Title': 'requirements as code - hello world', 4 | 'Implementation-Version':archiveVersion 5 | } 6 | } 7 | 8 | dependencies { 9 | implementation project(':requirementsascodecore') 10 | testImplementation 'org.mockito:mockito-core:3.+' 11 | } 12 | 13 | -------------------------------------------------------------------------------- /requirementsascodeextract/src/test/java/org/requirementsascode/extract/freemarker/predicate/SomeConditionIsFulfilled.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.extract.freemarker.predicate; 2 | 3 | import org.requirementsascode.Condition; 4 | 5 | public class SomeConditionIsFulfilled implements Condition { 6 | @Override 7 | public boolean evaluate() { 8 | return true; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /requirementsascodeextract/src/test/java/org/requirementsascode/extract/freemarker/usercommand/EntersName.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.extract.freemarker.usercommand; 2 | 3 | public class EntersName { 4 | 5 | public final String name; 6 | 7 | public EntersName(String name) { 8 | this.name = name; 9 | } 10 | 11 | public String getName() { 12 | return name; 13 | } 14 | } -------------------------------------------------------------------------------- /requirementsascodeexamples/creditcard_eventsourcing/src/main/java/creditcard_eventsourcing/model/event/DomainEvent.java: -------------------------------------------------------------------------------- 1 | package creditcard_eventsourcing.model.event; 2 | 3 | /** 4 | * Based on code by Jakub Pilimon: 5 | * https://gitlab.com/pilloPl/eventsourced-credit-cards/blob/4329a0aac283067f1376b3802e13f5a561f18753 6 | * 7 | */ 8 | public interface DomainEvent { 9 | String getType(); 10 | } 11 | -------------------------------------------------------------------------------- /requirementsascodeextract/src/test/java/org/requirementsascode/extract/freemarker/systemreaction/LogsException.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.extract.freemarker.systemreaction; 2 | 3 | import java.util.function.Consumer; 4 | 5 | public class LogsException implements Consumer { 6 | 7 | @Override 8 | public void accept(Exception t) { 9 | t.printStackTrace(); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include 'requirementsascodecore' 2 | include 'requirementsascodeextract' 3 | include 'requirementsascodeexamples:helloworld' 4 | include 'requirementsascodeexamples:crosscuttingconcerns' 5 | include 'requirementsascodeexamples:actor' 6 | include 'requirementsascodeexamples:pizzavolumecalculator' 7 | include 'requirementsascodeexamples:akka' 8 | include 'requirementsascodeexamples:creditcard_eventsourcing' 9 | 10 | -------------------------------------------------------------------------------- /requirementsascodeexamples/creditcard_eventsourcing/src/main/java/creditcard_eventsourcing/model/command/RequestRepay.java: -------------------------------------------------------------------------------- 1 | package creditcard_eventsourcing.model.command; 2 | 3 | import java.math.BigDecimal; 4 | 5 | public class RequestRepay { 6 | private BigDecimal amount; 7 | 8 | public RequestRepay(BigDecimal amount) { 9 | this.amount = amount; 10 | } 11 | 12 | public BigDecimal getAmount() { 13 | return amount; 14 | } 15 | } -------------------------------------------------------------------------------- /requirementsascodeexamples/creditcard_eventsourcing/src/main/java/creditcard_eventsourcing/model/command/RequestWithdrawal.java: -------------------------------------------------------------------------------- 1 | package creditcard_eventsourcing.model.command; 2 | 3 | import java.math.BigDecimal; 4 | 5 | public class RequestWithdrawal { 6 | private BigDecimal amount; 7 | 8 | public RequestWithdrawal(BigDecimal amount) { 9 | this.amount = amount; 10 | } 11 | 12 | public BigDecimal getAmount() { 13 | return amount; 14 | } 15 | } -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/Condition.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | /** 4 | * A condition, as part of a model. A condition is used to evaluate when a model 5 | * runner triggers a system reaction, given that a message of a type defined in 6 | * the model is received. 7 | * 8 | * @author b_muth 9 | * 10 | */ 11 | @FunctionalInterface 12 | public interface Condition { 13 | boolean evaluate(); 14 | } 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /requirementsascodeexamples/creditcard_eventsourcing/src/main/java/creditcard_eventsourcing/model/command/RequestToAssignLimit.java: -------------------------------------------------------------------------------- 1 | package creditcard_eventsourcing.model.command; 2 | 3 | import java.math.BigDecimal; 4 | 5 | public class RequestToAssignLimit { 6 | private BigDecimal amount; 7 | 8 | public RequestToAssignLimit(BigDecimal amount) { 9 | this.amount = amount; 10 | } 11 | 12 | public BigDecimal getAmount() { 13 | return amount; 14 | } 15 | } -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/BehaviorException.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | /** 4 | * Exception that is thrown by a behavior when it encounters an internal 5 | * problem. 6 | * 7 | * @author b_muth 8 | * 9 | */ 10 | @SuppressWarnings("serial") 11 | public class BehaviorException extends RuntimeException { 12 | BehaviorException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /requirementsascodeexamples/akka/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | actor { 3 | provider = "akka.remote.RemoteActorRefProvider" 4 | serialize-creators=on 5 | 6 | /sayHelloActor { 7 | remote = "akka.tcp://modelBasedActorSystem@127.0.0.1:2552" 8 | } 9 | } 10 | remote { 11 | enabled-transports = ["akka.remote.netty.tcp"] 12 | netty.tcp { 13 | hostname = "127.0.0.1" 14 | port = 2552 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /requirementsascodeextract/src/test/resources/org/requirementsascode/extract/freemarker/testextract.ftl: -------------------------------------------------------------------------------- 1 | <@compress single_line=true> 2 | <#list model.useCases as useCase> 3 | Use case: ${useCase}. 4 | <#list useCase.flows as f> 5 | Flow: ${f} ${flowCondition(f)} 6 | <#list f.steps as s> 7 | Step: ${s}. ${reactWhileOfStep(s)}${actorPartOfStep(s)}${userPartOfStep(s)}${inCasePartOfStep(s)}${systemPartOfStep(s)} 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/flowposition/Anytime.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.flowposition; 2 | 3 | import org.requirementsascode.ModelRunner; 4 | 5 | public class Anytime extends FlowPosition{ 6 | public Anytime() { 7 | super(null); 8 | } 9 | 10 | @Override 11 | protected boolean isRunnerAtRightPositionFor(ModelRunner modelRunner) { 12 | return true; 13 | } 14 | 15 | @Override 16 | public void resolveSteps() { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /requirementsascodeextract/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | } 4 | 5 | jar { 6 | manifest { 7 | attributes 'Implementation-Title': 'requirements as code - extract', 8 | 'Implementation-Version' : archiveVersion 9 | } 10 | } 11 | 12 | dependencies { 13 | implementation 'org.freemarker:freemarker:2.3.31' 14 | implementation 'org.apache.commons:commons-lang3:3.11' 15 | api ('org.requirementsascode:requirementsascodecore:' + version) 16 | } 17 | -------------------------------------------------------------------------------- /requirementsascodeextract/src/test/java/org/requirementsascode/extract/freemarker/systemreaction/GreetsUser.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.extract.freemarker.systemreaction; 2 | 3 | import java.util.function.Consumer; 4 | 5 | import org.requirementsascode.extract.freemarker.usercommand.EntersName; 6 | 7 | public class GreetsUser implements Consumer { 8 | @Override 9 | public void accept(EntersName enterName) { 10 | System.out.println("Hello, " + enterName.name); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /requirementsascodeextract/src/test/java/org/requirementsascode/extract/freemarker/systemreaction/NameEntered.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.extract.freemarker.systemreaction; 2 | 3 | import java.util.function.Function; 4 | 5 | import org.requirementsascode.extract.freemarker.usercommand.EntersName; 6 | 7 | public class NameEntered implements Function { 8 | 9 | @Override 10 | public String apply(EntersName entersName) { 11 | return entersName.getName(); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /requirementsascodecore/src/test/java/org/requirementsascode/testbehavior/TestExceptionThrower.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.testbehavior; 2 | 3 | import org.requirementsascode.Model; 4 | import org.requirementsascode.BehaviorModel; 5 | 6 | public class TestExceptionThrower implements BehaviorModel { 7 | @Override 8 | public Model model() { 9 | return Model.builder() 10 | .on(String.class).systemPublish(msg -> "Throws an exception, since it's a string (same response type as request type)") 11 | .build(); 12 | } 13 | } -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/systemreaction/AbstractContinues.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.systemreaction; 2 | 3 | import java.util.Objects; 4 | import java.util.function.Consumer; 5 | 6 | public abstract class AbstractContinues implements Consumer { 7 | private String stepName; 8 | 9 | public AbstractContinues(String stepName) { 10 | Objects.requireNonNull(stepName); 11 | this.stepName = stepName; 12 | } 13 | 14 | public String getStepName() { 15 | return stepName; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/main/java/helloworld/commandhandler/SaveName.java: -------------------------------------------------------------------------------- 1 | package helloworld.commandhandler; 2 | 3 | import java.util.function.Consumer; 4 | 5 | import helloworld.command.EnterText; 6 | import helloworld.domain.Person; 7 | 8 | public class SaveName implements Consumer { 9 | private Person person; 10 | 11 | public SaveName(Person person) { 12 | this.person = person; 13 | } 14 | 15 | @Override 16 | public void accept(EnterText t) { 17 | person.saveName(t.getText()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /requirementsascodeexamples/creditcard_eventsourcing/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "org.springframework.boot" version "2.5.3" 3 | id "io.spring.dependency-management" version "1.0.11.RELEASE" 4 | } 5 | 6 | jar { 7 | manifest { 8 | attributes 'Implementation-Title': 'creditcard eventsourcing', 9 | 'Implementation-Version':archiveVersion 10 | } 11 | } 12 | 13 | dependencies { 14 | implementation 'org.springframework.boot:spring-boot-starter-data-rest' 15 | implementation ('org.requirementsascode:requirementsascodecore:' + version) 16 | } 17 | 18 | -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/main/java/helloworld/commandhandler/SaveAge.java: -------------------------------------------------------------------------------- 1 | package helloworld.commandhandler; 2 | 3 | import java.util.function.Consumer; 4 | 5 | import helloworld.command.EnterText; 6 | import helloworld.domain.Person; 7 | 8 | public class SaveAge implements Consumer { 9 | private Person person; 10 | 11 | public SaveAge(Person person) { 12 | this.person = person; 13 | } 14 | 15 | @Override 16 | public void accept(EnterText t) { 17 | int age = Integer.parseInt(t.getText()); 18 | person.saveAge(age); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/exception/NestedCallOfReactTo.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.exception; 2 | 3 | public class NestedCallOfReactTo extends RuntimeException{ 4 | private static final long serialVersionUID = 4979548355692063295L; 5 | 6 | public NestedCallOfReactTo() { 7 | super(exceptionMessage()); 8 | } 9 | 10 | private static String exceptionMessage() { 11 | String message = "Don't call modelRunner.reactTo(x) from a message handler (i.e. from a method specified in system(..)). Use systemPublish() and return x from the specified message."; 12 | return message; 13 | } 14 | } -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/systemreaction/ContinuesAfter.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.systemreaction; 2 | 3 | import org.requirementsascode.FlowStep; 4 | import org.requirementsascode.UseCase; 5 | 6 | public class ContinuesAfter extends AbstractContinuesAfter{ 7 | private UseCase useCase; 8 | 9 | public ContinuesAfter(String stepName, UseCase useCase) { 10 | super(stepName); 11 | this.useCase = useCase; 12 | } 13 | 14 | @Override 15 | public FlowStep resolvePreviousStep() { 16 | FlowStep previousStep = (FlowStep) useCase.findStep(getStepName()); 17 | return previousStep; 18 | } 19 | } -------------------------------------------------------------------------------- /requirementsascodecore/src/test/java/org/requirementsascode/testbehavior/TestBehaviorModel.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.testbehavior; 2 | 3 | import org.requirementsascode.BehaviorModel; 4 | import org.requirementsascode.Model; 5 | 6 | public class TestBehaviorModel implements BehaviorModel { 7 | @Override 8 | public Model model() { 9 | return Model.builder() 10 | .user(TestCreateListRequest.class).systemPublish(addTask -> new TestCreateListResponse()) 11 | .user(TestAddTaskRequest.class).systemPublish(addTask -> new TestAddTaskResponse()) 12 | .user(TestCompleteTaskRequest.class).system(() -> {}) 13 | .build(); 14 | } 15 | } -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/exception/InfiniteRepetition.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.exception; 2 | 3 | import org.requirementsascode.Step; 4 | 5 | /** 6 | * Exception that is thrown when internally, a StackOverflowException occurs. 7 | * The likely cause is that a condition is always true. 8 | * 9 | * @author b_muth 10 | * 11 | */ 12 | public class InfiniteRepetition extends RuntimeException{ 13 | private static final long serialVersionUID = 5249803987787358917L; 14 | 15 | public InfiniteRepetition(Step step) { 16 | super("Possible cause: " + step.getName() + " has an always true condition."); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/main/java/helloworld/commandhandler/GreetWithDefaultName.java: -------------------------------------------------------------------------------- 1 | package helloworld.commandhandler; 2 | 3 | import helloworld.domain.Greeting; 4 | import helloworld.infrastructure.OutputAdapter; 5 | 6 | public class GreetWithDefaultName implements Runnable{ 7 | private OutputAdapter outputAdapter; 8 | 9 | public GreetWithDefaultName() { 10 | this.outputAdapter = new OutputAdapter(); 11 | } 12 | 13 | @Override 14 | public void run() { 15 | greetWithName("User"); 16 | } 17 | 18 | private void greetWithName(String name) { 19 | String greeting = Greeting.forUserWithName(name); 20 | outputAdapter.showMessage(greeting); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/exception/NoSuchElementInModel.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.exception; 2 | 3 | /** 4 | * Exception that is thrown when an element should be in the model because it is 5 | * referenced from somewhere, but it can't be found. 6 | * 7 | * @author b_muth 8 | * 9 | */ 10 | public class NoSuchElementInModel extends RuntimeException{ 11 | private static final long serialVersionUID = -6636292150079241122L; 12 | 13 | public NoSuchElementInModel(String elementName) { 14 | super(exceptionMessage(elementName)); 15 | } 16 | 17 | private static String exceptionMessage(String elementName) { 18 | return "Element does not exist in model:" + elementName; 19 | } 20 | } -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/exception/ElementAlreadyInModel.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.exception; 2 | 3 | /** 4 | * Exception that is thrown when somebody tries to create a new model element, 5 | * and a model element with the same name is already in the model. 6 | * 7 | * @author b_muth 8 | * 9 | */ 10 | public class ElementAlreadyInModel extends RuntimeException{ 11 | private static final long serialVersionUID = -510216736346192818L; 12 | 13 | public ElementAlreadyInModel(String elementName) { 14 | super(exceptionMessage(elementName)); 15 | } 16 | 17 | private static String exceptionMessage(String elementName) { 18 | return "Element already exists in model: " + elementName; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/systemreaction/AbstractContinuesAfter.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.systemreaction; 2 | 3 | import org.requirementsascode.FlowStep; 4 | import org.requirementsascode.ModelRunner; 5 | 6 | public abstract class AbstractContinuesAfter extends AbstractContinues { 7 | private FlowStep previousStep; 8 | 9 | public AbstractContinuesAfter(String stepName) { 10 | super(stepName); 11 | } 12 | 13 | @Override 14 | public void accept(ModelRunner runner) { 15 | if(previousStep == null) { 16 | previousStep = resolvePreviousStep(); 17 | } 18 | runner.setLatestStep(previousStep); 19 | } 20 | 21 | public abstract FlowStep resolvePreviousStep(); 22 | } 23 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/UserActor.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | class UserActor extends Actor{ 4 | public UserActor() { 5 | super("User"); 6 | } 7 | 8 | @Override 9 | public boolean equals(Object obj) { 10 | if (this == obj) 11 | return true; 12 | if (obj == null) 13 | return false; 14 | if (getClass() != obj.getClass()) 15 | return false; 16 | UserActor other = (UserActor) obj; 17 | if (getName() == null) { 18 | if (other.getName() != null) 19 | return false; 20 | } else if (!getName().equals(other.getName())) 21 | return false; 22 | return true; 23 | } 24 | 25 | @Override 26 | public int hashCode() { 27 | return getName().hashCode(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/main/java/helloworld/domain/Person.java: -------------------------------------------------------------------------------- 1 | package helloworld.domain; 2 | 3 | public class Person { 4 | private final int MIN_AGE = 5; 5 | private final int MAX_AGE = 130; 6 | 7 | private String name; 8 | private int age; 9 | 10 | public boolean ageIsOutOfBounds() { 11 | return age < MIN_AGE || age > MAX_AGE; 12 | } 13 | 14 | public boolean ageIsOk() { 15 | return !ageIsOutOfBounds(); 16 | } 17 | 18 | public void saveName(String name) { 19 | this.name = name; 20 | } 21 | 22 | public void saveAge(int age) { 23 | this.age = age; 24 | } 25 | 26 | public String getName() { 27 | return name; 28 | } 29 | 30 | public int getAge() { 31 | return age; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/SystemActor.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | class SystemActor extends Actor{ 4 | public SystemActor() { 5 | super("System"); 6 | } 7 | 8 | @Override 9 | public boolean equals(Object obj) { 10 | if (this == obj) 11 | return true; 12 | if (obj == null) 13 | return false; 14 | if (getClass() != obj.getClass()) 15 | return false; 16 | SystemActor other = (SystemActor) obj; 17 | if (getName() == null) { 18 | if (other.getName() != null) 19 | return false; 20 | } else if (!getName().equals(other.getName())) 21 | return false; 22 | return true; 23 | } 24 | 25 | @Override 26 | public int hashCode() { 27 | return getName().hashCode(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/test/java/helloworld/actor/InvalidUser.java: -------------------------------------------------------------------------------- 1 | package helloworld.actor; 2 | 3 | import org.requirementsascode.AbstractActor; 4 | import org.requirementsascode.Model; 5 | 6 | import helloworld.command.EnterText; 7 | 8 | public class InvalidUser extends AbstractActor{ 9 | private final AbstractActor helloWorldActor; 10 | 11 | public InvalidUser(AbstractActor helloWorldActor) { 12 | this.helloWorldActor = helloWorldActor; 13 | } 14 | 15 | @Override 16 | protected Model behavior() { 17 | Model model = Model.builder() 18 | .useCase("Get greeted") 19 | .basicFlow() 20 | .step("S1a").systemPublish(() -> new EnterText("John Q. Public")).to(helloWorldActor) 21 | .build(); 22 | 23 | return model; 24 | } 25 | } -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/main/java/helloworld/commandhandler/GreetPersonWithAge.java: -------------------------------------------------------------------------------- 1 | package helloworld.commandhandler; 2 | 3 | import helloworld.domain.Greeting; 4 | import helloworld.domain.Person; 5 | import helloworld.infrastructure.OutputAdapter; 6 | 7 | public class GreetPersonWithAge implements Runnable{ 8 | private Person person; 9 | private OutputAdapter outputAdapter; 10 | 11 | public GreetPersonWithAge(Person person) { 12 | this.person = person; 13 | this.outputAdapter = new OutputAdapter(); 14 | } 15 | 16 | @Override 17 | public void run() { 18 | greetWithAge(person.getAge()); 19 | } 20 | 21 | private void greetWithAge(int age) { 22 | String greeting = Greeting.forUserWithAge(age); 23 | outputAdapter.showMessage(greeting); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/FlowlessStep.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | import java.util.function.Predicate; 4 | 5 | /** 6 | * A step that is not part of a flow (i.e. no flow definition in the model). 7 | * 8 | * @author b_muth 9 | * 10 | */ 11 | public class FlowlessStep extends Step { 12 | FlowlessStep(String stepName, UseCase useCase, Condition optionalCondition) { 13 | super(stepName, useCase, optionalCondition); 14 | } 15 | 16 | @Override 17 | public Predicate getPredicate() { 18 | Predicate predicate = toPredicate(getConditionOrElseTrue()); 19 | return predicate; 20 | } 21 | 22 | private Condition getConditionOrElseTrue() { 23 | Condition condition = getCondition().orElse(() -> true); 24 | return condition; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/main/java/helloworld/commandhandler/GreetPersonWithName.java: -------------------------------------------------------------------------------- 1 | package helloworld.commandhandler; 2 | 3 | import helloworld.domain.Greeting; 4 | import helloworld.domain.Person; 5 | import helloworld.infrastructure.OutputAdapter; 6 | 7 | public class GreetPersonWithName implements Runnable{ 8 | private Person person; 9 | private OutputAdapter outputAdapter; 10 | 11 | public GreetPersonWithName(Person person) { 12 | this.person = person; 13 | this.outputAdapter = new OutputAdapter(); 14 | } 15 | 16 | @Override 17 | public void run() { 18 | greetWithName(person.getName()); 19 | } 20 | 21 | private void greetWithName(String name) { 22 | String greeting = Greeting.forUserWithName(name); 23 | outputAdapter.showMessage(greeting); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/systemreaction/ContinuesAt.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.systemreaction; 2 | 3 | import org.requirementsascode.FlowStep; 4 | 5 | public class ContinuesAt extends AbstractContinues { 6 | private FlowStep currentStep; 7 | private FlowStep continueAtStep; 8 | 9 | public ContinuesAt(String continueAtStepName, FlowStep currentStep) { 10 | super(continueAtStepName); 11 | this.currentStep = currentStep; 12 | 13 | } 14 | 15 | @Override 16 | public void accept(Object message) { 17 | if(continueAtStep == null) { 18 | resolveContinueAtStep(); 19 | } 20 | } 21 | 22 | public void resolveContinueAtStep() { 23 | continueAtStep = ((FlowStep) currentStep.getUseCase().findStep(getStepName())); 24 | continueAtStep.orAfter(currentStep); 25 | } 26 | } -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/test/java/cleanarchitecture/CleanArchitectureOutlineTest.java: -------------------------------------------------------------------------------- 1 | package cleanarchitecture; 2 | 3 | import static org.mockito.Mockito.mock; 4 | import static org.mockito.Mockito.verify; 5 | 6 | import org.junit.jupiter.api.Test; 7 | import org.requirementsascode.Behavior; 8 | import org.requirementsascode.StatelessBehavior; 9 | 10 | public class CleanArchitectureOutlineTest { 11 | 12 | @Test 13 | public void greetsJack() { 14 | ConsolePrinter mockConsolePrinter = mock(ConsolePrinter.class); 15 | GreetingServiceModel greetingServiceModel = new GreetingServiceModel(mockConsolePrinter); 16 | Behavior greetingService = StatelessBehavior.of(greetingServiceModel); 17 | 18 | greetingService.reactTo(new SayHelloRequest("Jack")); 19 | 20 | verify(mockConsolePrinter).accept("Hello, Jack."); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/main/java/helloworld/actor/ValidUser.java: -------------------------------------------------------------------------------- 1 | package helloworld.actor; 2 | 3 | import org.requirementsascode.AbstractActor; 4 | import org.requirementsascode.Model; 5 | 6 | import helloworld.command.EnterText; 7 | 8 | public class ValidUser extends AbstractActor{ 9 | private final AbstractActor helloWorldActor; 10 | 11 | public ValidUser(AbstractActor helloWorldActor) { 12 | this.helloWorldActor = helloWorldActor; 13 | } 14 | 15 | @Override 16 | protected Model behavior() { 17 | Model model = Model.builder() 18 | .useCase("Get greeted") 19 | .basicFlow() 20 | .step("S1").systemPublish(() -> new EnterText("John Q. Public")).to(helloWorldActor) 21 | .step("S2").systemPublish(() -> new EnterText("43")).to(helloWorldActor) 22 | .build(); 23 | 24 | return model; 25 | } 26 | } -------------------------------------------------------------------------------- /requirementsascodeextract/src/main/java/org/requirementsascode/extract/freemarker/methodmodel/util/Words.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.extract.freemarker.methodmodel.util; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | public class Words { 6 | public static String getLowerCaseWordsOfClassName(Class clazz) { 7 | String[] wordArray = toWordArray(clazz.getSimpleName()); 8 | String words = wordArrayToLowerCaseString(wordArray); 9 | return words; 10 | } 11 | 12 | private static String[] toWordArray(String camelCaseString) { 13 | String[] wordsArray = StringUtils.splitByCharacterTypeCamelCase(camelCaseString); 14 | return wordsArray; 15 | } 16 | 17 | private static String wordArrayToLowerCaseString(String[] wordArray) { 18 | String wordString = StringUtils.join(wordArray, " ").toLowerCase(); 19 | return wordString; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /requirementsascodeexamples/creditcard_eventsourcing/src/main/java/creditcard_eventsourcing/model/event/CycleClosed.java: -------------------------------------------------------------------------------- 1 | package creditcard_eventsourcing.model.event; 2 | 3 | import java.time.Instant; 4 | import java.util.UUID; 5 | 6 | /** 7 | * Based on code by Jakub Pilimon: 8 | * https://gitlab.com/pilloPl/eventsourced-credit-cards/blob/4329a0aac283067f1376b3802e13f5a561f18753 9 | * 10 | */ 11 | public class CycleClosed implements DomainEvent { 12 | private final UUID cardNo; 13 | private final Instant timestamp; 14 | 15 | public CycleClosed(UUID cardNo, Instant timestamp) { 16 | this.cardNo = cardNo; 17 | this.timestamp = timestamp; 18 | } 19 | 20 | public UUID getCardNo() { 21 | return cardNo; 22 | } 23 | 24 | public Instant getTimestamp() { 25 | return timestamp; 26 | } 27 | 28 | @Override 29 | public String getType() { 30 | return "cycle-closed"; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/main/java/helloworld/commandhandler/GreetWithEnteredName.java: -------------------------------------------------------------------------------- 1 | package helloworld.commandhandler; 2 | 3 | import java.util.function.Consumer; 4 | 5 | import helloworld.command.EnterText; 6 | import helloworld.domain.Greeting; 7 | import helloworld.infrastructure.OutputAdapter; 8 | 9 | public class GreetWithEnteredName implements Runnable, Consumer { 10 | private OutputAdapter outputAdapter; 11 | 12 | public GreetWithEnteredName() { 13 | this.outputAdapter = new OutputAdapter(); 14 | } 15 | 16 | @Override 17 | public void accept(EnterText t) { 18 | greetWithName(t.getText()); 19 | } 20 | 21 | @Override 22 | public void run() { 23 | greetWithName("User"); 24 | } 25 | 26 | private void greetWithName(String name) { 27 | String greeting = Greeting.forUserWithName(name); 28 | outputAdapter.showMessage(greeting); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/exception/MissingUseCaseStepPart.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.exception; 2 | 3 | import org.requirementsascode.Step; 4 | 5 | /** 6 | * Exception that is thrown when the model runner tries to to access a certain 7 | * part of a step, but that part does not exist. 8 | * 9 | * @author b_muth 10 | * 11 | */ 12 | public class MissingUseCaseStepPart extends RuntimeException{ 13 | private static final long serialVersionUID = 1154053717206525045L; 14 | 15 | public MissingUseCaseStepPart(Step useCaseStep, String partName) { 16 | super(exceptionMessage(useCaseStep, partName)); 17 | } 18 | 19 | private static String exceptionMessage(Step useCaseStep, String partName) { 20 | String message = "Step \"" + useCaseStep + "\" has no defined " + partName 21 | + " part! Please have a look and update your model for this step!"; 22 | return message; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/main/java/helloworld/HelloWorld01.java: -------------------------------------------------------------------------------- 1 | package helloworld; 2 | 3 | import org.requirementsascode.AbstractActor; 4 | import org.requirementsascode.Model; 5 | 6 | import helloworld.commandhandler.GreetWithDefaultName; 7 | 8 | public class HelloWorld01 { 9 | public static void main(String[] args) { 10 | HelloWorldActor01 actor = new HelloWorldActor01(new GreetWithDefaultName()); 11 | actor.run(); 12 | } 13 | } 14 | 15 | class HelloWorldActor01 extends AbstractActor { 16 | private final Runnable greetsUser; 17 | 18 | public HelloWorldActor01(Runnable greetsUser) { 19 | this.greetsUser = greetsUser; 20 | } 21 | 22 | @Override 23 | protected Model behavior() { 24 | Model model = Model.builder() 25 | .useCase("Get greeted") 26 | .basicFlow() 27 | .step("S1").system(greetsUser) 28 | .build(); 29 | 30 | return model; 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/main/java/helloworld/actor/AnonymousUser.java: -------------------------------------------------------------------------------- 1 | package helloworld.actor; 2 | 3 | import org.requirementsascode.AbstractActor; 4 | import org.requirementsascode.Model; 5 | 6 | import helloworld.command.EnterText; 7 | 8 | public class AnonymousUser extends AbstractActor{ 9 | private final AbstractActor helloWorldActor; 10 | private final String ageString; 11 | 12 | public AnonymousUser(AbstractActor helloWorldActor, String ageString) { 13 | this.helloWorldActor = helloWorldActor; 14 | this.ageString = ageString; 15 | } 16 | 17 | @Override 18 | protected Model behavior() { 19 | Model model = Model.builder() 20 | .useCase("Get greeted") 21 | .basicFlow() 22 | .step("S1").systemPublish(() -> new EnterText(ageString)).to(helloWorldActor) 23 | .step("S2").systemPublish(() -> new EnterText("43")).to(helloWorldActor) 24 | .build(); 25 | 26 | return model; 27 | } 28 | } -------------------------------------------------------------------------------- /requirementsascodeextract/src/test/java/org/requirementsascode/extract/freemarker/methodmodel/WordsTest.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.extract.freemarker.methodmodel; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | import org.requirementsascode.extract.freemarker.methodmodel.util.Words; 7 | import org.requirementsascode.extract.freemarker.usercommand.DecidesToQuit; 8 | import org.requirementsascode.extract.freemarker.usercommand.EntersName; 9 | 10 | import freemarker.template.TemplateModelException; 11 | 12 | public class WordsTest { 13 | @Test 14 | public void returnsTwoLowerCaseWords() throws TemplateModelException { 15 | assertEquals("enters name", Words.getLowerCaseWordsOfClassName(EntersName.class)); 16 | } 17 | 18 | @Test 19 | public void returnsThreeLowerCaseWords() throws TemplateModelException { 20 | assertEquals("decides to quit", Words.getLowerCaseWordsOfClassName(DecidesToQuit.class)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/exception/MoreThanOneStepCanReact.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.exception; 2 | 3 | import java.util.Collection; 4 | import java.util.stream.Collectors; 5 | 6 | import org.requirementsascode.Step; 7 | 8 | /** 9 | * Exception that is thrown when more than one step could react to a certain 10 | * message. 11 | * 12 | * @author b_muth 13 | * 14 | */ 15 | public class MoreThanOneStepCanReact extends RuntimeException{ 16 | private static final long serialVersionUID = 1773129287125843814L; 17 | 18 | public MoreThanOneStepCanReact(Collection useCaseSteps) { 19 | super(exceptionMessage(useCaseSteps)); 20 | } 21 | 22 | private static String exceptionMessage(Collection useCaseSteps) { 23 | String message = "System can react to more than one step: "; 24 | String useCaseStepsClassNames = useCaseSteps.stream().map(useCaseStep -> useCaseStep.toString()) 25 | .collect(Collectors.joining(",", message, "")); 26 | return useCaseStepsClassNames; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/main/java/helloworld/domain/Color.java: -------------------------------------------------------------------------------- 1 | package helloworld.domain; 2 | 3 | public class Color { 4 | private String inputColor = "green"; 5 | private String outputColor; 6 | 7 | public boolean isInputColorRed() { 8 | return inputColor.equals("red"); 9 | } 10 | 11 | public boolean isInputColorYellow() { 12 | return inputColor.equals("yellow"); 13 | } 14 | 15 | public boolean isInputColorGreen() { 16 | return inputColor.equals("green"); 17 | } 18 | 19 | public void setOutputColorToRed() { 20 | outputColor = "red"; 21 | } 22 | 23 | public void setOutputColorToYellow() { 24 | outputColor = "yellow"; 25 | } 26 | 27 | public void setOutputColorToGreen() { 28 | outputColor = "green"; 29 | } 30 | 31 | public String getInputColor() { 32 | return inputColor; 33 | } 34 | 35 | public void setInputColor(String inputColor) { 36 | this.inputColor = inputColor; 37 | } 38 | 39 | public String getOutputColor() { 40 | return outputColor; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /requirementsascodeexamples/creditcard_eventsourcing/src/main/java/creditcard_eventsourcing/model/event/CardRepaid.java: -------------------------------------------------------------------------------- 1 | package creditcard_eventsourcing.model.event; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.Instant; 5 | import java.util.UUID; 6 | 7 | /** 8 | * Based on code by Jakub Pilimon: 9 | * https://gitlab.com/pilloPl/eventsourced-credit-cards/blob/4329a0aac283067f1376b3802e13f5a561f18753 10 | * 11 | */ 12 | public class CardRepaid implements DomainEvent { 13 | private final UUID cardNo; 14 | private final BigDecimal amount; 15 | private final Instant timestamp; 16 | 17 | public CardRepaid(UUID cardNo, BigDecimal amount, Instant timestamp) { 18 | this.cardNo = cardNo; 19 | this.amount = amount; 20 | this.timestamp = timestamp; 21 | } 22 | 23 | public UUID getCardNo() { 24 | return cardNo; 25 | } 26 | 27 | public BigDecimal getAmount() { 28 | return amount; 29 | } 30 | 31 | public Instant getTimestamp() { 32 | return timestamp; 33 | } 34 | 35 | @Override 36 | public String getType() { 37 | return "card-repaid"; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /requirementsascodeexamples/creditcard_eventsourcing/src/main/java/creditcard_eventsourcing/model/event/CardWithdrawn.java: -------------------------------------------------------------------------------- 1 | package creditcard_eventsourcing.model.event; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.Instant; 5 | import java.util.UUID; 6 | 7 | /** 8 | * Based on code by Jakub Pilimon: 9 | * https://gitlab.com/pilloPl/eventsourced-credit-cards/blob/4329a0aac283067f1376b3802e13f5a561f18753 10 | * 11 | */ 12 | public class CardWithdrawn implements DomainEvent { 13 | private final UUID cardNo; 14 | private final BigDecimal amount; 15 | private final Instant timestamp; 16 | 17 | public CardWithdrawn(UUID cardNo, BigDecimal amount, Instant timestamp) { 18 | this.cardNo = cardNo; 19 | this.amount = amount; 20 | this.timestamp = timestamp; 21 | } 22 | 23 | public UUID getCardNo() { 24 | return cardNo; 25 | } 26 | 27 | public BigDecimal getAmount() { 28 | return amount; 29 | } 30 | 31 | public Instant getTimestamp() { 32 | return timestamp; 33 | } 34 | 35 | @Override 36 | public String getType() { 37 | return "card-withdrawn"; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /requirementsascodeexamples/creditcard_eventsourcing/src/main/java/creditcard_eventsourcing/model/event/LimitAssigned.java: -------------------------------------------------------------------------------- 1 | package creditcard_eventsourcing.model.event; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.Instant; 5 | import java.util.UUID; 6 | 7 | /** 8 | * Based on code by Jakub Pilimon: 9 | * https://gitlab.com/pilloPl/eventsourced-credit-cards/blob/4329a0aac283067f1376b3802e13f5a561f18753 10 | * 11 | */ 12 | public class LimitAssigned implements DomainEvent { 13 | private final UUID cardNo; 14 | private final BigDecimal amount; 15 | private final Instant timestamp; 16 | 17 | public LimitAssigned(UUID cardNo, BigDecimal amount, Instant timestamp) { 18 | this.cardNo = cardNo; 19 | this.amount = amount; 20 | this.timestamp = timestamp; 21 | } 22 | 23 | public BigDecimal getAmount() { 24 | return amount; 25 | } 26 | 27 | public Instant getTimestamp() { 28 | return timestamp; 29 | } 30 | 31 | public UUID getCardNo() { 32 | return cardNo; 33 | } 34 | 35 | @Override 36 | public String getType() { 37 | return "limit-assigned"; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/Actor.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | import java.util.Objects; 4 | 5 | /** 6 | * An actor with dynamically attachable behavior. 7 | * 8 | * @author b_muth 9 | */ 10 | public class Actor extends AbstractActor{ 11 | private Model behavior; 12 | 13 | /** 14 | * Creates an actor with a name equal to the current class' simple name. 15 | * 16 | */ 17 | public Actor() { 18 | super(); 19 | } 20 | 21 | /** 22 | * Creates an actor with the specified name. 23 | * 24 | * @param name the name of the actor 25 | */ 26 | public Actor(String name) { 27 | super(name); 28 | } 29 | 30 | /** 31 | * Attach the specified behavior to the actor. 32 | * 33 | * @param behaviorModel the behvior the actor will show from now on 34 | * 35 | * @return this actor 36 | */ 37 | public Actor withBehavior(Model behaviorModel) { 38 | this.behavior = Objects.requireNonNull(behaviorModel); 39 | return this; 40 | } 41 | 42 | @Override 43 | protected Model behavior() { 44 | return behavior; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/main/java/helloworld/actor/NormalUser.java: -------------------------------------------------------------------------------- 1 | package helloworld.actor; 2 | 3 | import org.requirementsascode.AbstractActor; 4 | import org.requirementsascode.Model; 5 | 6 | import helloworld.command.EnterText; 7 | 8 | public class NormalUser extends AbstractActor{ 9 | private final AbstractActor helloWorldActor; 10 | private final String name; 11 | private final String ageString; 12 | 13 | public NormalUser(AbstractActor helloWorldActor, String name, String ageString) { 14 | this.helloWorldActor = helloWorldActor; 15 | this.name = name; 16 | this.ageString = ageString; 17 | } 18 | 19 | @Override 20 | protected Model behavior() { 21 | Model model = Model.builder() 22 | .useCase("Get greeted") 23 | .basicFlow() 24 | .step("S1").systemPublish(() -> new EnterText(name)).to(helloWorldActor) 25 | .step("S2").systemPublish(() -> new EnterText(ageString)).to(helloWorldActor) 26 | .step("S3").systemPublish(() -> new EnterText("43")).to(helloWorldActor) 27 | .build(); 28 | 29 | return model; 30 | } 31 | } -------------------------------------------------------------------------------- /requirementsascodeexamples/creditcard_eventsourcing/src/main/java/creditcard_eventsourcing/persistence/EventStore.java: -------------------------------------------------------------------------------- 1 | package creditcard_eventsourcing.persistence; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.Set; 8 | import java.util.UUID; 9 | 10 | import org.springframework.stereotype.Repository; 11 | 12 | import creditcard_eventsourcing.model.event.DomainEvent; 13 | 14 | /** 15 | * Based on code by Jakub Pilimon: 16 | * https://gitlab.com/pilloPl/eventsourced-credit-cards/blob/4329a0aac283067f1376b3802e13f5a561f18753 17 | * 18 | * @author b_muth 19 | * 20 | */ 21 | @Repository 22 | public class EventStore { 23 | private final Map> eventStream = new HashMap<>(); 24 | 25 | public void save(UUID uuid, List currentStream) { 26 | eventStream.put(uuid, currentStream); 27 | } 28 | 29 | public List loadEvents(UUID uuid) { 30 | return eventStream.getOrDefault(uuid, new ArrayList<>()); 31 | } 32 | 33 | public Set uuids() { 34 | return eventStream.keySet(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/main/java/helloworld/HelloWorld03.java: -------------------------------------------------------------------------------- 1 | package helloworld; 2 | 3 | import java.util.function.Consumer; 4 | 5 | import org.requirementsascode.AbstractActor; 6 | import org.requirementsascode.Model; 7 | 8 | import helloworld.command.EnterText; 9 | import helloworld.commandhandler.GreetWithEnteredName; 10 | 11 | public class HelloWorld03 { 12 | public static void main(String[] args) { 13 | HelloWorldActor03 actor = new HelloWorldActor03(new GreetWithEnteredName()); 14 | actor.reactTo(new EnterText("John Q. Public")); 15 | } 16 | } 17 | 18 | class HelloWorldActor03 extends AbstractActor { 19 | private final Class entersName = EnterText.class; 20 | private final Consumer greetsUser; 21 | 22 | public HelloWorldActor03(Consumer greetsUser) { 23 | this.greetsUser = greetsUser; 24 | } 25 | 26 | @Override 27 | protected Model behavior() { 28 | Model model = Model.builder() 29 | .useCase("Get greeted") 30 | .basicFlow() 31 | .step("S1").user(entersName).system(greetsUser) 32 | .build(); 33 | 34 | return model; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /requirementsascodeexamples/actor/src/main/java/actor/PublishingActorExample.java: -------------------------------------------------------------------------------- 1 | package actor; 2 | 3 | import java.util.Optional; 4 | 5 | import org.requirementsascode.AbstractActor; 6 | import org.requirementsascode.Model; 7 | 8 | import message.EnterName; 9 | 10 | public class PublishingActorExample { 11 | public static void main(String[] args) { 12 | AbstractActor actor = new PublishingActor(); 13 | 14 | Optional userName = actor.reactTo(new EnterName("Joe")); 15 | System.out.println("Your name is: " + userName.get() + "."); 16 | } 17 | } 18 | 19 | class PublishingActor extends AbstractActor { 20 | @Override 21 | protected Model behavior() { 22 | Model model = Model.builder() 23 | .user(EnterName.class).systemPublish(this::publishNameAsString) 24 | .on(String.class).system(this::displayNameString) 25 | .build(); 26 | return model; 27 | } 28 | 29 | private String publishNameAsString(EnterName enterName) { 30 | return enterName.getUserName(); 31 | } 32 | 33 | public void displayNameString(String nameString) { 34 | System.out.println("Welcome, " + nameString + "."); 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /requirementsascodeexamples/crosscuttingconcerns/README.md: -------------------------------------------------------------------------------- 1 | # cross-cutting concerns example - measure performance of system reactions 2 | ``` java 3 | class CrossCuttingConcernsActor01 extends AbstractActor { 4 | public CrossCuttingConcernsActor01() { 5 | getModelRunner().handleWith(this::measuresPerformance); 6 | } 7 | 8 | @Override 9 | public Model behavior() { 10 | Model model = Model.builder() 11 | .user(RequestCalculating.class).system(this::calculate) 12 | .build(); 13 | return model; 14 | } 15 | 16 | private void measuresPerformance(StepToBeRun stepToBeRun) { 17 | long timeBefore = System.nanoTime(); 18 | stepToBeRun.run(); 19 | long timeAfter = System.nanoTime(); 20 | long timeElapsed = timeAfter - timeBefore; 21 | 22 | System.out.println("Elapsed time: " + timeElapsed + " nanoseconds."); 23 | } 24 | 25 | private void calculate() { 26 | Math.pow(2, 1000); 27 | } 28 | } 29 | ``` 30 | For the full source code, [look here](https://github.com/bertilmuth/requirementsascode/blob/master/requirementsascodeexamples/crosscuttingconcerns/src/main/java/crosscuttingconcerns/CrossCuttingConcerns01.java). 31 | -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/main/java/helloworld/HelloWorld02.java: -------------------------------------------------------------------------------- 1 | package helloworld; 2 | 3 | import org.requirementsascode.AbstractActor; 4 | import org.requirementsascode.Condition; 5 | import org.requirementsascode.Model; 6 | 7 | import helloworld.commandhandler.GreetWithDefaultName; 8 | 9 | public class HelloWorld02 { 10 | public static void main(String[] args) { 11 | HelloWorldActor02 actor = new HelloWorldActor02(new GreetWithDefaultName()); 12 | actor.run(); 13 | } 14 | } 15 | 16 | class HelloWorldActor02 extends AbstractActor { 17 | private final Runnable greetsUser; 18 | private final Condition lessThan3 = this::lessThan3; 19 | 20 | private int greetingsCounter = 0; 21 | 22 | public HelloWorldActor02(Runnable greetsUser) { 23 | this.greetsUser = greetsUser; 24 | } 25 | 26 | @Override 27 | protected Model behavior() { 28 | Model model = Model.builder() 29 | .useCase("Get greeted") 30 | .basicFlow() 31 | .step("S1").system(greetsUser).reactWhile(lessThan3) 32 | .build(); 33 | 34 | return model; 35 | } 36 | 37 | private boolean lessThan3() { 38 | return greetingsCounter++ < 3; 39 | } 40 | } -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/BehaviorModel.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | /** 4 | * Implement this interface to specify a model that describes a behavior: which 5 | * messages are consumed, and which responses are produced. See See 6 | * the requirements 7 | * as code website for examples. 8 | * 9 | */ 10 | public interface BehaviorModel { 11 | /** 12 | * A model that defines which messages are consumed and which responses are 13 | * produced. See See 14 | * the requirements 15 | * as code website for examples. 16 | * 17 | * @return the behavior model 18 | */ 19 | Model model(); 20 | 21 | /** 22 | * The default response is the response that a behavior returns when a message 23 | * is just consumed (via a .system(...) definition in the model), 24 | * or when a handler function returns null. 25 | * 26 | * @return the default response, null by default. Override this method to 27 | * provide a non-null default response. 28 | */ 29 | default Object defaultResponse() { 30 | return null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/ModelElement.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | import java.util.Objects; 4 | 5 | /** 6 | * Abstract base class for elements of a model. The main purpose of this class 7 | * is to create named model elements, and make them part of a model. 8 | * 9 | * @author b_muth 10 | */ 11 | abstract class ModelElement{ 12 | private String name; 13 | private Model model; 14 | 15 | /** 16 | * Creates a new element that is a part of the specified model. 17 | * 18 | * @param name the name of the element to be created 19 | * @param model the model that will contain the element 20 | */ 21 | ModelElement(String name, Model model) { 22 | Objects.requireNonNull(name); 23 | Objects.requireNonNull(model); 24 | this.name = name; 25 | this.model = model; 26 | } 27 | 28 | /** 29 | * Returns the name of the element. 30 | * 31 | * @return the name 32 | */ 33 | public String getName() { 34 | return name; 35 | } 36 | 37 | /** 38 | * Returns the model that this element is part of. 39 | * 40 | * @return the model 41 | */ 42 | public Model getModel() { 43 | return model; 44 | } 45 | 46 | @Override 47 | public String toString() { 48 | return getName(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /requirementsascodeexamples/creditcard_eventsourcing/src/main/java/creditcard_eventsourcing/controller/CreditCardController.java: -------------------------------------------------------------------------------- 1 | package creditcard_eventsourcing.controller; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Set; 6 | import java.util.UUID; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | import creditcard_eventsourcing.model.CreditCardAggregateRoot; 13 | import creditcard_eventsourcing.persistence.EventStore; 14 | 15 | /** 16 | * Based on code by Jakub Pilimon: 17 | * https://gitlab.com/pilloPl/eventsourced-credit-cards/blob/4329a0aac283067f1376b3802e13f5a561f18753 18 | * 19 | */ 20 | @RestController 21 | class CreditCardController { 22 | @Autowired 23 | EventStore eventStore; 24 | 25 | @GetMapping("/cards") 26 | List creditCardList() { 27 | List creditCards = new ArrayList<>(); 28 | Set uuids = eventStore.uuids(); 29 | for (UUID uuid : uuids) { 30 | CreditCardAggregateRoot creditCard = new CreditCardAggregateRoot(uuid, eventStore); 31 | creditCards.add(creditCard); 32 | } 33 | return creditCards; 34 | } 35 | } -------------------------------------------------------------------------------- /requirementsascodeexamples/crosscuttingconcerns/src/main/java/crosscuttingconcerns/CrossCuttingConcerns01.java: -------------------------------------------------------------------------------- 1 | package crosscuttingconcerns; 2 | 3 | import org.requirementsascode.AbstractActor; 4 | import org.requirementsascode.Model; 5 | import org.requirementsascode.StepToBeRun; 6 | 7 | public class CrossCuttingConcerns01 { 8 | public static void main(String[] args) { 9 | new CrossCuttingConcernsActor01().reactTo(new RequestCalculating()); 10 | } 11 | } 12 | 13 | class CrossCuttingConcernsActor01 extends AbstractActor { 14 | public CrossCuttingConcernsActor01() { 15 | getModelRunner().handleWith(this::measuresPerformance); 16 | } 17 | 18 | @Override 19 | protected Model behavior() { 20 | Model model = Model.builder() 21 | .user(RequestCalculating.class).system(this::calculate) 22 | .build(); 23 | return model; 24 | } 25 | 26 | private void measuresPerformance(StepToBeRun stepToBeRun) { 27 | long timeBefore = System.nanoTime(); 28 | stepToBeRun.run(); 29 | long timeAfter = System.nanoTime(); 30 | long timeElapsed = timeAfter - timeBefore; 31 | 32 | System.out.println("Elapsed time: " + timeElapsed + " nanoseconds."); 33 | } 34 | 35 | private void calculate() { 36 | Math.pow(2, 1000); 37 | } 38 | } 39 | 40 | class RequestCalculating { 41 | } 42 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/Behavior.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | import java.util.Optional; 4 | 5 | /** 6 | * A behavior occurs when reacting to a message, and optionally producing a 7 | * response. Which messages are reacted to and responses produced depends on the 8 | * behavior's model. 9 | * 10 | * @author b_muth 11 | * 12 | */ 13 | public interface Behavior { 14 | /** 15 | * Reacts to the specified message. 16 | * 17 | * If the message's class is the same or a sub class of a class defined in the 18 | * behavior model with .user(...) or .on(...), the 19 | * system will react with the function specified by .system(...) or 20 | * .systemPublish(...). 21 | * 22 | * @param the type you expect (return value will be cast to it) 23 | * @param message the incoming message 24 | * @return either the optional result of a function, or an optional default 25 | * response, or an empty optional if both are not present 26 | */ 27 | Optional reactTo(Object message); 28 | 29 | /** 30 | * Returns the behavior's model, that defines which message to react to and 31 | * which responses to produce. 32 | * 33 | * @return the behavior model 34 | */ 35 | BehaviorModel behaviorModel(); 36 | } 37 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/flowposition/InsteadOf.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.flowposition; 2 | 3 | import org.requirementsascode.FlowStep; 4 | import org.requirementsascode.ModelRunner; 5 | import org.requirementsascode.UseCase; 6 | 7 | public class InsteadOf extends FlowPosition{ 8 | private String stepName; 9 | private FlowStep step; 10 | 11 | public InsteadOf(String stepName, UseCase useCase) { 12 | super(useCase); 13 | this.stepName = stepName; 14 | } 15 | 16 | @Override 17 | protected boolean isRunnerAtRightPositionFor(ModelRunner modelRunner) { 18 | if(step == null) { 19 | throw new RuntimeException("step has not been resolved. Please call resolveSteps()!"); 20 | } 21 | FlowPosition flowPosition = step.getFlowPosition(); 22 | return flowPosition.test(modelRunner); 23 | } 24 | 25 | public void resolveSteps() { 26 | if(step == null) { 27 | FlowStep resolvedStep = null; 28 | 29 | UseCase useCase = getUseCase(); 30 | String stepName = getStepName(); 31 | if (useCase != null && stepName != null) { 32 | resolvedStep = (FlowStep) useCase.findStep(stepName); 33 | } 34 | 35 | this.step = resolvedStep; 36 | } 37 | } 38 | 39 | public final String getStepName() { 40 | return stepName; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/InterruptingFlowStep.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | import java.util.function.Predicate; 4 | 5 | import org.requirementsascode.flowposition.FlowPosition; 6 | 7 | public class InterruptingFlowStep extends FlowStep { 8 | InterruptingFlowStep(String stepName, Flow useCaseFlow, FlowPosition flowPosition, Condition condition) { 9 | super(stepName, useCaseFlow, condition); 10 | setFlowPosition(flowPosition); 11 | } 12 | 13 | public Predicate getPredicate() { 14 | Predicate predicate; 15 | Condition reactWhile = getReactWhile(); 16 | 17 | predicate = isFlowConditionTrueAndRunnerInDifferentFlow(); 18 | if (reactWhile != null) { 19 | predicate = predicate.and(toPredicate(reactWhile)); 20 | } 21 | 22 | return predicate; 23 | } 24 | 25 | private Predicate isFlowConditionTrueAndRunnerInDifferentFlow() { 26 | Predicate flowCondition = getFlowPosition().and(isRunnerInDifferentFlow()).and(isConditionTrue()); 27 | return flowCondition; 28 | } 29 | 30 | private Predicate isRunnerInDifferentFlow() { 31 | Predicate isRunnerInDifferentFlow = runner -> runner.getLatestFlow() 32 | .map(runnerFlow -> !runnerFlow.equals(getFlow())).orElse(true); 33 | return isRunnerInDifferentFlow; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute to requirements as code 2 | 3 | Do you want to report a bug, implement a new feature or support the project by creating documentation? 4 | 5 | Please create a GitHub issue, that contains at least the following information: 6 | * A clear, descriptive title 7 | * For a new feature: 8 | * What is the feature that you want to contribute? 9 | * Why do you want to contribute the feature? What is the benefit for the users of requirementsascode? 10 | * If possible: an outline of the technical solution 11 | * For a bug: 12 | * A description of the unexpected behavior, and what behavior you would have expected 13 | * A step-by-step description on how to reproduce the behavior 14 | * The requirementsascode version you used 15 | * The environment in which you found the bug (Linux or Windows, Eclipse or IntelliJ etc.) 16 | * For documentation: 17 | * What is the documentation that you want to contribute? 18 | * What is the benefit for the users of requirementsascode? 19 | 20 | Once your issue is accepted, you can suggest changes by creating a branch first, and then a pull request. 21 | 22 | Apart from the code you write, please write JUnit tests to ensure quality. 23 | 24 | Note that requirements as code core uses Gradle as a build tool. 25 | So if you can use it, please use it, as it simplifies multi project builds. 26 | This is not mandatory, though. 27 | -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/main/java/helloworld/HelloUser.java: -------------------------------------------------------------------------------- 1 | package helloworld; 2 | 3 | import java.util.function.Consumer; 4 | 5 | import org.requirementsascode.Behavior; 6 | import org.requirementsascode.BehaviorModel; 7 | import org.requirementsascode.Model; 8 | import org.requirementsascode.StatelessBehavior; 9 | 10 | public class HelloUser { 11 | public static void main(String[] args) { 12 | GreeterModel greeterModel = new GreeterModel(HelloUser::sayHello); 13 | Behavior greeter = StatelessBehavior.of(greeterModel); 14 | greeter.reactTo(new SayHelloRequest("Joe")); 15 | } 16 | 17 | private static void sayHello(SayHelloRequest requestsHello) { 18 | System.out.println("Hello, " + requestsHello.getUserName() + "."); 19 | } 20 | } 21 | 22 | class GreeterModel implements BehaviorModel { 23 | private final Consumer sayHello; 24 | 25 | public GreeterModel(Consumer sayHello) { 26 | this.sayHello = sayHello; 27 | } 28 | 29 | @Override 30 | public Model model() { 31 | Model model = Model.builder() 32 | .user(SayHelloRequest.class).system(sayHello) 33 | .build(); 34 | return model; 35 | } 36 | } 37 | 38 | class SayHelloRequest { 39 | private final String userName; 40 | 41 | public SayHelloRequest(String userName) { 42 | this.userName = userName; 43 | } 44 | 45 | public String getUserName() { 46 | return userName; 47 | } 48 | } -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/main/java/helloworld/HelloWorld03a.java: -------------------------------------------------------------------------------- 1 | package helloworld; 2 | 3 | import java.util.function.Consumer; 4 | 5 | import org.requirementsascode.AbstractActor; 6 | import org.requirementsascode.Model; 7 | 8 | import helloworld.actor.ValidUser; 9 | import helloworld.command.EnterText; 10 | import helloworld.commandhandler.GreetWithEnteredName; 11 | 12 | public class HelloWorld03a { 13 | public static void main(String[] args) { 14 | HelloWorldActor03a helloWorldActor = new HelloWorldActor03a(new GreetWithEnteredName()); 15 | ValidUser validUser03a = new ValidUser(helloWorldActor); 16 | helloWorldActor.setValidUser(validUser03a); 17 | validUser03a.run(); 18 | } 19 | } 20 | 21 | class HelloWorldActor03a extends AbstractActor { 22 | private AbstractActor validUser; 23 | private final Class entersName = EnterText.class; 24 | private final Consumer greetsUser; 25 | 26 | public HelloWorldActor03a(Consumer greetsUser) { 27 | this.greetsUser = greetsUser; 28 | } 29 | 30 | @Override 31 | protected Model behavior() { 32 | Model model = Model.builder() 33 | .useCase("Get greeted").as(validUser) 34 | .basicFlow() 35 | .step("S1").user(entersName).system(greetsUser) 36 | .build(); 37 | 38 | return model; 39 | } 40 | 41 | public void setValidUser(AbstractActor validUser) { 42 | this.validUser = validUser; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/builder/StepConditionPart.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.builder; 2 | 3 | import static org.requirementsascode.builder.StepPart.interruptableFlowStepPart; 4 | 5 | import java.util.Objects; 6 | 7 | import org.requirementsascode.Condition; 8 | import org.requirementsascode.exception.ElementAlreadyInModel; 9 | 10 | 11 | public class StepConditionPart { 12 | private FlowPart flowPart; 13 | private Condition condition; 14 | 15 | private StepConditionPart(Condition condition, FlowPart flowPart) { 16 | this.flowPart = Objects.requireNonNull(flowPart); 17 | this.condition = condition; 18 | } 19 | 20 | static StepConditionPart stepConditionPart(Condition condition, FlowPart flowPart) { 21 | return new StepConditionPart(condition, flowPart); 22 | } 23 | 24 | /** 25 | * Creates a conditional new step in this flow, with the specified name, that follows the 26 | * last step in sequence. 27 | * 28 | * @param stepName the name of the step to be created 29 | * @return the newly created step 30 | * @throws ElementAlreadyInModel if a step with the specified name already 31 | * exists in the use case 32 | */ 33 | public StepPart step(String stepName) { 34 | Objects.requireNonNull(stepName); 35 | StepPart trailingStepInFlowPart = interruptableFlowStepPart(stepName, flowPart, condition); 36 | return trailingStepInFlowPart; 37 | } 38 | } -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/flowposition/FlowPosition.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.flowposition; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.function.Predicate; 6 | 7 | import org.requirementsascode.ModelRunner; 8 | import org.requirementsascode.UseCase; 9 | 10 | public abstract class FlowPosition implements Predicate { 11 | private UseCase useCase; 12 | private List afterForEachSingleStep; 13 | 14 | protected abstract boolean isRunnerAtRightPositionFor(ModelRunner modelRunner); 15 | 16 | public FlowPosition(UseCase useCase) { 17 | this.useCase = useCase; 18 | this.afterForEachSingleStep = new ArrayList<>(); 19 | } 20 | 21 | @Override 22 | public final boolean test(ModelRunner modelRunner) { 23 | resolveSteps(); 24 | 25 | boolean isRunnerAtRightPosition = isRunnerAtRightPositionFor(modelRunner); 26 | return isRunnerAtRightPosition; 27 | } 28 | 29 | public abstract void resolveSteps(); 30 | 31 | public final UseCase getUseCase() { 32 | return useCase; 33 | } 34 | 35 | public FlowPosition orAfter(String stepName, UseCase useCase) { 36 | AfterSingleStep afterSingleStep = new AfterSingleStep(stepName, useCase); 37 | afterForEachSingleStep.add(afterSingleStep); 38 | return this; 39 | } 40 | 41 | public List getAfterForEachSingleStep() { 42 | return afterForEachSingleStep; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /requirementsascodeexamples/actor/src/main/java/actor/InteractingActorsExample.java: -------------------------------------------------------------------------------- 1 | package actor; 2 | 3 | import org.requirementsascode.AbstractActor; 4 | import org.requirementsascode.Model; 5 | 6 | import message.EnterName; 7 | import message.NameEntered; 8 | 9 | public class InteractingActorsExample { 10 | public static void main(String[] args) { 11 | AbstractActor consumer = new MessageConsumer(); 12 | AbstractActor producer = new MessageProducer(consumer); 13 | 14 | producer.reactTo(new EnterName("Joe")); 15 | } 16 | } 17 | 18 | class MessageProducer extends AbstractActor { 19 | private AbstractActor messageConsumer; 20 | 21 | public MessageProducer(AbstractActor messageConsumer) { 22 | this.messageConsumer = messageConsumer; 23 | } 24 | 25 | @Override 26 | protected Model behavior() { 27 | Model model = Model.builder() 28 | .user(EnterName.class).systemPublish(this::nameEntered).to(messageConsumer) 29 | .build(); 30 | return model; 31 | } 32 | 33 | private NameEntered nameEntered(EnterName enterName) { 34 | return new NameEntered(enterName.getUserName()); 35 | } 36 | } 37 | 38 | class MessageConsumer extends AbstractActor { 39 | @Override 40 | protected Model behavior() { 41 | Model model = Model.builder() 42 | .on(NameEntered.class).system(this::displayName) 43 | .build(); 44 | return model; 45 | } 46 | 47 | public void displayName(NameEntered nameEntered) { 48 | System.out.println("Welcome, " + nameEntered.getUserName() + "."); 49 | } 50 | } -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/flowposition/AfterSingleStep.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.flowposition; 2 | 3 | import java.util.Objects; 4 | import java.util.function.Predicate; 5 | 6 | import org.requirementsascode.FlowStep; 7 | import org.requirementsascode.ModelRunner; 8 | import org.requirementsascode.Step; 9 | import org.requirementsascode.UseCase; 10 | 11 | /** 12 | * Tests whether the specified step was the last step run. 13 | * 14 | * @author b_muth 15 | * 16 | */ 17 | public class AfterSingleStep implements Predicate{ 18 | private String stepName; 19 | private FlowStep step; 20 | private UseCase useCase; 21 | 22 | public AfterSingleStep(String stepName, UseCase useCase) { 23 | this.stepName = stepName; 24 | this.useCase = useCase; 25 | } 26 | 27 | public void resolveStep() { 28 | if (step == null) { 29 | this.step = resolveStep(stepName); 30 | } 31 | } 32 | 33 | private FlowStep resolveStep(String stepName) { 34 | FlowStep resolvedStep = null; 35 | 36 | if (useCase != null && stepName != null) { 37 | resolvedStep = (FlowStep) useCase.findStep(stepName); 38 | } 39 | 40 | return resolvedStep; 41 | } 42 | 43 | public final String getStepName() { 44 | return stepName; 45 | } 46 | 47 | @Override 48 | public boolean test(ModelRunner modelRunner) { 49 | Step latestStepRun = modelRunner.getLatestStep().orElse(null); 50 | boolean stepWasRunLast = Objects.equals(step, latestStepRun); 51 | return stepWasRunLast; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /requirementsascodeexamples/pizzavolumecalculator/src/main/java/pizzavolumecalculator/console/PizzaVolumeCalculatorConsole.java: -------------------------------------------------------------------------------- 1 | package pizzavolumecalculator.console; 2 | 3 | import java.util.Optional; 4 | import java.util.Scanner; 5 | 6 | import pizzavolumecalculator.actor.PizzaVolumeCalculator; 7 | import pizzavolumecalculator.actor.command.CalculateVolume; 8 | import pizzavolumecalculator.actor.command.EnterHeight; 9 | import pizzavolumecalculator.actor.command.EnterRadius; 10 | 11 | 12 | public class PizzaVolumeCalculatorConsole { 13 | private Scanner scanner; 14 | 15 | private PizzaVolumeCalculatorConsole(){ 16 | this.scanner = new Scanner(System.in); 17 | } 18 | 19 | private void start() { 20 | PizzaVolumeCalculator calculator = new PizzaVolumeCalculator(); 21 | 22 | System.out.print("Please enter the radius: "); 23 | calculator.reactTo(enterRadius()); 24 | 25 | System.out.print("Please enter the height: "); 26 | calculator.reactTo(enterHeight()); 27 | 28 | Optional volume = calculator.reactTo(new CalculateVolume()); 29 | 30 | System.out.println("The volume is: " + volume.get()); 31 | } 32 | 33 | public static void main(String[] args) { 34 | PizzaVolumeCalculatorConsole console = new PizzaVolumeCalculatorConsole(); 35 | console.start(); 36 | } 37 | 38 | protected EnterRadius enterRadius() { 39 | int radius = scanner.nextInt(); 40 | return new EnterRadius(radius); 41 | } 42 | 43 | protected EnterHeight enterHeight() { 44 | int height = scanner.nextInt(); 45 | return new EnterHeight(height); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /requirementsascodeexamples/creditcard_eventsourcing/src/main/java/creditcard_eventsourcing/EventsourcingApplication.java: -------------------------------------------------------------------------------- 1 | package creditcard_eventsourcing; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.UUID; 5 | 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.scheduling.annotation.EnableScheduling; 9 | import org.springframework.scheduling.annotation.Scheduled; 10 | 11 | import creditcard_eventsourcing.model.CreditCardAggregateRoot; 12 | import creditcard_eventsourcing.model.command.RequestToAssignLimit; 13 | import creditcard_eventsourcing.model.command.RequestWithdrawal; 14 | import creditcard_eventsourcing.persistence.EventStore; 15 | 16 | /** 17 | * Based on code by Jakub Pilimon: 18 | * https://gitlab.com/pilloPl/eventsourced-credit-cards/blob/4329a0aac283067f1376b3802e13f5a561f18753 19 | * 20 | */ 21 | @SpringBootApplication 22 | @EnableScheduling 23 | public class EventsourcingApplication { 24 | 25 | private final EventStore eventStore; 26 | 27 | public EventsourcingApplication(EventStore eventStore) { 28 | this.eventStore = eventStore; 29 | } 30 | 31 | public static void main(String[] args) { 32 | SpringApplication.run(EventsourcingApplication.class, args); 33 | } 34 | 35 | @Scheduled(fixedRate = 2000) 36 | public void randomCards() { 37 | CreditCardAggregateRoot cardModelRunner = new CreditCardAggregateRoot(UUID.randomUUID(), eventStore); 38 | cardModelRunner.accept(new RequestToAssignLimit(BigDecimal.TEN)); 39 | cardModelRunner.accept(new RequestWithdrawal(BigDecimal.ONE)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /requirementsascodeextract/src/main/java/org/requirementsascode/extract/freemarker/methodmodel/ActorPartOfStep.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.extract.freemarker.methodmodel; 2 | 3 | import static org.requirementsascode.extract.freemarker.methodmodel.util.Steps.getStepFromFreemarker; 4 | import static org.requirementsascode.extract.freemarker.methodmodel.util.Steps.hasNonDefaultActor; 5 | 6 | import java.util.List; 7 | 8 | import org.apache.commons.lang3.StringUtils; 9 | import org.requirementsascode.Step; 10 | 11 | import freemarker.template.SimpleScalar; 12 | import freemarker.template.TemplateMethodModelEx; 13 | import freemarker.template.TemplateModelException; 14 | 15 | public class ActorPartOfStep implements TemplateMethodModelEx { 16 | private static final String ACTOR_PREFIX = "As "; 17 | private static final String ACTOR_SEPARATOR = "/"; 18 | private static final String ACTOR_POSTFIX = ": "; 19 | 20 | @SuppressWarnings("rawtypes") 21 | @Override 22 | public Object exec(List arguments) throws TemplateModelException { 23 | if (arguments.size() != 1) { 24 | throw new TemplateModelException("Wrong number of arguments. Must be 1."); 25 | } 26 | 27 | Step step = getStepFromFreemarker(arguments.get(0)); 28 | String actors = getJoinedActors(step, ACTOR_SEPARATOR); 29 | 30 | return new SimpleScalar(actors); 31 | } 32 | 33 | private String getJoinedActors(Step step, String separator) { 34 | String actorNames = ""; 35 | if (hasNonDefaultActor(step)) { 36 | actorNames = ACTOR_PREFIX + StringUtils.join(step.getActors(), separator) + ACTOR_POSTFIX; 37 | } 38 | return actorNames; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /requirementsascodeextract/src/main/java/org/requirementsascode/extract/freemarker/methodmodel/ReactWhileOfStep.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.extract.freemarker.methodmodel; 2 | 3 | import static org.requirementsascode.extract.freemarker.methodmodel.util.Steps.getStepFromFreemarker; 4 | import static org.requirementsascode.extract.freemarker.methodmodel.util.Words.getLowerCaseWordsOfClassName; 5 | 6 | import java.util.List; 7 | 8 | import org.requirementsascode.Condition; 9 | import org.requirementsascode.FlowStep; 10 | import org.requirementsascode.Step; 11 | 12 | import freemarker.template.SimpleScalar; 13 | import freemarker.template.TemplateMethodModelEx; 14 | import freemarker.template.TemplateModelException; 15 | 16 | public class ReactWhileOfStep implements TemplateMethodModelEx { 17 | private static final String REACT_WHILE_PREFIX = "As long as "; 18 | private static final String REACT_WHILE_POSTFIX = ": "; 19 | 20 | @SuppressWarnings("rawtypes") 21 | @Override 22 | public Object exec(List arguments) throws TemplateModelException { 23 | if (arguments.size() != 1) { 24 | throw new TemplateModelException("Wrong number of arguments. Must be 1."); 25 | } 26 | 27 | Step step = getStepFromFreemarker(arguments.get(0)); 28 | 29 | String reactWhile = ""; 30 | if (step instanceof FlowStep) { 31 | Condition reactWhileCondition = ((FlowStep) step).getReactWhile(); 32 | if (reactWhileCondition != null) { 33 | reactWhile = REACT_WHILE_PREFIX + getLowerCaseWordsOfClassName(reactWhileCondition.getClass()) 34 | + REACT_WHILE_POSTFIX; 35 | } 36 | } 37 | 38 | return new SimpleScalar(reactWhile); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /requirementsascodeextract/src/main/java/org/requirementsascode/extract/freemarker/methodmodel/InCasePartOfStep.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.extract.freemarker.methodmodel; 2 | 3 | import static org.requirementsascode.extract.freemarker.methodmodel.util.Steps.getStepFromFreemarker; 4 | import static org.requirementsascode.extract.freemarker.methodmodel.util.Words.getLowerCaseWordsOfClassName; 5 | 6 | import java.util.List; 7 | 8 | import org.requirementsascode.Condition; 9 | import org.requirementsascode.Step; 10 | 11 | import freemarker.template.SimpleScalar; 12 | import freemarker.template.TemplateMethodModelEx; 13 | import freemarker.template.TemplateModelException; 14 | 15 | public class InCasePartOfStep implements TemplateMethodModelEx { 16 | private static final String IN_CASE = "In case "; 17 | 18 | @SuppressWarnings("rawtypes") 19 | @Override 20 | public Object exec(List arguments) throws TemplateModelException { 21 | if (arguments.size() != 1) { 22 | throw new TemplateModelException("Wrong number of arguments. Must be 1."); 23 | } 24 | 25 | Step step = getStepFromFreemarker(arguments.get(0)); 26 | 27 | String caseCondition = getCaseConditionOfStep(step); 28 | 29 | return new SimpleScalar(caseCondition); 30 | } 31 | 32 | private String getCaseConditionOfStep(Step step) { 33 | String caseCondition = step.getCase() 34 | .map(this::getCaseCondition) 35 | .orElse(""); 36 | 37 | return caseCondition; 38 | } 39 | 40 | private String getCaseCondition(Condition caseCondition) { 41 | String conditionWords = IN_CASE + " " + getLowerCaseWordsOfClassName(caseCondition.getClass()) + ", "; 42 | return conditionWords; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/flowposition/After.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.flowposition; 2 | 3 | import org.requirementsascode.FlowStep; 4 | import org.requirementsascode.ModelRunner; 5 | import org.requirementsascode.UseCase; 6 | 7 | /** 8 | * Tests whether the specified step was the last step run. 9 | * 10 | * @author b_muth 11 | * 12 | */ 13 | public class After extends FlowPosition { 14 | public After(String[] stepNames, UseCase useCase) { 15 | super(useCase); 16 | afterSteps(stepNames); 17 | } 18 | 19 | private void afterSteps(String[] stepNames) { 20 | for (int i = 0; i < stepNames.length; i++) { 21 | orAfter(stepNames[i], getUseCase()); 22 | } 23 | } 24 | 25 | public static After afterFlowStep(FlowStep flowStep) { 26 | UseCase useCase = flowStep == null ? null : flowStep.getUseCase(); 27 | String stepName = flowStep == null ? null : flowStep.getName(); 28 | After afterFlowStep = new After(new String[] {stepName}, useCase); 29 | return afterFlowStep; 30 | } 31 | 32 | @Override 33 | protected boolean isRunnerAtRightPositionFor(ModelRunner modelRunner) { 34 | boolean result = isAfterAnyStep(modelRunner); 35 | return result; 36 | } 37 | 38 | private boolean isAfterAnyStep(ModelRunner modelRunner) { 39 | boolean isAfterStep = false; 40 | for (AfterSingleStep afterSingleStep : getAfterForEachSingleStep()) { 41 | if (afterSingleStep.test(modelRunner)) { 42 | isAfterStep = true; 43 | break; 44 | } 45 | } 46 | return isAfterStep; 47 | } 48 | 49 | @Override 50 | public void resolveSteps() { 51 | for(AfterSingleStep afterSingleStep : getAfterForEachSingleStep()) { 52 | afterSingleStep.resolveStep(); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /requirementsascodeextract/src/main/java/org/requirementsascode/extract/freemarker/methodmodel/FlowlessCondition.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.extract.freemarker.methodmodel; 2 | 3 | import static org.requirementsascode.extract.freemarker.methodmodel.util.Steps.getStepFromFreemarker; 4 | import static org.requirementsascode.extract.freemarker.methodmodel.util.Words.getLowerCaseWordsOfClassName; 5 | 6 | import java.util.List; 7 | 8 | import org.requirementsascode.Step; 9 | 10 | import freemarker.template.SimpleScalar; 11 | import freemarker.template.TemplateMethodModelEx; 12 | import freemarker.template.TemplateModelException; 13 | 14 | public class FlowlessCondition implements TemplateMethodModelEx { 15 | private static final String WHEN = "When "; 16 | private static final String CONDITION_POSTFIX = ": "; 17 | 18 | @SuppressWarnings("rawtypes") 19 | @Override 20 | public Object exec(List arguments) throws TemplateModelException { 21 | if (arguments.size() != 1) { 22 | throw new TemplateModelException("Wrong number of arguments. Must be 1."); 23 | } 24 | 25 | Step step = getStepFromFreemarker(arguments.get(0)); 26 | 27 | String condition = getConditionWithPostfix(step); 28 | 29 | return new SimpleScalar(condition); 30 | } 31 | 32 | private String getConditionWithPostfix(Step step) { 33 | String condition = getCondition(step); 34 | String sep = condition.isEmpty() ? "" : CONDITION_POSTFIX; 35 | String conditionWithColon = condition + sep; 36 | return conditionWithColon; 37 | } 38 | 39 | private String getCondition(Step step) { 40 | String conditionWords = step.getCondition().map( 41 | condition -> (WHEN + getLowerCaseWordsOfClassName(condition.getClass()))).orElse(""); 42 | return conditionWords; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/main/java/helloworld/HelloWorld04.java: -------------------------------------------------------------------------------- 1 | package helloworld; 2 | 3 | import java.util.function.Consumer; 4 | 5 | import org.requirementsascode.AbstractActor; 6 | import org.requirementsascode.Model; 7 | 8 | import helloworld.command.EnterText; 9 | import helloworld.commandhandler.GreetPersonWithName; 10 | import helloworld.commandhandler.SaveAge; 11 | import helloworld.commandhandler.SaveName; 12 | import helloworld.domain.Person; 13 | 14 | public class HelloWorld04 { 15 | public static void main(String[] args) { 16 | Person person = new Person(); 17 | HelloWorldActor04 actor = new HelloWorldActor04(new SaveName(person), new SaveAge(person), 18 | new GreetPersonWithName(person)); 19 | actor.reactTo(new EnterText("John Q. Public")); 20 | actor.reactTo(new EnterText("43")); 21 | } 22 | } 23 | 24 | class HelloWorldActor04 extends AbstractActor { 25 | private final Class entersName = EnterText.class; 26 | private final Consumer savesName; 27 | private final Class entersAge = EnterText.class; 28 | private final Consumer savesAge; 29 | private final Runnable greetsUser; 30 | 31 | public HelloWorldActor04(Consumer savesName, Consumer savesAge, Runnable greetsUser) { 32 | this.savesName = savesName; 33 | this.savesAge = savesAge; 34 | this.greetsUser = greetsUser; 35 | } 36 | 37 | @Override 38 | protected Model behavior() { 39 | Model model = Model.builder() 40 | .useCase("Get greeted") 41 | .basicFlow() 42 | .step("S1").user(entersName).system(savesName) 43 | .step("S2").user(entersAge).system(savesAge) 44 | .step("S3").system(greetsUser) 45 | .build(); 46 | 47 | return model; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /requirementsascodecore/src/test/java/org/requirementsascode/RunStopAndRestartTest.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertFalse; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class RunStopAndRestartTest extends AbstractTestCase { 10 | 11 | @BeforeEach 12 | public void setup() throws Exception { 13 | setupWithRecordingModelRunner(); 14 | 15 | } 16 | 17 | @Test 18 | public void modelRunnerIsNotRunningAtFirst() { 19 | assertFalse(modelRunner.isRunning()); 20 | } 21 | 22 | @Test 23 | public void modelRunnerIsRunningAfterRunCall() { 24 | Model model = modelBuilder.build(); 25 | modelRunner.run(model); 26 | assertTrue(modelRunner.isRunning()); 27 | } 28 | 29 | @Test 30 | public void modelRunnerIsRunningAfterRunCallAndRestart() { 31 | Model model = modelBuilder.build(); 32 | modelRunner.run(model); 33 | modelRunner.restart(); 34 | assertTrue(modelRunner.isRunning()); 35 | } 36 | 37 | @Test 38 | public void modelRunnerIsNotRunningWhenBeingStoppedBeforeRunCall() { 39 | modelRunner.stop(); 40 | assertFalse(modelRunner.isRunning()); 41 | } 42 | 43 | @Test 44 | public void modelRunnerIsNotRunningWhenBeingStoppedAfterRunCall() { 45 | Model model = modelBuilder.build(); 46 | modelRunner.run(model); 47 | modelRunner.stop(); 48 | assertFalse(modelRunner.isRunning()); 49 | } 50 | 51 | @Test 52 | public void modelRunnerIsRunningWhenBeingStoppedAndRestartedAfterRunCall() { 53 | Model model = modelBuilder.build(); 54 | modelRunner.run(model); 55 | modelRunner.stop(); 56 | modelRunner.restart(); 57 | assertTrue(modelRunner.isRunning()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /requirementsascodeextract/src/main/java/org/requirementsascode/extract/freemarker/methodmodel/UserPartOfStep.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.extract.freemarker.methodmodel; 2 | 3 | import static org.requirementsascode.extract.freemarker.methodmodel.util.Steps.getStepFromFreemarker; 4 | import static org.requirementsascode.extract.freemarker.methodmodel.util.Steps.getUserActor; 5 | import static org.requirementsascode.extract.freemarker.methodmodel.util.Steps.hasSystemEvent; 6 | import static org.requirementsascode.extract.freemarker.methodmodel.util.Steps.hasSystemUser; 7 | import static org.requirementsascode.extract.freemarker.methodmodel.util.Words.getLowerCaseWordsOfClassName; 8 | 9 | import java.util.List; 10 | 11 | import org.requirementsascode.Step; 12 | 13 | import freemarker.template.SimpleScalar; 14 | import freemarker.template.TemplateMethodModelEx; 15 | import freemarker.template.TemplateModelException; 16 | 17 | public class UserPartOfStep implements TemplateMethodModelEx { 18 | private static final String USER_POSTFIX = "."; 19 | 20 | @SuppressWarnings("rawtypes") 21 | @Override 22 | public Object exec(List arguments) throws TemplateModelException { 23 | if (arguments.size() != 1) { 24 | throw new TemplateModelException("Wrong number of arguments. Must be 1."); 25 | } 26 | 27 | String userPartOfStep = ""; 28 | Step step = getStepFromFreemarker(arguments.get(0)); 29 | if (hasUser(step)) { 30 | String userActorName = getUserActor(step).getName(); 31 | String wordsOfUserEventClassName = getLowerCaseWordsOfClassName(step.getMessageClass()); 32 | userPartOfStep = userActorName + " " + wordsOfUserEventClassName + USER_POSTFIX; 33 | } 34 | 35 | return new SimpleScalar(userPartOfStep); 36 | } 37 | 38 | private boolean hasUser(Step step) { 39 | return !hasSystemUser(step) && !hasSystemEvent(step); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/FlowStep.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | import java.util.Objects; 4 | import java.util.Optional; 5 | 6 | import org.requirementsascode.flowposition.FlowPosition; 7 | 8 | /** 9 | * @author b_muth 10 | */ 11 | public abstract class FlowStep extends Step{ 12 | private Flow flow; 13 | private FlowPosition flowPosition; 14 | private FlowStep previousStepInFlow; 15 | private Condition reactWhile; 16 | 17 | FlowStep(String stepName, Flow flow, Condition condition) { 18 | super(stepName, flow.getUseCase(), condition); 19 | this.flow = flow; 20 | } 21 | 22 | public Flow getFlow() { 23 | return flow; 24 | } 25 | 26 | public Optional getPreviousStepInFlow() { 27 | return Optional.ofNullable(previousStepInFlow); 28 | } 29 | 30 | void setPreviousStepInFlow(FlowStep previousStepInFlow) { 31 | this.previousStepInFlow = previousStepInFlow; 32 | } 33 | 34 | public FlowPosition getFlowPosition() { 35 | return flowPosition; 36 | } 37 | 38 | void setFlowPosition(FlowPosition flowPosition) { 39 | Objects.requireNonNull(flowPosition); 40 | 41 | this.flowPosition = flowPosition; 42 | } 43 | 44 | public void orAfter(FlowStep step) { 45 | Objects.requireNonNull(step); 46 | if(flowPosition == null) { 47 | throw new RuntimeException("flowPosition must be initialized before calling this method!"); 48 | } 49 | FlowPosition flowPositionAfterStep = flowPosition.orAfter(step.getName(), step.getUseCase()); 50 | setFlowPosition(flowPositionAfterStep); 51 | } 52 | 53 | public void setReactWhile(Condition reactWhileCondition) { 54 | this.reactWhile = reactWhileCondition; 55 | createLoop(); 56 | } 57 | 58 | private void createLoop() { 59 | getFlowPosition().orAfter(getName(), getUseCase()); 60 | } 61 | 62 | public Condition getReactWhile() { 63 | return reactWhile; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/builder/StepUseCasePart.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.builder; 2 | 3 | import org.requirementsascode.AbstractActor; 4 | import org.requirementsascode.Model; 5 | import org.requirementsascode.UseCase; 6 | 7 | /** 8 | * Part used by the {@link ModelBuilder} to build a {@link Model}. 9 | * 10 | * @see UseCase 11 | * @author b_muth 12 | */ 13 | public class StepUseCasePart { 14 | private UseCasePart useCasePart; 15 | 16 | public StepUseCasePart(UseCasePart useCasePart) { 17 | this.useCasePart = useCasePart; 18 | } 19 | 20 | static StepUseCasePart stepUseCasePart(UseCasePart useCasePart) { 21 | return new StepUseCasePart(useCasePart); 22 | } 23 | 24 | /** 25 | * Start the "happy day scenario" where all is fine and dandy. 26 | * 27 | * @return the flow part to create the steps of the basic flow. 28 | */ 29 | public FlowPart basicFlow() { 30 | return useCasePart.basicFlow(); 31 | } 32 | 33 | /** 34 | * Start a flow with the specified name. 35 | * 36 | * @param flowName the name of the flow. 37 | * 38 | * @return the flow part to create the steps of the flow. 39 | */ 40 | public FlowPart flow(String flowName) { 41 | return useCasePart.flow(flowName); 42 | } 43 | 44 | /** 45 | * Define a default actor that will be used for each step of the use case, 46 | * unless it is overwritten by specific actors for the steps (with 47 | * as). 48 | * 49 | * @param defaultActor the actor to use as a default for the use case's steps 50 | * @return this use case part 51 | */ 52 | public StepUseCasePart as(AbstractActor defaultActor) { 53 | useCasePart.as(defaultActor); 54 | return this; 55 | } 56 | 57 | /** 58 | * Returns the model that has been built. 59 | * 60 | * @return the model 61 | */ 62 | public Model build() { 63 | return useCasePart.build(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /requirementsascodeextract/src/main/java/org/requirementsascode/extract/freemarker/methodmodel/util/Steps.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.extract.freemarker.methodmodel.util; 2 | 3 | import org.requirementsascode.AbstractActor; 4 | import org.requirementsascode.Flow; 5 | import org.requirementsascode.ModelRunner; 6 | import org.requirementsascode.Step; 7 | import org.requirementsascode.systemreaction.IgnoresIt; 8 | 9 | import freemarker.ext.beans.BeanModel; 10 | 11 | public class Steps { 12 | public static Class getClassFromFreemarker(Object argument) { 13 | return (Class) ((BeanModel) argument).getAdaptedObject(Class.class); 14 | } 15 | 16 | public static Step getStepFromFreemarker(Object argument) { 17 | return (Step) ((BeanModel) argument).getAdaptedObject(Step.class); 18 | } 19 | 20 | public static Flow getFlowFromFreemarker(Object argument) { 21 | return (Flow) ((BeanModel) argument).getAdaptedObject(Flow.class); 22 | } 23 | 24 | public static boolean hasNonDefaultActor(Step step) { 25 | AbstractActor firstActorOfStep = getFirstActorOfStep(step); 26 | return !getUserActor(step).equals(firstActorOfStep) && !getSystemActor(step).equals(firstActorOfStep); 27 | } 28 | 29 | public static boolean hasSystemUser(Step step) { 30 | return getSystemActor(step).equals(getFirstActorOfStep(step)); 31 | } 32 | 33 | public static boolean hasSystemEvent(Step step) { 34 | return ModelRunner.class.equals(step.getMessageClass()); 35 | } 36 | 37 | public static boolean hasSystemReaction(Step step) { 38 | return !(step.getSystemReaction().getModelObject() instanceof IgnoresIt); 39 | } 40 | 41 | public static AbstractActor getUserActor(Step step) { 42 | return step.getModel().getUserActor(); 43 | } 44 | 45 | public static AbstractActor getSystemActor(Step step) { 46 | return step.getModel().getSystemActor(); 47 | } 48 | 49 | private static AbstractActor getFirstActorOfStep(Step step) { 50 | return step.getActors()[0]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/builder/FlowConditionPart.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.builder; 2 | 3 | import java.util.Objects; 4 | 5 | import org.requirementsascode.Condition; 6 | import org.requirementsascode.Flow; 7 | import org.requirementsascode.Model; 8 | import org.requirementsascode.exception.ElementAlreadyInModel; 9 | import org.requirementsascode.flowposition.FlowPosition; 10 | 11 | import static org.requirementsascode.builder.StepPart.*; 12 | 13 | /** 14 | * Part used by the {@link ModelBuilder} to build a {@link Model}. 15 | * 16 | * @see Flow#getCondition() 17 | * @author b_muth 18 | */ 19 | public class FlowConditionPart { 20 | private FlowPart flowPart; 21 | private Condition optionalCondition; 22 | private FlowPosition optionalFlowPosition; 23 | 24 | private FlowConditionPart(Condition optionalCondition, FlowPart flowPart, FlowPosition optionalFlowPosition) { 25 | this.flowPart = Objects.requireNonNull(flowPart); 26 | this.optionalCondition = optionalCondition; 27 | this.optionalFlowPosition = optionalFlowPosition; 28 | } 29 | 30 | static FlowConditionPart flowConditionPart(Condition optionalCondition, FlowPart flowPart, FlowPosition optionalFlowPosition) { 31 | return new FlowConditionPart(optionalCondition, flowPart, optionalFlowPosition); 32 | } 33 | 34 | /** 35 | * Creates the first step of this flow. It can be run when the runner is at the 36 | * right position. 37 | * 38 | * @param stepName the name of the step to be created 39 | * @return the newly created step part, to ease creation of further steps 40 | * @throws ElementAlreadyInModel if a step with the specified name already 41 | * exists in the use case 42 | */ 43 | public StepPart step(String stepName) { 44 | StepPart stepPart = interruptingFlowStepPart(stepName, flowPart, optionalFlowPosition, getOptionalCondition()); 45 | return stepPart; 46 | } 47 | 48 | Condition getOptionalCondition() { 49 | return optionalCondition; 50 | } 51 | } -------------------------------------------------------------------------------- /requirementsascodecore/src/test/java/org/requirementsascode/testbehavior/TestCompleteTaskRequest.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.testbehavior; 2 | 3 | import java.util.UUID; 4 | 5 | public class TestCompleteTaskRequest { 6 | private final UUID todoListId; 7 | private final UUID taskId; 8 | private final String newTaskName; 9 | 10 | public TestCompleteTaskRequest(UUID todoListId, UUID taskId, String newTaskName) { 11 | this.todoListId = todoListId; 12 | this.taskId = taskId; 13 | this.newTaskName = newTaskName; 14 | } 15 | 16 | public UUID getTodoListId() { 17 | return todoListId; 18 | } 19 | 20 | public UUID getTaskId() { 21 | return taskId; 22 | } 23 | 24 | public String getNewTaskName() { 25 | return newTaskName; 26 | } 27 | 28 | @Override 29 | public int hashCode() { 30 | final int prime = 31; 31 | int result = 1; 32 | result = prime * result + ((newTaskName == null) ? 0 : newTaskName.hashCode()); 33 | result = prime * result + ((taskId == null) ? 0 : taskId.hashCode()); 34 | result = prime * result + ((todoListId == null) ? 0 : todoListId.hashCode()); 35 | return result; 36 | } 37 | 38 | @Override 39 | public boolean equals(Object obj) { 40 | if (this == obj) 41 | return true; 42 | if (obj == null) 43 | return false; 44 | if (getClass() != obj.getClass()) 45 | return false; 46 | TestCompleteTaskRequest other = (TestCompleteTaskRequest) obj; 47 | if (newTaskName == null) { 48 | if (other.newTaskName != null) 49 | return false; 50 | } else if (!newTaskName.equals(other.newTaskName)) 51 | return false; 52 | if (taskId == null) { 53 | if (other.taskId != null) 54 | return false; 55 | } else if (!taskId.equals(other.taskId)) 56 | return false; 57 | if (todoListId == null) { 58 | if (other.todoListId != null) 59 | return false; 60 | } else if (!todoListId.equals(other.todoListId)) 61 | return false; 62 | return true; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/SystemReaction.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | import java.util.Objects; 4 | import java.util.function.Consumer; 5 | import java.util.function.Function; 6 | import java.util.function.Supplier; 7 | 8 | /** 9 | * An instance of this class represents a system reaction as a function, even if it has been specified as 10 | * a Consumer or Runnable by the user of the library. This approach simplifies development of the rest of the API. 11 | * 12 | * An instance of this class also allows access to the original object (i.e. Consumer, Runnable, or Function) 13 | * specified via .system(..) or .systemPublish() by the user of the library. 14 | * That element is called modelObject. 15 | * 16 | * @author b_muth 17 | * 18 | * @param the kind of message that is the input for this system reactions 19 | */ 20 | public class SystemReaction implements Function { 21 | private Object modelObject; 22 | private Function internalFunction; 23 | 24 | SystemReaction(Consumer modelObject) { 25 | this.modelObject = Objects.requireNonNull(modelObject); 26 | 27 | Function nonPublishingReaction = message -> { 28 | modelObject.accept(message); 29 | return null; 30 | }; 31 | this.internalFunction = nonPublishingReaction; 32 | } 33 | 34 | SystemReaction(Runnable modelObject) { 35 | this((Consumer) ignoredRunner -> modelObject.run()); 36 | this.modelObject = modelObject; 37 | } 38 | 39 | SystemReaction(Supplier modelObject) { 40 | this.modelObject = Objects.requireNonNull(modelObject); 41 | 42 | Function publishingReaction = (Function) message -> modelObject.get(); 43 | this.internalFunction = publishingReaction; 44 | } 45 | 46 | SystemReaction(Function modelObject) { 47 | Objects.requireNonNull(modelObject); 48 | this.modelObject = modelObject; 49 | this.internalFunction = modelObject; 50 | } 51 | 52 | public Object getModelObject() { 53 | return modelObject; 54 | } 55 | 56 | @Override 57 | public Object apply(T message) { 58 | return internalFunction.apply(message); 59 | } 60 | } -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/ModelElementContainer.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | import java.util.Collection; 4 | import java.util.Map; 5 | import java.util.Objects; 6 | 7 | import org.requirementsascode.exception.ElementAlreadyInModel; 8 | import org.requirementsascode.exception.NoSuchElementInModel; 9 | 10 | /** 11 | * Contains static helper methods to ease the implementation of model creation 12 | * and finding elements in it, using maps. 13 | * 14 | * @author b_muth 15 | */ 16 | class ModelElementContainer { 17 | static T findModelElement(String modelElementName, 18 | Map modelElementNameToElementMap) { 19 | Objects.requireNonNull(modelElementName); 20 | Objects.requireNonNull(modelElementNameToElementMap); 21 | 22 | if (!hasModelElement(modelElementName, modelElementNameToElementMap)) { 23 | throw new NoSuchElementInModel(modelElementName); 24 | } 25 | T modelElement = modelElementNameToElementMap.get(modelElementName); 26 | return modelElement; 27 | } 28 | 29 | static boolean hasModelElement(String modelElementName, 30 | Map modelElementNameToElementMap) { 31 | Objects.requireNonNull(modelElementName); 32 | Objects.requireNonNull(modelElementNameToElementMap); 33 | 34 | boolean hasModelElement = modelElementNameToElementMap.containsKey(modelElementName); 35 | return hasModelElement; 36 | } 37 | 38 | static void saveModelElement(T modelElement, Map modelElementNameToElementMap) { 39 | Objects.requireNonNull(modelElement); 40 | Objects.requireNonNull(modelElementNameToElementMap); 41 | 42 | String modelElementName = modelElement.getName(); 43 | if (hasModelElement(modelElementName, modelElementNameToElementMap)) { 44 | throw new ElementAlreadyInModel(modelElementName); 45 | } 46 | modelElementNameToElementMap.put(modelElementName, modelElement); 47 | } 48 | 49 | static Collection getModelElements(Map modelElementNameToElementMap) { 50 | Objects.requireNonNull(modelElementNameToElementMap); 51 | 52 | return modelElementNameToElementMap.values(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/main/java/helloworld/HelloWorld07.java: -------------------------------------------------------------------------------- 1 | package helloworld; 2 | 3 | import org.requirementsascode.AbstractActor; 4 | import org.requirementsascode.Condition; 5 | import org.requirementsascode.Model; 6 | 7 | import helloworld.domain.Color; 8 | 9 | public class HelloWorld07 { 10 | public static void main(String[] args) { 11 | Color color = new Color(); 12 | HelloWorldActor07 actor = new HelloWorldActor07(color::isInputColorRed, color::isInputColorYellow, 13 | color::isInputColorGreen, color::setOutputColorToRed, color::setOutputColorToYellow, color::setOutputColorToGreen, 14 | () -> System.out.println("The output color is: " + color.getOutputColor())); 15 | actor.run(); 16 | } 17 | } 18 | 19 | class HelloWorldActor07 extends AbstractActor { 20 | private final Condition isInputColorRed; 21 | private final Condition isInputColorYellow; 22 | private final Condition isInputColorGreen; 23 | private final Runnable displayColor; 24 | private final Runnable setOutputColorToRed; 25 | private final Runnable setOutputColorToYellow; 26 | private final Runnable setOutputColorToGreen; 27 | 28 | public HelloWorldActor07(Condition isInputColorRed, Condition isInputColorYellow, Condition isInputColorGreen, 29 | Runnable setOutputColorToRed, Runnable setOutputColorToYellow, Runnable setOutputColorToGreen, Runnable displayColor) { 30 | this.isInputColorRed = isInputColorRed; 31 | this.isInputColorYellow = isInputColorYellow; 32 | this.isInputColorGreen = isInputColorGreen; 33 | this.setOutputColorToRed = setOutputColorToRed; 34 | this.setOutputColorToYellow = setOutputColorToYellow; 35 | this.setOutputColorToGreen = setOutputColorToGreen; 36 | this.displayColor = displayColor; 37 | } 38 | 39 | @Override 40 | protected Model behavior() { 41 | Model model = Model.builder() 42 | .useCase("Handle colors") 43 | .basicFlow() 44 | .step("S1").inCase(isInputColorRed).system(setOutputColorToRed) 45 | .step("S2").inCase(isInputColorYellow).system(setOutputColorToYellow) 46 | .step("S3").inCase(isInputColorGreen).system(setOutputColorToGreen) 47 | .step("S4").system(displayColor) 48 | .build(); 49 | 50 | return model; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/builder/FlowPositionPart.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.builder; 2 | 3 | import static org.requirementsascode.builder.FlowConditionPart.flowConditionPart; 4 | 5 | import java.util.Objects; 6 | 7 | import org.requirementsascode.Condition; 8 | import org.requirementsascode.exception.ElementAlreadyInModel; 9 | import org.requirementsascode.flowposition.FlowPosition; 10 | 11 | public class FlowPositionPart { 12 | private FlowPosition optionalFlowPosition; 13 | private FlowPart flowPart; 14 | private FlowConditionPart conditionPart; 15 | 16 | private FlowPositionPart(FlowPosition optionalFlowPosition, FlowPart flowPart) { 17 | this.optionalFlowPosition = optionalFlowPosition; 18 | this.flowPart = Objects.requireNonNull(flowPart); 19 | } 20 | 21 | static FlowPositionPart flowPositionPart(FlowPosition optionalFlowPosition, FlowPart flowPart) { 22 | return new FlowPositionPart(optionalFlowPosition, flowPart); 23 | } 24 | 25 | /** 26 | * Constrains the flow's condition: only if the specified condition is true as 27 | * well (beside the flow position), the flow is started. 28 | * 29 | * @param condition the condition that constrains when the flow is started 30 | * @return this condition part, to ease creation of the first step of the flow 31 | */ 32 | public FlowConditionPart condition(Condition condition) { 33 | FlowPart flowPart = getFlowPart(); 34 | FlowPosition optionalFlowPosition = getOptionalFlowPosition(); 35 | this.conditionPart = flowConditionPart(condition, flowPart, optionalFlowPosition); 36 | return conditionPart; 37 | } 38 | 39 | /** 40 | * Creates the first step of this flow. It can be run when the runner is at the 41 | * right position. 42 | * 43 | * @param stepName the name of the step to be created 44 | * @return the newly created step part, to ease creation of further steps 45 | * @throws ElementAlreadyInModel if a step with the specified name already 46 | * exists in the use case 47 | */ 48 | public StepPart step(String stepName) { 49 | return condition(null).step(stepName); 50 | } 51 | 52 | FlowPart getFlowPart() { 53 | return flowPart; 54 | } 55 | 56 | FlowPosition getOptionalFlowPosition() { 57 | return optionalFlowPosition; 58 | } 59 | } -------------------------------------------------------------------------------- /requirementsascodecore/src/test/java/org/requirementsascode/RecordingTest.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | 6 | public class RecordingTest extends AbstractTestCase { 7 | 8 | @BeforeEach 9 | public void setup() throws Exception { 10 | setupWithRecordingModelRunner(); 11 | } 12 | 13 | @Test 14 | public void recordIsEmptyForFreshlyRunModel() { 15 | Model model = modelBuilder.useCase(USE_CASE) 16 | .on(EntersText.class).system(displaysEnteredText()) 17 | .build(); 18 | 19 | modelRunner.run(model); 20 | 21 | assertRecordedStepNames(); 22 | } 23 | 24 | @Test 25 | public void recordSingleEvent() { 26 | Model model = modelBuilder.useCase(USE_CASE) 27 | .on(EntersText.class).system(displaysEnteredText()) 28 | .build(); 29 | 30 | modelRunner.run(model); 31 | modelRunner.reactTo(entersText()); 32 | 33 | assertRecordedStepNames("S1"); 34 | } 35 | 36 | @Test 37 | public void recordMultipleEvents_startRecordingAfterRunning() { 38 | Model model = modelBuilder.useCase(USE_CASE) 39 | .on(EntersText.class).system(displaysEnteredText()) 40 | .on(EntersNumber.class).system(displaysEnteredNumber()) 41 | .build(); 42 | 43 | modelRunner.run(model); 44 | modelRunner.reactTo(entersText(), entersNumber()); 45 | 46 | assertRecordedStepNames("S1","S2"); 47 | } 48 | 49 | @Test 50 | public void recordMultipleEvents() { 51 | Model model = modelBuilder.useCase(USE_CASE) 52 | .on(EntersText.class).system(displaysEnteredText()) 53 | .on(EntersNumber.class).system(displaysEnteredNumber()) 54 | .build(); 55 | 56 | modelRunner.run(model); 57 | modelRunner.reactTo(entersText(), entersNumber()); 58 | 59 | assertRecordedStepNames("S1","S2"); 60 | } 61 | 62 | @Test 63 | public void continueRecordingAfterRestart() { 64 | Model model = modelBuilder.useCase(USE_CASE) 65 | .on(EntersText.class).system(displaysEnteredText()) 66 | .on(EntersNumber.class).system(displaysEnteredNumber()) 67 | .build(); 68 | 69 | modelRunner.run(model); 70 | modelRunner.reactTo(entersText()); 71 | modelRunner.restart(); 72 | modelRunner.reactTo(entersNumber()); 73 | 74 | assertRecordedStepNames("S1","S2"); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/main/java/helloworld/HelloWorld05.java: -------------------------------------------------------------------------------- 1 | package helloworld; 2 | 3 | import java.util.function.Consumer; 4 | 5 | import org.requirementsascode.AbstractActor; 6 | import org.requirementsascode.Condition; 7 | import org.requirementsascode.Model; 8 | 9 | import helloworld.command.EnterText; 10 | import helloworld.commandhandler.GreetPersonWithName; 11 | import helloworld.commandhandler.SaveAge; 12 | import helloworld.commandhandler.SaveName; 13 | import helloworld.domain.Person; 14 | 15 | public class HelloWorld05 { 16 | public static void main(String[] args) { 17 | Person person = new Person(); 18 | HelloWorldActor05 actor = new HelloWorldActor05(new SaveName(person), new SaveAge(person), 19 | new GreetPersonWithName(person), person::ageIsOutOfBounds); 20 | actor.reactTo(new EnterText("John Q. Public")); 21 | actor.reactTo(new EnterText("43")); 22 | } 23 | } 24 | 25 | class HelloWorldActor05 extends AbstractActor { 26 | private final Class entersName = EnterText.class; 27 | private final Consumer savesName; 28 | private final Class entersAge = EnterText.class; 29 | private final Consumer savesAge; 30 | private final Runnable greetsUser; 31 | private final Condition ageIsOutOfBounds; 32 | private final Class numberFormatException = NumberFormatException.class; 33 | 34 | public HelloWorldActor05(Consumer savesName, Consumer savesAge, Runnable greetsUser, 35 | Condition ageIsOutOfBounds) { 36 | this.savesName = savesName; 37 | this.savesAge = savesAge; 38 | this.greetsUser = greetsUser; 39 | this.ageIsOutOfBounds = ageIsOutOfBounds; 40 | } 41 | 42 | @Override 43 | protected Model behavior() { 44 | Model model = Model.builder() 45 | .useCase("Get greeted") 46 | .basicFlow() 47 | .step("S1").user(entersName).system(savesName) 48 | .step("S2").user(entersAge).system(savesAge) 49 | .step("S3").system(greetsUser) 50 | .flow("Handle out-of-bounds age").insteadOf("S3").condition(ageIsOutOfBounds) 51 | .step("S3a_1").continuesAt("S2") 52 | .flow("Handle non-numerical age").after("S2") 53 | .step("S3b_1").on(numberFormatException).continuesAt("S2") 54 | .build(); 55 | 56 | return model; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /requirementsascodecore/src/test/java/org/requirementsascode/FlowWithCaseStepTest.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | import static org.junit.jupiter.api.Assertions.assertNull; 6 | 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | 10 | public class FlowWithCaseStepTest extends AbstractTestCase{ 11 | private String actualResult; 12 | 13 | @BeforeEach 14 | public void setup() { 15 | setupWithRecordingModelRunner(); 16 | } 17 | 18 | @Test 19 | public void runsTrueStepWithoutEvent() { 20 | Model model = Model.builder() 21 | .useCase("inCase Test") 22 | .basicFlow() 23 | .step("step1").inCase(() -> true).system(() -> actualResult = "step1 run") 24 | .build(); 25 | 26 | modelRunner.run(model); 27 | assertEquals("step1 run", actualResult); 28 | assertRecordedStepNames("step1"); 29 | } 30 | 31 | @Test 32 | public void runsTrueStepWithEvent() { 33 | Model model = Model.builder() 34 | .useCase("inCase Test") 35 | .basicFlow() 36 | .step("step1").on(String.class).inCase(() -> true).system(str -> actualResult = str) 37 | .build(); 38 | 39 | modelRunner.run(model).reactTo("event"); 40 | assertEquals("event", actualResult); 41 | 42 | assertRecordedStepNames("step1"); 43 | } 44 | 45 | @Test 46 | public void runsFalseStepTrueStep() { 47 | Model model = Model.builder() 48 | .useCase("inCase Test") 49 | .basicFlow() 50 | .step("step1").on(String.class).inCase(() -> false).system(str -> actualResult = str) 51 | .step("step2").on(String.class).inCase(() -> true).system(str -> actualResult = str) 52 | .build(); 53 | 54 | modelRunner.run(model).reactTo("event1", "event2"); 55 | assertEquals("event2", actualResult); 56 | 57 | assertRecordedStepNames("step2"); 58 | } 59 | 60 | @Test 61 | public void runsFalseStepFalseStep() { 62 | Model model = Model.builder() 63 | .useCase("inCase Test") 64 | .basicFlow() 65 | .step("step1").user(String.class).inCase(() -> false).system(str -> actualResult = str) 66 | .step("step2").inCase(() -> false).system(str -> actualResult = "not evaluated") 67 | .build(); 68 | 69 | modelRunner.run(model).reactTo("event1", "event2"); 70 | assertNull(actualResult); 71 | 72 | assertRecordedStepNames(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/StatelessBehavior.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | import java.util.Objects; 4 | import java.util.Optional; 5 | 6 | import org.requirementsascode.exception.InfiniteRepetition; 7 | 8 | /** 9 | * This class represents stateless behavior, that is: behavior occurring without 10 | * remembering past occurences of the same behavior. It is thus suited to be 11 | * used as a singleton e.g. in a Spring environment. 12 | * 13 | * @author b_muth 14 | * 15 | */ 16 | public class StatelessBehavior implements Behavior { 17 | private final BehaviorModel behaviorModel; 18 | private final Model model; 19 | private final Object defaultResponse; 20 | 21 | private StatelessBehavior(BehaviorModel behaviorModel) { 22 | this.behaviorModel = Objects.requireNonNull(behaviorModel, "behaviorModel must not be null!"); 23 | this.model = Objects.requireNonNull(behaviorModel.model(), "behavior must not be null!"); 24 | this.defaultResponse = behaviorModel.defaultResponse(); 25 | } 26 | 27 | /** 28 | * Creates a behavior with the specified model. 29 | * 30 | * @param behaviorModel the model describing the requests/responses of the 31 | * behavior 32 | * @return the new StatelessBehavior instance 33 | */ 34 | public static StatelessBehavior of(BehaviorModel behaviorModel) { 35 | return new StatelessBehavior(behaviorModel); 36 | } 37 | 38 | @SuppressWarnings("unchecked") 39 | public Optional reactTo(Object message) { 40 | try { 41 | final ModelRunner runner = newModelRunner().run(model); 42 | final Optional optionalResponse = runner.reactTo(message); 43 | final T response = optionalResponse.orElse((T) defaultResponse); 44 | 45 | return Optional.ofNullable(response); 46 | } catch (InfiniteRepetition e) { 47 | final String exceptionMessage = createExceptionMessage(message); 48 | throw new BehaviorException(exceptionMessage, e); 49 | } 50 | } 51 | 52 | @Override 53 | public BehaviorModel behaviorModel() { 54 | return behaviorModel; 55 | } 56 | 57 | private ModelRunner newModelRunner() { 58 | return new ModelRunner(); 59 | } 60 | 61 | private String createExceptionMessage(Object message) { 62 | final String exceptionMessage = "Request type must be different from all response types " 63 | + "(current request type: " + message.getClass().getName() + "), and there mustn't be an always true condition"; 64 | return exceptionMessage; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/main/java/cleanarchitecture/CleanArchitectureOutline.java: -------------------------------------------------------------------------------- 1 | package cleanarchitecture; 2 | 3 | import java.util.function.Consumer; 4 | 5 | import org.requirementsascode.Behavior; 6 | import org.requirementsascode.BehaviorModel; 7 | import org.requirementsascode.Model; 8 | import org.requirementsascode.StatelessBehavior; 9 | 10 | public class CleanArchitectureOutline { 11 | public static void main(String[] args) { 12 | ConsolePrinter consolePrinter = new ConsolePrinter(); 13 | GreetingServiceModel greetingServiceModel = new GreetingServiceModel(consolePrinter); 14 | Behavior greetingService = StatelessBehavior.of(greetingServiceModel); 15 | 16 | greetingService.reactTo(new SayHelloRequest("Joe")); 17 | } 18 | } 19 | 20 | /** 21 | * The behavior model defines that a consumer reacts to SayHelloRequest. 22 | * 23 | * @author b_muth 24 | * 25 | */ 26 | class GreetingServiceModel implements BehaviorModel { 27 | private final Consumer outputPort; 28 | 29 | public GreetingServiceModel(Consumer outputPort) { 30 | this.outputPort = outputPort; 31 | } 32 | 33 | @Override 34 | public Model model() { 35 | Model model = Model.builder() 36 | .user(SayHelloRequest.class).system(sayHello()) 37 | .build(); 38 | return model; 39 | } 40 | 41 | private SayHello sayHello() { 42 | return new SayHello(outputPort); 43 | } 44 | } 45 | 46 | /** 47 | * Command class 48 | */ 49 | class SayHelloRequest { 50 | private final String userName; 51 | 52 | public SayHelloRequest(String userName) { 53 | this.userName = userName; 54 | } 55 | 56 | public String getUserName() { 57 | return userName; 58 | } 59 | } 60 | 61 | /** 62 | * Message handler 63 | */ 64 | class SayHello implements Consumer { 65 | private final Consumer outputPort; 66 | 67 | public SayHello(Consumer outputPort) { 68 | this.outputPort = outputPort; 69 | } 70 | 71 | public void accept(SayHelloRequest requestHello) { 72 | String greeting = Greeting.forUser(requestHello.getUserName()); 73 | outputPort.accept(greeting); 74 | } 75 | } 76 | 77 | /** 78 | * Infrastructure class 79 | */ 80 | class ConsolePrinter implements Consumer{ 81 | public void accept(String message) { 82 | System.out.println(message); 83 | } 84 | } 85 | 86 | /** 87 | * Domain class 88 | */ 89 | class Greeting { 90 | public static String forUser(String userName) { 91 | return "Hello, " + userName + "."; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/builder/FlowlessUseCasePart.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.builder; 2 | 3 | import org.requirementsascode.Condition; 4 | import org.requirementsascode.Model; 5 | import org.requirementsascode.UseCase; 6 | 7 | /** 8 | * Part used by the {@link ModelBuilder} to build a {@link Model}. 9 | * 10 | * @see UseCase 11 | * @author b_muth 12 | */ 13 | public class FlowlessUseCasePart { 14 | private UseCasePart useCasePart; 15 | 16 | public FlowlessUseCasePart(UseCasePart useCasePart) { 17 | this.useCasePart = useCasePart; 18 | } 19 | 20 | static FlowlessUseCasePart useCasePart(UseCasePart useCasePart) { 21 | return new FlowlessUseCasePart(useCasePart); 22 | } 23 | 24 | 25 | /** 26 | * Constrains the condition for triggering a system reaction: only if the 27 | * specified condition is true, a system reaction can be triggered. 28 | * 29 | * @param condition the condition that constrains when the system reaction is 30 | * triggered 31 | * @return the created condition part 32 | */ 33 | public FlowlessConditionPart condition(Condition condition) { 34 | return useCasePart.condition(condition); 35 | } 36 | 37 | /** 38 | * Creates a named step. 39 | * 40 | * @param stepName the name of the created step 41 | * @return the created step part 42 | */ 43 | public FlowlessStepPart step(String stepName) { 44 | return useCasePart.step(stepName); 45 | } 46 | 47 | /** 48 | * Defines the type of commands that will cause a system reaction. 49 | * 50 | *

51 | * The system reacts to objects that are instances of the specified class or 52 | * instances of any direct or indirect subclass of the specified class. 53 | * 54 | * @param commandClass the class of commands the system reacts to 55 | * @param the type of the class 56 | * @return the created user part 57 | */ 58 | public FlowlessUserPart user(Class commandClass) { 59 | return useCasePart.user(commandClass); 60 | } 61 | 62 | /** 63 | * Defines the type of events or exceptions that will cause a system reaction. 64 | * 65 | *

66 | * The system reacts to objects that are instances of the specified class or 67 | * instances of any direct or indirect subclass of the specified class. 68 | * 69 | * @param eventOrExceptionClass the class of events the system reacts to 70 | * @param the type of the class 71 | * @return the created user part 72 | */ 73 | public FlowlessUserPart on(Class eventOrExceptionClass) { 74 | return useCasePart.on(eventOrExceptionClass); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/InterruptableFlowStep.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | import java.util.Collection; 4 | import java.util.List; 5 | import java.util.function.Predicate; 6 | 7 | import org.requirementsascode.flowposition.After; 8 | 9 | /** 10 | * An interruptable flow step is either the first step of a flow without a user 11 | * specified condition, or a step that is not the first step (in any flow). 12 | * 13 | * This kind of step can be "interrupted" by the first step of a different flow, 14 | * that has a user specified condition. 15 | * 16 | * So an interruptable flow step can be considered the [else] case, if no other 17 | * flow starts. 18 | * 19 | * @author b_muth 20 | */ 21 | public class InterruptableFlowStep extends FlowStep{ 22 | /** 23 | * Creates unconditional step with the specified name as the last step of the 24 | * specified flow. 25 | * 26 | * @param stepName the name of the step to be created 27 | * @param flow the flow that will contain the new step 28 | * @param optionalCondition the condition of the step, or null if it has none 29 | */ 30 | InterruptableFlowStep(String stepName, Flow flow, Condition optionalCondition) { 31 | super(stepName, flow, optionalCondition); 32 | appendToLastStepOfFlow(); 33 | } 34 | 35 | private void appendToLastStepOfFlow() { 36 | List flowSteps = getFlow().getSteps(); 37 | FlowStep lastFlowStep = flowSteps.isEmpty() ? null : flowSteps.get(flowSteps.size() - 1); 38 | setPreviousStepInFlow(lastFlowStep); 39 | setFlowPosition(After.afterFlowStep(lastFlowStep)); 40 | } 41 | 42 | @Override 43 | public Predicate getPredicate() { 44 | Condition reactWhile = getReactWhile(); 45 | 46 | Predicate predicate = getFlowPosition().and(noStepInterrupts()).and(isConditionTrue()); 47 | if (reactWhile != null) { 48 | predicate = predicate.and(toPredicate(reactWhile)); 49 | } 50 | 51 | return predicate; 52 | } 53 | 54 | private Predicate noStepInterrupts() { 55 | return modelRunner -> { 56 | Class messageClass = getMessageClass(); 57 | 58 | boolean noStepInterrupts = true; 59 | if (modelRunner.isRunning()) { 60 | Collection steps = getFlow().getModel().getModifiableSteps(); 61 | 62 | for (Step step : steps) { 63 | if(isInterruptingStep(step) && modelRunner.canReactToMessageClass(step, messageClass)) { 64 | noStepInterrupts = false; 65 | break; 66 | } 67 | } 68 | } 69 | 70 | return noStepInterrupts; 71 | }; 72 | 73 | } 74 | 75 | private boolean isInterruptingStep(Step step) { 76 | return InterruptingFlowStep.class.equals(step.getClass()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /requirementsascodeexamples/akka/src/main/java/akka/Akka.java: -------------------------------------------------------------------------------- 1 | package akka; 2 | 3 | import java.io.IOException; 4 | import java.io.Serializable; 5 | 6 | import org.requirementsascode.Model; 7 | import org.requirementsascode.ModelRunner; 8 | 9 | import akka.actor.AbstractActor; 10 | import akka.actor.ActorRef; 11 | import akka.actor.ActorSystem; 12 | import akka.actor.Props; 13 | import akka.actor.UntypedAbstractActor; 14 | 15 | public class Akka { 16 | private static final Class ASKS_FOR_HELLO_TO_USER = AsksForHelloToUser.class; 17 | private static final Class ASKS_FOR_HELLO_WORLD = AsksForHelloWorld.class; 18 | 19 | public static void main(String[] args) { 20 | ActorSystem actorSystem = ActorSystem.create("modelBasedActorSystem"); 21 | 22 | ActorRef sayHelloActor = spawn("sayHelloActor", actorSystem, SayHelloActor.class); 23 | 24 | sayHelloActor.tell(new AsksForHelloWorld(), ActorRef.noSender()); 25 | sayHelloActor.tell(new AsksForHelloToUser("Sandra"), ActorRef.noSender()); 26 | 27 | waitForReturnKeyPressed(); 28 | 29 | actorSystem.terminate(); 30 | } 31 | 32 | static ActorRef spawn(String actorName, ActorSystem system, Class actorClass) { 33 | Props props = Props.create(actorClass); 34 | return system.actorOf(props, actorName); 35 | } 36 | 37 | static class AsksForHelloWorld implements Serializable{ 38 | private static final long serialVersionUID = -8546529101337178227L; 39 | } 40 | 41 | static class AsksForHelloToUser implements Serializable { 42 | private static final long serialVersionUID = -133206036145556906L; 43 | private String name; 44 | public AsksForHelloToUser(String name) { 45 | this.name = name; 46 | } 47 | public String getName() { 48 | return name; 49 | } 50 | } 51 | 52 | static class SayHelloActor extends UntypedAbstractActor { 53 | private ModelRunner modelRunner; 54 | 55 | public SayHelloActor() { 56 | this.modelRunner = new ModelRunner().run(model()); 57 | } 58 | 59 | @Override 60 | public void onReceive(Object msg) throws Exception { 61 | modelRunner.reactTo(msg); 62 | } 63 | } 64 | static Model model() { 65 | Model model = Model.builder().useCase("Say hello to world, then user") 66 | .basicFlow() 67 | .step("S1").user(ASKS_FOR_HELLO_WORLD).system(() -> System.out.println("Hello, World!")) 68 | .step("S2").user(ASKS_FOR_HELLO_TO_USER).system(message -> System.out.println("Hello, " + message.getName() + ".")) 69 | .build(); 70 | return model; 71 | } 72 | 73 | static void waitForReturnKeyPressed() { 74 | System.out.println("Please press return key to terminate."); 75 | try { 76 | System.in.read(); 77 | } catch (IOException e) { 78 | e.printStackTrace(); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /requirementsascodeexamples/pizzavolumecalculator/src/test/java/pizzavolumecalculator/actor/PizzaVolumeCalculatorTest.java: -------------------------------------------------------------------------------- 1 | package pizzavolumecalculator.actor; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | import static org.junit.jupiter.api.Assertions.assertThrows; 6 | 7 | import java.util.Optional; 8 | 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.requirementsascode.RecordingActor; 12 | 13 | import pizzavolumecalculator.actor.command.CalculateVolume; 14 | import pizzavolumecalculator.actor.command.EnterHeight; 15 | import pizzavolumecalculator.actor.command.EnterRadius; 16 | 17 | public class PizzaVolumeCalculatorTest { 18 | private RecordingActor pizzaVolumeCalculator; 19 | 20 | @BeforeEach 21 | public void setup() { 22 | pizzaVolumeCalculator = 23 | RecordingActor.basedOn(new PizzaVolumeCalculator()); 24 | } 25 | 26 | @Test 27 | public void testBasicFlow() { 28 | pizzaVolumeCalculator.reactTo(new EnterRadius(4)); 29 | pizzaVolumeCalculator.reactTo(new EnterHeight(5)); 30 | Optional pizzaVolume = pizzaVolumeCalculator.reactTo(new CalculateVolume()); 31 | assertRecordedStepNames("S1", "S2", "S3", "S4"); 32 | 33 | assertEquals(251.327, pizzaVolume.get(), 0.01); 34 | } 35 | 36 | @Test 37 | public void testBasicFlowTwice() { 38 | reactAndAssertMessagesAreHandled(new EnterRadius(2), new EnterHeight(3), new CalculateVolume(), 39 | new EnterRadius(4), new EnterHeight(5)); 40 | Optional pizzaVolume = pizzaVolumeCalculator.reactTo(new CalculateVolume()); 41 | 42 | assertRecordedStepNames("S1", "S2", "S3", "S4", "S1", "S2", "S3", "S4"); 43 | assertEquals(251.32, pizzaVolume.get(), 0.01); 44 | } 45 | 46 | @Test 47 | public void testIllegalRadius() { 48 | assertThrows(IllegalArgumentException.class, () -> { 49 | pizzaVolumeCalculator.reactTo(new EnterRadius(-1)); 50 | }); 51 | } 52 | 53 | @Test 54 | public void testIllegalHeight() { 55 | assertThrows(IllegalArgumentException.class, () -> { 56 | pizzaVolumeCalculator.reactTo(new EnterRadius(5)); 57 | pizzaVolumeCalculator.reactTo(new EnterHeight(-1)); 58 | }); 59 | } 60 | 61 | protected void reactAndAssertMessagesAreHandled(Object... messages) { 62 | pizzaVolumeCalculator.reactTo(messages); 63 | final Object[] actualMessages = pizzaVolumeCalculator.getRecordedMessages(); 64 | assertArrayEquals(messages, actualMessages); 65 | } 66 | 67 | protected void assertRecordedStepNames(String... expectedStepNames) { 68 | String[] actualStepNames = pizzaVolumeCalculator.getRecordedStepNames(); 69 | assertArrayEquals(expectedStepNames, actualStepNames); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/RecordingActor.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Objects; 6 | import java.util.function.Consumer; 7 | 8 | public class RecordingActor extends AbstractActor { 9 | private static final Class SYSTEM_EVENT_CLASS = ModelRunner.class; 10 | 11 | private final AbstractActor baseActor; 12 | private List recordedStepNames; 13 | private List recordedMessages; 14 | 15 | private RecordingActor(AbstractActor baseActor) { 16 | this.baseActor = Objects.requireNonNull(baseActor, "baseActor must be non-null!"); 17 | startRecordingStepsAndMessages(); 18 | } 19 | 20 | private void startRecordingStepsAndMessages() { 21 | Consumer messageHandler = getModelRunner().getMessageHandler(); 22 | getModelRunner().handleWith(stepToBeRun -> { 23 | recordStepNameAndMessage(stepToBeRun.getStepName(), stepToBeRun.getMessage().orElse(null)); 24 | messageHandler.accept(stepToBeRun); 25 | }); 26 | recordedStepNames = new ArrayList<>(); 27 | recordedMessages = new ArrayList<>(); 28 | } 29 | 30 | private void recordStepNameAndMessage(String stepName, Object message) { 31 | recordedStepNames.add(stepName); 32 | if (message != null && !isSystemEvent(message)) { 33 | recordedMessages.add(message); 34 | } 35 | } 36 | 37 | private boolean isSystemEvent(T message) { 38 | return SYSTEM_EVENT_CLASS.equals(message.getClass()); 39 | } 40 | 41 | 42 | /** 43 | * Creates an actor that records the steps and messages that are handled 44 | * by the specified base actor. 45 | * 46 | * @param baseActor the actor whose steps and messages to record 47 | * @return an actor ready for recording 48 | */ 49 | public static RecordingActor basedOn(AbstractActor baseActor) { 50 | return new RecordingActor(baseActor); 51 | } 52 | 53 | @Override 54 | public ModelRunner getModelRunner() { 55 | return baseActor.getModelRunner(); 56 | } 57 | 58 | @Override 59 | protected Model behavior() { 60 | return baseActor.behavior(); 61 | } 62 | 63 | /** 64 | * Returns the names of steps that have been run, since the creation of this 65 | * actor. 66 | * 67 | * @return the step names 68 | */ 69 | public String[] getRecordedStepNames() { 70 | String[] stepNames = recordedStepNames.stream().toArray(String[]::new); 71 | return stepNames; 72 | } 73 | 74 | /** 75 | * Returns the messages that caused a step to run, since the creation of this 76 | * actor. 77 | * 78 | * @return the messages 79 | */ 80 | public Object[] getRecordedMessages() { 81 | Object[] messages = recordedMessages.toArray(); 82 | return messages; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/queue/EventQueue.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.queue; 2 | 3 | import java.util.concurrent.BlockingDeque; 4 | import java.util.concurrent.LinkedBlockingDeque; 5 | import java.util.function.Consumer; 6 | 7 | /** 8 | * A simple event queue that forwards events to an event consumer, one at a 9 | * time. 10 | * 11 | * To do that, it internally runs its own event producer thread. 12 | * 13 | * @author b_muth 14 | * 15 | */ 16 | public class EventQueue { 17 | private final BlockingDeque events; 18 | private final EventProducer eventProducer; 19 | private final Thread eventProducerThread; 20 | private final Consumer eventConsumer; 21 | 22 | /** 23 | * Create an event queue whose events will be consumed by the specified 24 | * consumer. 25 | * 26 | * @param eventConsumer the target of events 27 | */ 28 | public EventQueue(Consumer eventConsumer) { 29 | this.events = new LinkedBlockingDeque(); 30 | this.eventProducer = new EventProducer(); 31 | this.eventProducerThread = new Thread(eventProducer); 32 | this.eventConsumer = eventConsumer; 33 | eventProducerThread.start(); 34 | } 35 | 36 | /** 37 | * Puts an event in the queue, that will be provided to the consumer 38 | * (if the event queue hasn't been stopped). 39 | * 40 | * @param event the event for the queue 41 | */ 42 | public void put(Object event) { 43 | try { 44 | events.put(event); 45 | } catch (InterruptedException e) { 46 | } 47 | } 48 | 49 | /** 50 | * Stop providing events to the consumer. 51 | * 52 | * Internally, that stops the producer thread. 53 | */ 54 | public void stop() { 55 | eventProducer.stopProviding(); 56 | try { 57 | eventProducerThread.interrupt(); 58 | eventProducerThread.join(); 59 | } catch (InterruptedException e) { 60 | } 61 | } 62 | 63 | /** 64 | * Returns whether this queue is empty. 65 | * 66 | * @return true if empty, false if not 67 | */ 68 | public boolean isEmpty() { 69 | return events.isEmpty(); 70 | } 71 | 72 | public int getSize() { 73 | return events.size(); 74 | } 75 | 76 | private class EventProducer implements Runnable { 77 | private boolean isRunning = true; 78 | 79 | @Override 80 | public void run() { 81 | while (isRunning) { 82 | final Object eventObject = take(); 83 | if (eventObject != null) { 84 | consume(eventObject); 85 | } 86 | } 87 | } 88 | 89 | private void consume(Object event) { 90 | eventConsumer.accept(event); 91 | } 92 | 93 | private Object take() { 94 | Object event = null; 95 | try { 96 | event = events.take(); 97 | } catch (InterruptedException e) { 98 | stopProviding(); 99 | } 100 | return event; 101 | } 102 | 103 | private void stopProviding() { 104 | isRunning = false; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /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 Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /requirementsascodeexamples/pizzavolumecalculator/src/main/java/pizzavolumecalculator/actor/PizzaVolumeCalculator.java: -------------------------------------------------------------------------------- 1 | package pizzavolumecalculator.actor; 2 | 3 | import static java.lang.Math.PI; 4 | 5 | import java.util.function.Consumer; 6 | import java.util.function.Supplier; 7 | 8 | import org.requirementsascode.AbstractActor; 9 | import org.requirementsascode.Condition; 10 | import org.requirementsascode.Model; 11 | 12 | import pizzavolumecalculator.actor.command.CalculateVolume; 13 | import pizzavolumecalculator.actor.command.EnterHeight; 14 | import pizzavolumecalculator.actor.command.EnterRadius; 15 | 16 | public class PizzaVolumeCalculator extends AbstractActor { 17 | private int r; 18 | private int h; 19 | 20 | private Consumer saveRadius; 21 | private Consumer saveHeight; 22 | private Supplier calculateVolume; 23 | private Runnable throwIllegalRadiusException; 24 | private Runnable throwIllegalHeightException; 25 | private Condition isNegativeRadius; 26 | private Condition isNegativeHeight; 27 | 28 | public PizzaVolumeCalculator() { 29 | this.saveRadius = this::saveRadius; 30 | this.saveHeight = this::saveHeight; 31 | this.calculateVolume = this::calculateVolume; 32 | this.throwIllegalRadiusException = this::throwIllegalRadiusException; 33 | this.throwIllegalHeightException = this::throwIllegalHeightException; 34 | this.isNegativeRadius = this::isNegativeRadius; 35 | this.isNegativeHeight = this::isNegativeHeight; 36 | } 37 | 38 | @Override 39 | protected Model behavior() { 40 | Model model = Model.builder() 41 | .useCase("Calculate Pizza Volume").basicFlow() 42 | .step("S1").user(EnterRadius.class).system(saveRadius) 43 | .step("S2").user(EnterHeight.class).system(saveHeight) 44 | .step("S3").user(CalculateVolume.class).systemPublish(calculateVolume) 45 | .step("S4").continuesAt("S1") 46 | 47 | .flow("Negative radius").after("S1").condition(isNegativeRadius) 48 | .step("S1a_1").system(throwIllegalRadiusException) 49 | 50 | .flow("Negative height").after("S2").condition(isNegativeHeight) 51 | .step("S2a_1").system(throwIllegalHeightException) 52 | 53 | .build(); 54 | 55 | return model; 56 | } 57 | 58 | // system reactions 59 | private void saveRadius(EnterRadius enterRadius) { 60 | this.r = enterRadius.getRadius(); 61 | } 62 | 63 | private void saveHeight(EnterHeight enterHeight) { 64 | this.h = enterHeight.getHeight(); 65 | } 66 | 67 | private Double calculateVolume() { 68 | return PI * r * r * h; 69 | } 70 | 71 | private void throwIllegalRadiusException() { 72 | throw new IllegalArgumentException("Please specify a non-negative radius!"); 73 | } 74 | 75 | private void throwIllegalHeightException() { 76 | throw new IllegalArgumentException("Please specify a non-negative height!"); 77 | } 78 | 79 | // conditions 80 | private boolean isNegativeRadius() { 81 | return r < 0; 82 | } 83 | 84 | private boolean isNegativeHeight() { 85 | return h < 0; 86 | } 87 | } -------------------------------------------------------------------------------- /requirementsascodecore/src/test/java/org/requirementsascode/ExceptionHandlingTest.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class ExceptionHandlingTest extends AbstractTestCase{ 10 | 11 | @BeforeEach 12 | public void setup() { 13 | setupWithRecordingModelRunner(); 14 | } 15 | 16 | @Test 17 | public void doesNotHandleExceptionIfNoExceptionOccurs() { 18 | Model model = 19 | modelBuilder.useCase(USE_CASE) 20 | .basicFlow() 21 | .step(SYSTEM_DISPLAYS_TEXT).system(displaysConstantText()) 22 | .flow(ALTERNATIVE_FLOW).after(SYSTEM_DISPLAYS_TEXT) 23 | .step(SYSTEM_HANDLES_EXCEPTION).on(ArrayIndexOutOfBoundsException.class).system(e -> {}) 24 | .build(); 25 | 26 | modelRunner.run(model); 27 | 28 | assertRecordedStepNames(SYSTEM_DISPLAYS_TEXT); 29 | } 30 | 31 | @Test 32 | public void doesNotHandleExceptionIfSystemReactionDoesNotThrowException() { 33 | Model model = 34 | modelBuilder.useCase(USE_CASE) 35 | .basicFlow() 36 | .step(SYSTEM_DISPLAYS_TEXT).system(displaysConstantText()) 37 | .flow(ALTERNATIVE_FLOW).after(SYSTEM_DISPLAYS_TEXT) 38 | .step(SYSTEM_HANDLES_EXCEPTION).on(ArrayIndexOutOfBoundsException.class).system(e -> {}) 39 | .build(); 40 | 41 | modelRunner.run(model); 42 | 43 | assertRecordedStepNames(SYSTEM_DISPLAYS_TEXT); 44 | } 45 | 46 | @Test 47 | public void handlesExceptionAfterSpecificStep() { 48 | Model model = 49 | modelBuilder.useCase(USE_CASE) 50 | .basicFlow() 51 | .step(SYSTEM_THROWS_EXCEPTION).system(throwsArrayIndexOutOfBoundsException()) 52 | .flow(ALTERNATIVE_FLOW).after(SYSTEM_THROWS_EXCEPTION) 53 | .step(SYSTEM_HANDLES_EXCEPTION).on(ArrayIndexOutOfBoundsException.class).system(e -> {}) 54 | .build(); 55 | 56 | modelRunner.run(model); 57 | 58 | assertEquals(SYSTEM_HANDLES_EXCEPTION, latestStepName()); 59 | } 60 | 61 | @Test 62 | public void handlesExceptionAfterAnyOfSeveralSteps() { 63 | Model model = 64 | modelBuilder.useCase(USE_CASE) 65 | .basicFlow() 66 | .step(SYSTEM_DISPLAYS_TEXT).system(displaysConstantText()) 67 | .step(SYSTEM_THROWS_EXCEPTION).system(throwsArrayIndexOutOfBoundsException()) 68 | .flow(ALTERNATIVE_FLOW).after(SYSTEM_DISPLAYS_TEXT, SYSTEM_THROWS_EXCEPTION) 69 | .step(SYSTEM_HANDLES_EXCEPTION).on(ArrayIndexOutOfBoundsException.class).system(e -> {}).build(); 70 | 71 | modelRunner.run(model); 72 | 73 | assertEquals(SYSTEM_HANDLES_EXCEPTION, latestStepName()); 74 | } 75 | 76 | @Test 77 | public void handlesExceptionAtAnyTime() { 78 | Model model = 79 | modelBuilder.useCase(USE_CASE) 80 | .basicFlow() 81 | .step(SYSTEM_DISPLAYS_TEXT).system(displaysConstantText()) 82 | .flow(ALTERNATIVE_FLOW).after(SYSTEM_DISPLAYS_TEXT) 83 | .step(SYSTEM_THROWS_EXCEPTION).system(throwsArrayIndexOutOfBoundsException()) 84 | .flow(ALTERNATIVE_FLOW_2).anytime() 85 | .step(SYSTEM_HANDLES_EXCEPTION).on(Exception.class).system(e -> {}) 86 | .build(); 87 | 88 | modelRunner.run(model); 89 | 90 | assertRecordedStepNames(SYSTEM_DISPLAYS_TEXT, SYSTEM_THROWS_EXCEPTION, SYSTEM_HANDLES_EXCEPTION); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at conduct@requirementsascode.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/Flow.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | import java.util.Collection; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import java.util.Optional; 7 | import java.util.stream.Collectors; 8 | 9 | import org.requirementsascode.flowposition.FlowPosition; 10 | 11 | /** 12 | * A flow defines a sequence of steps that lead the user through a use case. 13 | * 14 | *

15 | * A flow either ends with the user reaching her/his goal, or terminates before, 16 | * usually because of an exception that occurred. 17 | * 18 | * @author b_muth 19 | */ 20 | public class Flow extends ModelElement{ 21 | private UseCase useCase; 22 | 23 | /** 24 | * Creates a flow with the specified name that belongs to the specified use 25 | * case. 26 | * 27 | * @param name the name of the flow to be created 28 | * @param useCase the use case that will contain the new flow 29 | */ 30 | Flow(String name, UseCase useCase) { 31 | super(name, useCase.getModel()); 32 | this.useCase = useCase; 33 | } 34 | 35 | /** 36 | * Returns the use case this flow is part of. 37 | * 38 | * @return the containing use case 39 | */ 40 | public UseCase getUseCase() { 41 | return useCase; 42 | } 43 | 44 | /** 45 | * Returns the steps contained in this flow. Do not modify the returned 46 | * collection directly. 47 | * 48 | * @return a collection of the steps 49 | */ 50 | public List getSteps() { 51 | List steps = getUseCase().getModifiableSteps().stream().filter(step -> isStepInThisFlow(step)) 52 | .map(step -> ((FlowStep)step)).collect(Collectors.toList()); 53 | return Collections.unmodifiableList(steps); 54 | } 55 | 56 | /** 57 | * Returns the first step of the flow 58 | * 59 | * @return the first step of the flow, or an empty optional if the flow has no 60 | * steps. 61 | */ 62 | public Optional getFirstStep() { 63 | Collection steps = getUseCase().getModifiableSteps(); 64 | FlowStep firstStep = null; 65 | for (Step step : steps) { 66 | if(isStepInThisFlow(step)) { 67 | firstStep = (FlowStep)step; 68 | break; 69 | } 70 | } 71 | return Optional.ofNullable(firstStep); 72 | } 73 | 74 | private boolean isStepInThisFlow(Step step) { 75 | return step instanceof FlowStep && this.equals(((FlowStep)step).getFlow()); 76 | } 77 | 78 | /** 79 | * Convenience method that returns the position of the flow (as defined e.g. by 80 | * "InsteadOf"). 81 | * 82 | *

83 | * Internally this calls the method of the same name of the first step in the 84 | * flow. 85 | * 86 | * @return the flow position, or null if the flow is empty. 87 | */ 88 | public FlowPosition getFlowPosition() { 89 | FlowPosition flowPosition = getFirstStep().map(step -> step.getFlowPosition()).orElse(null); 90 | return flowPosition; 91 | } 92 | 93 | /** 94 | * Convenience method that returns the condition of the flow. 95 | * 96 | *

97 | * Internally this calls the method of the same name of the first step in the 98 | * flow. 99 | * 100 | * @return the condition 101 | */ 102 | public Optional getCondition() { 103 | Optional condition = getFirstStep().flatMap(step -> step.getCondition()); 104 | return condition; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /requirementsascodecore/src/test/java/org/requirementsascode/BehaviorTest.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertFalse; 5 | import static org.junit.jupiter.api.Assertions.assertNotNull; 6 | import static org.junit.jupiter.api.Assertions.assertThrows; 7 | import static org.junit.jupiter.api.Assertions.assertTrue; 8 | 9 | import java.util.Optional; 10 | 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.Test; 13 | import org.requirementsascode.testbehavior.EmptyBehavior; 14 | import org.requirementsascode.testbehavior.TestAddTaskRequest; 15 | import org.requirementsascode.testbehavior.TestBehaviorModel; 16 | import org.requirementsascode.testbehavior.TestCreateListRequest; 17 | import org.requirementsascode.testbehavior.TestExceptionThrower; 18 | import org.requirementsascode.testbehavior.TestAddTaskResponse; 19 | import org.requirementsascode.testbehavior.TestCreateListResponse; 20 | 21 | public class BehaviorTest { 22 | private String receivedMessage; 23 | 24 | @BeforeEach 25 | void setup() { 26 | receivedMessage = "n/a"; 27 | } 28 | 29 | @Test 30 | void emptyBehaviorDoesntReactToMessage() { 31 | StatelessBehavior statelessBehavior = StatelessBehavior.of(new EmptyBehavior()); 32 | Optional response = statelessBehavior.reactTo("DummyStringMessage"); 33 | assertFalse(response.isPresent()); 34 | } 35 | 36 | @Test 37 | void reactsToStringMessage() { 38 | StatelessBehavior statelessBehavior = StatelessBehavior.of(new MessageFieldMutator()); 39 | 40 | final String expectedMessage = "ExpectedTestMessage"; 41 | Optional response = statelessBehavior.reactTo(expectedMessage); 42 | 43 | assertEquals(expectedMessage, receivedMessage); 44 | assertFalse(response.isPresent()); 45 | } 46 | 47 | @Test 48 | void reactsToCreateList() { 49 | StatelessBehavior statelessBehavior = StatelessBehavior.of(new TestBehaviorModel()); 50 | 51 | final TestCreateListRequest createList = new TestCreateListRequest(); 52 | final Optional optionalResponse = statelessBehavior.reactTo(createList); 53 | TestCreateListResponse todoListId = optionalResponse.get(); 54 | 55 | assertNotNull(todoListId); 56 | } 57 | 58 | @Test 59 | void reactsToAddTask() { 60 | StatelessBehavior statelessBehavior = StatelessBehavior.of(new TestBehaviorModel()); 61 | 62 | final TestAddTaskRequest addTask = new TestAddTaskRequest(); 63 | final Optional optionalResponse = statelessBehavior.reactTo(addTask); 64 | TestAddTaskResponse taskId = optionalResponse.get(); 65 | 66 | assertNotNull(taskId); 67 | } 68 | 69 | @Test 70 | void throwsExceptionIfPublishedTypeMatchesRequestType() { 71 | StatelessBehavior statelessBehavior = StatelessBehavior.of(new TestExceptionThrower()); 72 | 73 | BehaviorException actualException = assertThrows(BehaviorException.class, 74 | () -> statelessBehavior.reactTo("StringMessageThatWillTriggerException")); 75 | 76 | // Assert that exception message contains request type name 77 | final String exceptionMessage = actualException.getMessage(); 78 | assertTrue(exceptionMessage.contains(String.class.getName())); 79 | } 80 | 81 | private class MessageFieldMutator implements BehaviorModel { 82 | @Override 83 | public Model model() { 84 | return Model.builder().user(String.class).system(msg -> receivedMessage = msg).build(); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/builder/StepToPart.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.builder; 2 | 3 | import static org.requirementsascode.builder.StepPart.interruptableFlowStepPart; 4 | 5 | import java.util.Objects; 6 | 7 | import org.requirementsascode.Behavior; 8 | import org.requirementsascode.Condition; 9 | import org.requirementsascode.FlowStep; 10 | import org.requirementsascode.Model; 11 | import org.requirementsascode.exception.ElementAlreadyInModel; 12 | 13 | public class StepToPart { 14 | private StepPart stepPart; 15 | 16 | private StepToPart(StepPart stepPart, Behavior recipient) { 17 | this.stepPart = Objects.requireNonNull(stepPart); 18 | } 19 | 20 | public static StepToPart stepToPart(StepSystemPart stepSystemPart, Behavior recipient) { 21 | StepPart stepPart = stepSystemPart.getStepPart(); 22 | stepPart.getStep().setPublishTo(recipient); 23 | return new StepToPart<>(stepPart, recipient); 24 | } 25 | 26 | /** 27 | * Creates a new step in this flow, with the specified name, that follows the 28 | * current step in sequence. 29 | * 30 | * @param stepName the name of the step to be created 31 | * @return the newly created step 32 | * @throws ElementAlreadyInModel if a step with the specified name already 33 | * exists in the use case 34 | */ 35 | public StepPart step(String stepName) { 36 | Objects.requireNonNull(stepName); 37 | StepPart trailingStepInFlowPart = interruptableFlowStepPart(stepName, stepPart.getFlowPart()); 38 | return trailingStepInFlowPart; 39 | } 40 | 41 | /** 42 | * Creates a new flow in the current use case. 43 | * 44 | * @param flowName the name of the flow to be created. 45 | * @return the newly created flow part 46 | * @throws ElementAlreadyInModel if a flow with the specified name already 47 | * exists in the use case 48 | */ 49 | public FlowPart flow(String flowName) { 50 | Objects.requireNonNull(flowName); 51 | FlowPart useCaseFlowPart = stepPart.getUseCasePart().flow(flowName); 52 | return useCaseFlowPart; 53 | } 54 | 55 | /** 56 | * Creates a new use case in the current model. 57 | * 58 | * @param useCaseName the name of the use case to be created. 59 | * @return the newly created use case part 60 | * @throws ElementAlreadyInModel if a use case with the specified name already 61 | * exists in the model 62 | */ 63 | public UseCasePart useCase(String useCaseName) { 64 | Objects.requireNonNull(useCaseName); 65 | UseCasePart useCasePart = stepPart.getModelBuilder().useCase(useCaseName); 66 | return useCasePart; 67 | } 68 | 69 | /** 70 | * React to this step's message as long as the condition is fulfilled. 71 | * 72 | *

73 | * Even when the condition is fulfilled, the flow can advance given that the 74 | * message of the next step is received. 75 | * 76 | *

77 | * Note that if the condition is not fulfilled after the previous step has been 78 | * performed, the step will not react at all. 79 | * 80 | * @param reactWhileCondition the condition to check 81 | * @return the system part 82 | */ 83 | public StepToPart reactWhile(Condition reactWhileCondition) { 84 | Objects.requireNonNull(reactWhileCondition); 85 | ((FlowStep) stepPart.getStep()).setReactWhile(reactWhileCondition); 86 | return this; 87 | } 88 | 89 | /** 90 | * Returns the model built so far. 91 | * 92 | * @return the model 93 | */ 94 | public Model build() { 95 | return stepPart.getModelBuilder().build(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /requirementsascodecore/src/test/java/org/requirementsascode/CanReactToTest.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertFalse; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import java.util.Set; 8 | 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | 12 | public class CanReactToTest extends AbstractTestCase { 13 | 14 | @BeforeEach 15 | public void setup() { 16 | setupWithRecordingModelRunner(); 17 | } 18 | 19 | @Test 20 | public void cantReactIfNotRunning() { 21 | boolean canReact = modelRunner.canReactTo(entersText().getClass()); 22 | assertFalse(canReact); 23 | } 24 | 25 | @Test 26 | public void cantReactIfEventIsWrong() { 27 | Model model = modelBuilder.useCase(USE_CASE) 28 | .basicFlow() 29 | .step(CUSTOMER_ENTERS_TEXT).user(EntersText.class).system(displaysEnteredText()) 30 | .build(); 31 | 32 | modelRunner.run(model); 33 | 34 | boolean canReact = modelRunner.canReactTo(EntersNumber.class); 35 | assertFalse(canReact); 36 | } 37 | 38 | @Test 39 | public void cantReactIfConditionIsWrong() { 40 | Model model = modelBuilder.useCase(USE_CASE) 41 | .basicFlow().condition(() -> false) 42 | .step(CUSTOMER_ENTERS_TEXT).user(EntersText.class).system(displaysEnteredText()) 43 | .build(); 44 | 45 | modelRunner.run(model); 46 | 47 | boolean canReact = modelRunner.canReactTo(EntersText.class); 48 | assertFalse(canReact); 49 | } 50 | 51 | @Test 52 | public void oneStepCanReactIfEventIsRight() { 53 | Model model = modelBuilder.useCase(USE_CASE) 54 | .basicFlow() 55 | .step(CUSTOMER_ENTERS_TEXT).user(EntersText.class).system(displaysEnteredText()) 56 | .build(); 57 | 58 | modelRunner.run(model); 59 | 60 | boolean canReact = modelRunner.canReactTo(EntersText.class); 61 | assertTrue(canReact); 62 | 63 | Set stepsThatCanReact = modelRunner.getStepsThatCanReactTo(EntersText.class); 64 | assertEquals(1, stepsThatCanReact.size()); 65 | assertEquals(CUSTOMER_ENTERS_TEXT, stepsThatCanReact.iterator().next().getName()); 66 | } 67 | 68 | @Test 69 | public void oneStepCanReactIfEventIsSubClass() { 70 | Model model = modelBuilder.useCase(USE_CASE) 71 | .basicFlow() 72 | .step(CUSTOMER_ENTERS_TEXT).user(EntersText.class).system(displaysEnteredText()) 73 | .build(); 74 | 75 | modelRunner.run(model); 76 | 77 | boolean canReact = modelRunner.canReactTo(EntersTextSubClass.class); 78 | assertTrue(canReact); 79 | 80 | Set stepsThatCanReact = modelRunner.getStepsThatCanReactTo(EntersText.class); 81 | assertEquals(1, stepsThatCanReact.size()); 82 | assertEquals(CUSTOMER_ENTERS_TEXT, stepsThatCanReact.iterator().next().getName()); 83 | } 84 | 85 | private class EntersTextSubClass extends EntersText { 86 | public EntersTextSubClass(String text) { 87 | super(text); 88 | } 89 | } 90 | 91 | @Test 92 | public void moreThanOneStepCanReact() { 93 | Model model = modelBuilder.useCase(USE_CASE) 94 | .basicFlow().anytime() 95 | .step(CUSTOMER_ENTERS_TEXT).user(EntersText.class).system(displaysEnteredText()) 96 | .flow("Alternative Flow: Could react as well").anytime() 97 | .step(CUSTOMER_ENTERS_ALTERNATIVE_TEXT).user(EntersText.class).system(displaysEnteredText()) 98 | .build(); 99 | 100 | modelRunner.run(model); 101 | 102 | boolean canReact = modelRunner.canReactTo(EntersText.class); 103 | assertTrue(canReact); 104 | 105 | Set stepsThatCanReact = modelRunner.getStepsThatCanReactTo(EntersText.class); 106 | assertEquals(2, stepsThatCanReact.size()); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/Step.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | import java.util.Arrays; 4 | import java.util.Optional; 5 | import java.util.function.Consumer; 6 | import java.util.function.Function; 7 | import java.util.function.Predicate; 8 | import java.util.function.Supplier; 9 | 10 | /** 11 | * A step is a part of a use case. The steps define the behavior of the use 12 | * case. 13 | * 14 | *

15 | * A step is the core class of requirementsascode, providing all the necessary 16 | * configuration information to the {@link ModelRunner} to cause the system to 17 | * react to messages. 18 | * 19 | * @author b_muth 20 | */ 21 | public abstract class Step extends ModelElement{ 22 | private UseCase useCase; 23 | private AbstractActor[] actors; 24 | private Condition condition; 25 | private Class messageClass; 26 | private SystemReaction systemReaction; 27 | private Behavior publishTo; 28 | private Condition aCase; 29 | 30 | /** 31 | * Creates a step with the specified name that belongs to the specified use 32 | * case. 33 | * 34 | * @param useCase the use case this step belongs to 35 | * @param stepName the name of the step to be created 36 | * @param condition 37 | */ 38 | Step(String stepName, UseCase useCase, Condition condition) { 39 | super(stepName, useCase.getModel()); 40 | this.useCase = useCase; 41 | this.condition = condition; 42 | } 43 | 44 | public abstract Predicate getPredicate(); 45 | 46 | public UseCase getUseCase() { 47 | return useCase; 48 | } 49 | 50 | public Optional getCondition() { 51 | return Optional.ofNullable(condition); 52 | } 53 | 54 | protected Predicate isConditionTrue() { 55 | Condition conditionOrElseTrue = getCondition().orElse(() -> true); 56 | Predicate flowCondition = toPredicate(conditionOrElseTrue); 57 | return flowCondition; 58 | } 59 | 60 | public AbstractActor[] getActors() { 61 | final AbstractActor[] actorsCopy = Arrays.copyOf(actors, actors.length); 62 | return actorsCopy; 63 | } 64 | 65 | public void setActors(AbstractActor[] actors) { 66 | this.actors = Arrays.copyOf(actors, actors.length); 67 | } 68 | 69 | public Class getMessageClass() { 70 | return messageClass; 71 | } 72 | 73 | public void setMessageClass(Class eventClass) { 74 | this.messageClass = eventClass; 75 | } 76 | 77 | public SystemReaction getSystemReaction() { 78 | return systemReaction; 79 | } 80 | 81 | public void setSystemReaction(Runnable systemReaction) { 82 | this.systemReaction = new SystemReaction<>(systemReaction); 83 | } 84 | 85 | public void setSystemReaction(Consumer systemReaction) { 86 | this.systemReaction = new SystemReaction<>(systemReaction); 87 | } 88 | 89 | public void setSystemReaction(Function systemReaction) { 90 | this.systemReaction = new SystemReaction<>(systemReaction); 91 | } 92 | 93 | public void setSystemReaction(Supplier systemReaction) { 94 | this.systemReaction = new SystemReaction<>(systemReaction); 95 | } 96 | 97 | protected static Predicate toPredicate(Condition condition) { 98 | return modelRunner -> condition.evaluate(); 99 | } 100 | 101 | public Optional getPublishTo() { 102 | return Optional.ofNullable(publishTo); 103 | } 104 | 105 | public void setPublishTo(Behavior recipient) { 106 | this.publishTo = recipient; 107 | } 108 | 109 | public void setCase(Condition aCase) { 110 | this.aCase = aCase; 111 | } 112 | 113 | public Optional getCase(){ 114 | return Optional.ofNullable(aCase); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /requirementsascodecore/src/test/java/org/requirementsascode/IncludesTest.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | 8 | public class IncludesTest extends AbstractTestCase{ 9 | protected static final String INCLUDED_USE_CASE = "Included use case"; 10 | protected static final String SYSTEM_INCLUDES_USE_CASE = "Step that includes use case"; 11 | 12 | @BeforeEach 13 | public void setup() { 14 | setupWithRecordingModelRunner(); 15 | } 16 | 17 | @Test 18 | public void includesUseCaseWithBasicFlowAtFirstStep_withCallButNoReturn() { 19 | Model fishLevelModel = Model.builder() 20 | .useCase(USE_CASE) 21 | .basicFlow() 22 | .step(CUSTOMER_ENTERS_NUMBER).user(EntersNumber.class).system(displaysEnteredNumber()) 23 | .build(); 24 | 25 | Model seaLevelModel = Model.builder() 26 | .useCase(INCLUDED_USE_CASE) 27 | .basicFlow() 28 | .step(SYSTEM_INCLUDES_USE_CASE).user(EntersNumber.class).systemPublish(en -> en) 29 | .build(); 30 | 31 | ModelRunner fishLevelModelRunner = new ModelRunner().run(fishLevelModel); 32 | ModelRunner seaLevelModelRunner = new ModelRunner().publishWith(event -> fishLevelModelRunner.reactTo(event)); 33 | seaLevelModelRunner.run(seaLevelModel).reactTo(entersNumber(), entersNumber()); 34 | 35 | assertEquals(CUSTOMER_ENTERS_NUMBER, fishLevelModelRunner.getLatestStep().get().getName()); 36 | assertEquals(SYSTEM_INCLUDES_USE_CASE, seaLevelModelRunner.getLatestStep().get().getName()); 37 | } 38 | 39 | @Test 40 | public void includesUseCaseWithBasicFlowAtFirstStep_withCallAndReturn() { 41 | Model fishLevelModel = Model.builder() 42 | .useCase(INCLUDED_USE_CASE) 43 | .basicFlow().anytime() 44 | .step(CUSTOMER_ENTERS_NUMBER).user(EntersNumber.class).system(displaysEnteredNumber()) 45 | .build(); 46 | 47 | Model seaLevelModel = Model.builder() 48 | .useCase(USE_CASE).basicFlow() 49 | .step(SYSTEM_INCLUDES_USE_CASE).user(EntersNumber.class).systemPublish(en -> en) 50 | .step(SYSTEM_DISPLAYS_TEXT).system(displaysConstantText()) 51 | .build(); 52 | 53 | ModelRunner fishLevelModelRunner = new ModelRunner().run(fishLevelModel); 54 | ModelRunner seaLevelModelRunner = new ModelRunner().publishWith(event -> fishLevelModelRunner.reactTo(event)); 55 | seaLevelModelRunner.run(seaLevelModel).reactTo(entersNumber(), entersNumber()); 56 | 57 | assertEquals(CUSTOMER_ENTERS_NUMBER, fishLevelModelRunner.getLatestStep().get().getName()); 58 | assertEquals(SYSTEM_DISPLAYS_TEXT, seaLevelModelRunner.getLatestStep().get().getName()); 59 | } 60 | 61 | @Test 62 | public void includesUseCaseWithBasicFlowAtFirstStep_withoutEventInIncludingUseCase() { 63 | Model fishLevelModel = Model.builder() 64 | .useCase(INCLUDED_USE_CASE) 65 | .basicFlow().anytime() 66 | .step(CUSTOMER_ENTERS_NUMBER).user(EntersNumber.class).system(displaysEnteredNumber()) 67 | .build(); 68 | 69 | Model seaLevelModel = Model.builder() 70 | .useCase(USE_CASE).basicFlow() 71 | .step(SYSTEM_INCLUDES_USE_CASE).systemPublish(() -> entersNumber()) 72 | .step(SYSTEM_DISPLAYS_TEXT).system(displaysConstantText()) 73 | .build(); 74 | 75 | ModelRunner fishLevelModelRunner = new ModelRunner().run(fishLevelModel); 76 | ModelRunner seaLevelModelRunner = new ModelRunner().publishWith(event -> fishLevelModelRunner.reactTo(event)); 77 | seaLevelModelRunner.run(seaLevelModel).reactTo(entersNumber(), entersNumber()); 78 | 79 | assertEquals(CUSTOMER_ENTERS_NUMBER, fishLevelModelRunner.getLatestStep().get().getName()); 80 | assertEquals(SYSTEM_DISPLAYS_TEXT, seaLevelModelRunner.getLatestStep().get().getName()); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/StepToBeRun.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | import java.util.Optional; 4 | import java.util.function.Consumer; 5 | import java.util.function.Function; 6 | 7 | /** 8 | * Use an instance of this class if you want to find out the details about the 9 | * step to be run in a custom message handler, and to trigger the system reaction. 10 | * 11 | * @see ModelRunner#handleWith(Consumer) 12 | * @author b_muth 13 | */ 14 | public class StepToBeRun{ 15 | private Step step; 16 | private Object message; 17 | private Object messageToBePublished; 18 | 19 | StepToBeRun() { 20 | } 21 | 22 | /** 23 | * Triggers the system reaction of this step. 24 | * 25 | * @return the message returned by the system reaction that will be published 26 | * after the handleWith() method completes. 27 | */ 28 | public Object run() { 29 | messageToBePublished = runSystemReactionOfStep(); 30 | return messageToBePublished; 31 | } 32 | 33 | private Object runSystemReactionOfStep() { 34 | @SuppressWarnings("unchecked") 35 | Function systemReactionFunction = (Function) step.getSystemReaction(); 36 | setMessageToBePublished(systemReactionFunction.apply(message)); 37 | return messageToBePublished; 38 | } 39 | 40 | /** 41 | * Returns the message that was returned by the latest system reaction. 42 | * 43 | * @return an optional containing the message, or an empty optional if there was none. 44 | */ 45 | public Optional getMessageToBePublished(){ 46 | return Optional.ofNullable(messageToBePublished); 47 | } 48 | 49 | /** 50 | * Alter the message to be published after the handleWith() method completes. 51 | * IMPORTANT: Call this method after {@link #run()}. 52 | * 53 | * @param messageToBePublished the message to be published 54 | */ 55 | public void setMessageToBePublished(Object messageToBePublished) { 56 | this.messageToBePublished = messageToBePublished; 57 | } 58 | 59 | /** 60 | * Returns the name of the step whose system reaction is performed when 61 | * {@link #run()} is called. 62 | * 63 | * @return the step name. 64 | */ 65 | public String getStepName() { 66 | return step.getName(); 67 | } 68 | 69 | /** 70 | * Returns the precondition that needs to be true to trigger the system reaction 71 | * when {@link #run()} is called. 72 | * 73 | * @return the condition, or an empty optional when no condition was specified. 74 | */ 75 | public Optional getCondition() { 76 | Optional optionalCondition = step.getCondition(); 77 | return optionalCondition; 78 | } 79 | 80 | /** 81 | * Returns the message object that will be passed to the system reaction when 82 | * {@link #run()} is called. 83 | * 84 | * @return the message, or an empty optional when no message was specified. 85 | */ 86 | public Optional getMessage() { 87 | Optional optionalMessage = null; 88 | if (message instanceof ModelRunner) { 89 | optionalMessage = Optional.empty(); 90 | } else { 91 | optionalMessage = Optional.of(message); 92 | } 93 | return optionalMessage; 94 | } 95 | 96 | /** 97 | * Returns the system reaction to be executed when {@link #run()} is called. 98 | * 99 | * @return the system reaction object, as specified in the model. 100 | */ 101 | public Object getSystemReaction() { 102 | Object systemReactionObject = step.getSystemReaction().getModelObject(); 103 | return systemReactionObject; 104 | } 105 | 106 | void setupWith(Step useCaseStep, Object message) { 107 | this.step = useCaseStep; 108 | this.message = message; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /requirementsascodeextract/src/main/java/org/requirementsascode/extract/freemarker/FreeMarkerEngine.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.extract.freemarker; 2 | 3 | import java.io.Writer; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | import org.requirementsascode.Model; 8 | import org.requirementsascode.extract.freemarker.methodmodel.ActorPartOfStep; 9 | import org.requirementsascode.extract.freemarker.methodmodel.FlowCondition; 10 | import org.requirementsascode.extract.freemarker.methodmodel.FlowlessCondition; 11 | import org.requirementsascode.extract.freemarker.methodmodel.InCasePartOfStep; 12 | import org.requirementsascode.extract.freemarker.methodmodel.ReactWhileOfStep; 13 | import org.requirementsascode.extract.freemarker.methodmodel.SystemPartOfStep; 14 | import org.requirementsascode.extract.freemarker.methodmodel.UserPartOfStep; 15 | 16 | import freemarker.template.Configuration; 17 | import freemarker.template.Template; 18 | import freemarker.template.TemplateExceptionHandler; 19 | 20 | public class FreeMarkerEngine { 21 | private Map dataModel; 22 | private Configuration cfg; 23 | 24 | /** 25 | * Creates an engine for generating documentation, based on a 26 | * requirements as code core Model and using the FreeMarker template engine. 27 | * 28 | * @param basePackagePath package path in your classpath, where your FreeMarker 29 | * templates are located. 30 | */ 31 | public FreeMarkerEngine(String basePackagePath) { 32 | createConfiguration(basePackagePath); 33 | putFreemarkerMethodsInDataModel(); 34 | } 35 | 36 | private void createConfiguration(String basePackagePath) { 37 | cfg = new Configuration(Configuration.VERSION_2_3_26); 38 | cfg.setClassLoaderForTemplateLoading(getClass().getClassLoader(), basePackagePath); 39 | cfg.setLogTemplateExceptions(false); 40 | setDefaultEncoding("UTF-8"); 41 | setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); 42 | } 43 | 44 | private void putFreemarkerMethodsInDataModel() { 45 | dataModel = new HashMap(); 46 | put("flowCondition", new FlowCondition()); 47 | put("flowlessCondition", new FlowlessCondition()); 48 | put("actorPartOfStep", new ActorPartOfStep()); 49 | put("userPartOfStep", new UserPartOfStep()); 50 | put("inCasePartOfStep", new InCasePartOfStep()); 51 | put("systemPartOfStep", new SystemPartOfStep()); 52 | put("reactWhileOfStep", new ReactWhileOfStep()); 53 | } 54 | 55 | public void put(String key, Object value) { 56 | dataModel.put(key, value); 57 | } 58 | 59 | public void setDefaultEncoding(String encoding) { 60 | cfg.setDefaultEncoding(encoding); 61 | } 62 | 63 | public void setTemplateExceptionHandler(TemplateExceptionHandler handler) { 64 | cfg.setTemplateExceptionHandler(handler); 65 | } 66 | 67 | /** 68 | * 'Extracts' the use cases from the model. This is done by putting the 69 | * specified models in the FreeMarker configuration under the name 'model'. 70 | * Then, the specified template is used to transform the model to text and write 71 | * it using the specified writer. 72 | * 73 | * @param model the input model, created with requirementsascodecore 74 | * @param templateFileName name of the template file, relative to the base class 75 | * path (when constructing the engine) 76 | * @param outputWriter the writer that writes out the resulting text 77 | * @throws Exception if anything goes wrong 78 | */ 79 | public void extract(Model model, String templateFileName, Writer outputWriter) throws Exception { 80 | put("model", model); 81 | Template template = cfg.getTemplate(templateFileName); 82 | template.process(dataModel, outputWriter); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /requirementsascodeexamples/helloworld/src/main/java/helloworld/HelloWorld06.java: -------------------------------------------------------------------------------- 1 | package helloworld; 2 | 3 | import java.util.function.Consumer; 4 | 5 | import org.requirementsascode.AbstractActor; 6 | import org.requirementsascode.Condition; 7 | import org.requirementsascode.Model; 8 | 9 | import helloworld.actor.AnonymousUser; 10 | import helloworld.actor.NormalUser; 11 | import helloworld.command.EnterText; 12 | import helloworld.commandhandler.GreetPersonWithAge; 13 | import helloworld.commandhandler.GreetPersonWithName; 14 | import helloworld.commandhandler.SaveAge; 15 | import helloworld.commandhandler.SaveName; 16 | import helloworld.domain.Person; 17 | 18 | public class HelloWorld06 { 19 | public static void main(String[] args) { 20 | Person person = new Person(); 21 | HelloWorldActor06 helloWorldActor = new HelloWorldActor06(new SaveName(person), new SaveAge(person), new GreetPersonWithName(person), 22 | new GreetPersonWithAge(person), person::ageIsOk, person::ageIsOutOfBounds); 23 | 24 | NormalUser normalUser = new NormalUser(helloWorldActor, "Jane", "21"); 25 | AnonymousUser anonymousUser = new AnonymousUser(helloWorldActor, "43"); 26 | helloWorldActor.setNormalUser(normalUser); 27 | helloWorldActor.setAnonymousUser(anonymousUser); 28 | 29 | normalUser.run(); 30 | } 31 | } 32 | 33 | class HelloWorldActor06 extends AbstractActor { 34 | private final Class entersName = EnterText.class; 35 | private final Consumer savesName; 36 | private final Class entersAge = EnterText.class; 37 | private final Consumer savesAge; 38 | private final Runnable greetsUserWithName; 39 | private final Runnable greetsUserWithAge; 40 | private final Condition ageIsOk; 41 | private final Condition ageIsOutOfBounds; 42 | private final Class numberFormatException = NumberFormatException.class; 43 | 44 | private AbstractActor normalUser; 45 | private AbstractActor anonymousUser; 46 | 47 | public HelloWorldActor06(Consumer savesName, Consumer savesAge, Runnable greetsUserWithName, 48 | Runnable greetsUserWithAge, Condition ageIsOk, Condition ageIsOutOfBounds) { 49 | this.savesName = savesName; 50 | this.savesAge = savesAge; 51 | this.greetsUserWithName = greetsUserWithName; 52 | this.greetsUserWithAge = greetsUserWithAge; 53 | this.ageIsOk = ageIsOk; 54 | this.ageIsOutOfBounds = ageIsOutOfBounds; 55 | } 56 | 57 | @Override 58 | protected Model behavior() { 59 | Model model = Model.builder() 60 | .useCase("Get greeted") 61 | .basicFlow() 62 | .step("S1").as(normalUser).user(entersName).system(savesName) 63 | .step("S2").as(normalUser, anonymousUser).user(entersAge).system(savesAge) 64 | .step("S3").as(normalUser).system(greetsUserWithName) 65 | .step("S4").as(normalUser, anonymousUser).system(greetsUserWithAge) 66 | .flow("Handle out-of-bounds age").after("S2").condition(ageIsOutOfBounds) 67 | .step("S3a_1").continuesAt("S2") 68 | .flow("Handle non-numerical age").anytime() 69 | .step("S3b_1").on(numberFormatException).continuesAt("S2") 70 | .flow("Anonymous greeted with age only").after("S2").condition(ageIsOk) 71 | .step("S3c_1").as(anonymousUser).continuesAt("S4") 72 | .flow("Anonymous does not enter name").insteadOf("S1") 73 | .step("S1a_1").as(anonymousUser).user(entersAge).system(savesAge) 74 | .step("S1a_2").continuesAfter("S2") 75 | .build(); 76 | 77 | return model; 78 | } 79 | 80 | public void setNormalUser(AbstractActor normalUser) { 81 | this.normalUser = normalUser; 82 | } 83 | 84 | public void setAnonymousUser(AbstractActor anonymousUser) { 85 | this.anonymousUser = anonymousUser; 86 | } 87 | } -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/builder/FlowlessToPart.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.builder; 2 | 3 | import static org.requirementsascode.builder.FlowlessConditionPart.flowlessConditionPart; 4 | 5 | import java.util.Objects; 6 | 7 | import org.requirementsascode.Behavior; 8 | import org.requirementsascode.Condition; 9 | import org.requirementsascode.Model; 10 | 11 | public class FlowlessToPart { 12 | private UseCasePart useCasePart; 13 | private long flowlessStepCounter; 14 | 15 | private FlowlessToPart(UseCasePart useCasePart, long flowlessStepCounter) { 16 | this.useCasePart = Objects.requireNonNull(useCasePart); 17 | this.flowlessStepCounter = flowlessStepCounter; 18 | } 19 | 20 | public static FlowlessToPart flowlessToPart(StepSystemPart stepSystemPart, Behavior recipient, long flowlessStepCounter) { 21 | UseCasePart useCasePart = stepSystemPart.getStepPart().getUseCasePart(); 22 | stepSystemPart.to(recipient); 23 | return new FlowlessToPart(useCasePart, flowlessStepCounter); 24 | } 25 | 26 | /** 27 | * Constrains the condition for triggering a system reaction: only if the 28 | * specified condition is true, a system reaction can be triggered. 29 | * 30 | * @param condition the condition that constrains when the system reaction is 31 | * triggered 32 | * @return the created condition part 33 | */ 34 | public FlowlessConditionPart condition(Condition condition) { 35 | FlowlessConditionPart conditionPart = flowlessConditionPart(condition, useCasePart, ++flowlessStepCounter); 36 | return conditionPart; 37 | } 38 | 39 | /** 40 | * Creates a named step. 41 | * 42 | * @param stepName the name of the created step 43 | * @return the created step part 44 | */ 45 | public FlowlessStepPart step(String stepName) { 46 | Objects.requireNonNull(stepName); 47 | FlowlessStepPart stepPart = condition(null).step(stepName); 48 | return stepPart; 49 | } 50 | 51 | /** 52 | * Defines the type of commands that will cause a system reaction. 53 | * 54 | *

55 | * The system reacts to objects that are instances of the specified class or 56 | * instances of any direct or indirect subclass of the specified class. 57 | * 58 | * @param commandClass the class of commands the system reacts to 59 | * @param the type of the class 60 | * @return the created user part 61 | */ 62 | public FlowlessUserPart user(Class commandClass) { 63 | Objects.requireNonNull(commandClass); 64 | FlowlessUserPart flowlessUserPart = condition(null).user(commandClass); 65 | return flowlessUserPart; 66 | } 67 | 68 | /** 69 | * Defines the type of messages or exceptions that will cause a system reaction. 70 | * 71 | *

72 | * The system reacts to objects that are instances of the specified class or 73 | * instances of any direct or indirect subclass of the specified class. 74 | * 75 | * @param messageClass the class of messages the system reacts to 76 | * @param the type of the class 77 | * @return the created user part 78 | */ 79 | public FlowlessUserPart on(Class messageClass) { 80 | Objects.requireNonNull(messageClass); 81 | FlowlessUserPart flowlessUserPart = condition(null).on(messageClass); 82 | return flowlessUserPart; 83 | } 84 | 85 | /** 86 | * Creates a new use case in the current model, and returns a part for building 87 | * its details. If a use case with the specified name already exists, returns a 88 | * part for the existing use case. 89 | * 90 | * @param useCaseName the name of the existing use case / use case to be 91 | * created. 92 | * @return the created / found use case's part. 93 | */ 94 | public UseCasePart useCase(String useCaseName) { 95 | Objects.requireNonNull(useCaseName); 96 | UseCasePart newUseCasePart = useCasePart.getModelBuilder().useCase(useCaseName); 97 | return newUseCasePart; 98 | } 99 | 100 | /** 101 | * Returns the model that has been built. 102 | * 103 | * @return the model 104 | */ 105 | public Model build() { 106 | return useCasePart.build(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/builder/FlowlessUserPart.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.builder; 2 | 3 | import static org.requirementsascode.builder.FlowlessSystemPart.flowlessSystemPartWithConsumer; 4 | import static org.requirementsascode.builder.FlowlessSystemPart.flowlessSystemPartWithFunction; 5 | import static org.requirementsascode.builder.FlowlessSystemPart.flowlessSystemPartWithRunnable; 6 | import static org.requirementsascode.builder.FlowlessSystemPart.flowlessSystemPartWithSupplier; 7 | 8 | import java.util.Objects; 9 | import java.util.function.Consumer; 10 | import java.util.function.Function; 11 | import java.util.function.Supplier; 12 | 13 | import org.requirementsascode.Model; 14 | import org.requirementsascode.ModelRunner; 15 | 16 | /** 17 | * Part used by the {@link ModelBuilder} to build a {@link Model}. Wraps 18 | * {@link StepUserPart}. 19 | * 20 | * @author b_muth 21 | */ 22 | public class FlowlessUserPart { 23 | private StepUserPart stepUserPart; 24 | private long flowlessStepCounter; 25 | 26 | private FlowlessUserPart(StepUserPart stepUserPart, long flowlessStepCounter) { 27 | this.stepUserPart = Objects.requireNonNull(stepUserPart); 28 | this.flowlessStepCounter = flowlessStepCounter; 29 | } 30 | 31 | static FlowlessUserPart flowlessUserPart(Class commandClass, StepPart stepPart, long flowlessStepCounter) { 32 | StepUserPart stepUserPart = stepPart.user(commandClass); 33 | return new FlowlessUserPart<>(stepUserPart, flowlessStepCounter); 34 | } 35 | 36 | static FlowlessUserPart flowlessOnPart(Class eventClass, StepPart stepPart, long flowlessStepCounter) { 37 | StepUserPart stepUserPart = stepPart.on(eventClass); 38 | return new FlowlessUserPart<>(stepUserPart, flowlessStepCounter); 39 | } 40 | 41 | /** 42 | * Defines the system reaction. The system will react as specified to the 43 | * message passed in, when {@link ModelRunner#reactTo(Object)} is called. 44 | * 45 | * @param systemReaction the specified system reaction 46 | * @return the created flowless system part 47 | */ 48 | public FlowlessSystemPart system(Consumer systemReaction) { 49 | FlowlessSystemPart flowlessSystemPart = flowlessSystemPartWithConsumer(stepUserPart, systemReaction, 50 | flowlessStepCounter); 51 | return flowlessSystemPart; 52 | } 53 | 54 | /** 55 | * Defines the system reaction. The system will react as specified, but it will 56 | * ignore the message passed in, when {@link ModelRunner#reactTo(Object)} is 57 | * called. 58 | * 59 | * @param systemReaction the specified system reaction 60 | * @return the created flowless system part 61 | */ 62 | public FlowlessSystemPart system(Runnable systemReaction) { 63 | FlowlessSystemPart flowlessSystemPart = flowlessSystemPartWithRunnable(stepUserPart, systemReaction, 64 | flowlessStepCounter); 65 | return flowlessSystemPart; 66 | } 67 | 68 | /** 69 | * Defines the system reaction. The system will react as specified to the 70 | * message passed in, when you call {@link ModelRunner#reactTo(Object)}. After 71 | * executing the system reaction, the runner will publish the returned event. 72 | * 73 | * @param systemReaction the specified system reaction, that returns an event to 74 | * be published. 75 | * @return the created flowless system part 76 | */ 77 | public FlowlessSystemPart systemPublish(Function systemReaction) { 78 | FlowlessSystemPart flowlessSystemPart = flowlessSystemPartWithFunction(stepUserPart, systemReaction, 79 | flowlessStepCounter); 80 | return flowlessSystemPart; 81 | } 82 | 83 | /** 84 | * Defines the system reaction. After executing the system reaction, the runner 85 | * will publish the returned event. 86 | * 87 | * @param systemReaction the specified system reaction, that returns an event to 88 | * be published. 89 | * @return the created flowless system part 90 | */ 91 | public FlowlessSystemPart systemPublish(Supplier systemReaction) { 92 | FlowlessSystemPart flowlessSystemPart = flowlessSystemPartWithSupplier(stepUserPart, systemReaction, 93 | flowlessStepCounter); 94 | return flowlessSystemPart; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /requirementsascodecore/src/main/java/org/requirementsascode/builder/FlowlessStepPart.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.builder; 2 | 3 | import static org.requirementsascode.builder.FlowlessUserPart.flowlessOnPart; 4 | import static org.requirementsascode.builder.FlowlessUserPart.flowlessUserPart; 5 | import static org.requirementsascode.builder.StepPart.stepPartWithoutFlow; 6 | 7 | import java.util.function.Supplier; 8 | 9 | import org.requirementsascode.AbstractActor; 10 | import org.requirementsascode.Condition; 11 | import org.requirementsascode.ModelRunner; 12 | 13 | public class FlowlessStepPart { 14 | private final StepPart stepPart; 15 | private final long flowlessStepCounter; 16 | 17 | private FlowlessStepPart(String stepName, UseCasePart useCasePart, Condition optionalCondition, long flowlessStepCounter) { 18 | this.stepPart = stepPartWithoutFlow(stepName, useCasePart, optionalCondition); 19 | this.flowlessStepCounter = flowlessStepCounter; 20 | setDefaultActorAsActor(useCasePart); 21 | } 22 | 23 | private void setDefaultActorAsActor(UseCasePart useCasePart) { 24 | stepPart.getStep().setActors(new AbstractActor[] {useCasePart.getDefaultActor()}); 25 | } 26 | 27 | static FlowlessStepPart flowlessStepPart(String stepName, UseCasePart useCasePart, Condition optionalCondition, long flowlessStepCounter) { 28 | return new FlowlessStepPart(stepName, useCasePart, optionalCondition, flowlessStepCounter); 29 | } 30 | 31 | /** 32 | * Defines the type of user commands that this step accepts. Commands of this 33 | * type can cause a system reaction. 34 | * 35 | *

36 | * Given that the step's condition is true, the system reacts to objects that 37 | * are instances of the specified class or instances of any direct or indirect 38 | * subclass of the specified class. 39 | * 40 | * @param commandClass the class of commands the system reacts to in this step 41 | * @param the type of the class 42 | * @return the created user part of this step 43 | */ 44 | public FlowlessUserPart user(Class commandClass) { 45 | FlowlessUserPart flowlessUserPart = flowlessUserPart(commandClass, stepPart, flowlessStepCounter); 46 | return flowlessUserPart; 47 | } 48 | 49 | /** 50 | * Defines the type of system event objects or exceptions that this step 51 | * handles. Events of the specified type can cause a system reaction. 52 | * 53 | *

54 | * Given that the step's condition is true, the system reacts to objects that 55 | * are instances of the specified class or instances of any direct or indirect 56 | * subclass of the specified class. 57 | * 58 | * @param messageClass the class of messages the system reacts to 59 | * @param the type of the class 60 | * @return the created user part of this step 61 | */ 62 | public FlowlessUserPart on(Class messageClass) { 63 | FlowlessUserPart flowlessUserPart = flowlessOnPart(messageClass, stepPart, flowlessStepCounter); 64 | return flowlessUserPart; 65 | } 66 | 67 | /** 68 | * Defines an "autonomous system reaction", meaning the system will react 69 | * without needing a message provided via {@link ModelRunner#reactTo(Object)}. 70 | * 71 | * @param systemReaction the autonomous system reaction 72 | * @return the created system part of this step 73 | */ 74 | public FlowlessSystemPart system(Runnable systemReaction) { 75 | FlowlessSystemPart flowlessSystemPart = user(ModelRunner.class).system(systemReaction); 76 | return flowlessSystemPart; 77 | } 78 | 79 | /** 80 | * Defines an "autonomous system reaction", meaning the system will react 81 | * without needing a message provided via {@link ModelRunner#reactTo(Object)}. 82 | * After executing the system reaction, the runner will publish the returned 83 | * event. 84 | * 85 | * @param systemReaction the autonomous system reaction, that returns a single 86 | * event to be published. 87 | * @return the created system part of this step 88 | */ 89 | public FlowlessSystemPart systemPublish(Supplier systemReaction) { 90 | FlowlessSystemPart flowlessSystemPart = user(ModelRunner.class).systemPublish(systemReaction); 91 | return flowlessSystemPart; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /requirementsascodeextract/src/main/java/org/requirementsascode/extract/freemarker/methodmodel/FlowCondition.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.extract.freemarker.methodmodel; 2 | 3 | import static org.requirementsascode.extract.freemarker.methodmodel.util.Steps.getFlowFromFreemarker; 4 | import static org.requirementsascode.extract.freemarker.methodmodel.util.Words.getLowerCaseWordsOfClassName; 5 | 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.requirementsascode.Flow; 11 | import org.requirementsascode.flowposition.After; 12 | import org.requirementsascode.flowposition.AfterSingleStep; 13 | import org.requirementsascode.flowposition.FlowPosition; 14 | import org.requirementsascode.flowposition.InsteadOf; 15 | 16 | import freemarker.template.SimpleScalar; 17 | import freemarker.template.TemplateMethodModelEx; 18 | import freemarker.template.TemplateModelException; 19 | 20 | public class FlowCondition implements TemplateMethodModelEx { 21 | private static final String AFTER_PREFIX = "after "; 22 | private static final String WHEN = "when "; 23 | private static final String PREDICATE_SEPARATOR = ", "; 24 | private static final String PREDICATE_POSTFIX = ": "; 25 | 26 | @SuppressWarnings("rawtypes") 27 | @Override 28 | public Object exec(List arguments) throws TemplateModelException { 29 | if (arguments.size() != 1) { 30 | throw new TemplateModelException("Wrong number of arguments. Must be 1."); 31 | } 32 | 33 | Flow flow = getFlowFromFreemarker(arguments.get(0)); 34 | 35 | String flowPredicate = getFlowPredicate(flow); 36 | 37 | return new SimpleScalar(flowPredicate); 38 | } 39 | 40 | private String getFlowPredicate(Flow flow) { 41 | String predicate = getFlowPosition(flow) + getFlowPredicateSeparator(flow, PREDICATE_SEPARATOR) 42 | + getCondition(flow); 43 | String sep = predicate.isEmpty() ? "" : PREDICATE_POSTFIX; 44 | String capitalizedPredicateWithColon = StringUtils.capitalize(predicate) + sep; 45 | return capitalizedPredicateWithColon; 46 | } 47 | 48 | private String getFlowPosition(Flow flow) { 49 | FlowPosition flowPosition = flow.getFlowPosition(); 50 | 51 | String flowPositionWords = ""; 52 | if(flowPosition instanceof InsteadOf) { 53 | InsteadOf insteadOf = (InsteadOf)flowPosition; 54 | String stepName = insteadOf.getStepName(); 55 | flowPositionWords = flowPositionToWords(flowPosition, stepName); 56 | } else if(flowPosition instanceof After) { 57 | After after = (After)flowPosition; 58 | 59 | String afterStepNames = 60 | after.getAfterForEachSingleStep().stream() 61 | .map(AfterSingleStep::getStepName) 62 | .collect(Collectors.joining(",")); 63 | 64 | flowPositionWords = isFlowWithoutFlowPosition(afterStepNames)? "" : AFTER_PREFIX + afterStepNames; 65 | } 66 | 67 | 68 | return flowPositionWords; 69 | } 70 | 71 | protected boolean isFlowWithoutFlowPosition(String afterStepNames) { 72 | return afterStepNames.startsWith("null"); 73 | } 74 | 75 | private String flowPositionToWords(FlowPosition flowPosition, String stepName) { 76 | String result = ""; 77 | if (flowPosition != null) { 78 | boolean isNonDefaultFlowPosition = isNonDefaultFlowCondition(flowPosition, stepName); 79 | if (isNonDefaultFlowPosition) { 80 | String flowPositionWords = getLowerCaseWordsOfClassName(flowPosition.getClass()); 81 | String flowPositionWithStepName = flowPositionWords + " " + stepName; 82 | result = flowPositionWithStepName.trim(); 83 | } 84 | } 85 | return result; 86 | } 87 | 88 | boolean isNonDefaultFlowCondition(FlowPosition flowPosition, String stepName) { 89 | return !After.class.equals(flowPosition.getClass()) || stepName != null; 90 | } 91 | 92 | private String getFlowPredicateSeparator(Flow flow, String sep) { 93 | String flowPosition = getFlowPosition(flow); 94 | String result = ""; 95 | if (!flowPosition.isEmpty() && !getCondition(flow).isEmpty()) { 96 | result = sep; 97 | } 98 | return result; 99 | } 100 | 101 | private String getCondition(Flow flow) { 102 | String conditionWords = flow.getCondition().map( 103 | condition -> (WHEN + getLowerCaseWordsOfClassName(condition.getClass()))).orElse(""); 104 | return conditionWords; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /requirementsascodeexamples/creditcard_eventsourcing/src/main/java/creditcard_eventsourcing/model/CreditCard.java: -------------------------------------------------------------------------------- 1 | package creditcard_eventsourcing.model; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.Optional; 7 | import java.util.UUID; 8 | 9 | import org.requirementsascode.Model; 10 | import org.requirementsascode.ModelRunner; 11 | import org.requirementsascode.Step; 12 | 13 | import creditcard_eventsourcing.model.event.CardRepaid; 14 | import creditcard_eventsourcing.model.event.CardWithdrawn; 15 | import creditcard_eventsourcing.model.event.CycleClosed; 16 | import creditcard_eventsourcing.model.event.DomainEvent; 17 | import creditcard_eventsourcing.model.event.LimitAssigned; 18 | 19 | /** 20 | * Based on code by Jakub Pilimon: 21 | * https://gitlab.com/pilloPl/eventsourced-credit-cards/blob/4329a0aac283067f1376b3802e13f5a561f18753 22 | * 23 | */ 24 | class CreditCard 25 | { 26 | static final String assigningLimit = "Assigning limit"; 27 | static final String assigningLimitTwice = "Assigning limit twice"; 28 | static final String withdrawingCard = "Withdrawing card"; 29 | static final String withdrawingCardAgain = "Withdrawing card again"; 30 | static final String withdrawingCardTooOften = "Withdrawing card too often"; 31 | static final String closingCycle = "Closing cycle"; 32 | static final String repaying = "Repaying"; 33 | static final String repeating = "Repeating"; 34 | 35 | private BigDecimal initialLimit; 36 | private BigDecimal usedLimit = BigDecimal.ZERO; 37 | private int withdrawals; 38 | 39 | private final UUID uuid; 40 | private final Model eventHandlingModel; 41 | private List pendingEvents = new ArrayList<>(); 42 | private ModelRunner modelRunner; 43 | 44 | public CreditCard(UUID uuid, List events) { 45 | this.uuid = uuid; 46 | this.eventHandlingModel = buildModel(); 47 | this.modelRunner = new ModelRunner().run(eventHandlingModel); 48 | replay(uuid, events); 49 | } 50 | 51 | /* 52 | * UUID 53 | */ 54 | public UUID uuid() { 55 | return uuid; 56 | } 57 | 58 | /** 59 | * Builds a model that maps received events to method calls 60 | * 61 | * @return the event to method call mapping model 62 | */ 63 | private Model buildModel() { 64 | return Model.builder() 65 | .step(assigningLimit).on(LimitAssigned.class).system(event -> assignLimit(event.getAmount())) 66 | .step(withdrawingCard).on(CardWithdrawn.class).system(event -> withdraw(event.getAmount())) 67 | .step(repaying).on(CardRepaid.class).system(event -> repay(event.getAmount())) 68 | .step(closingCycle).on(CycleClosed.class).system(event -> closeCycle()) 69 | .build(); 70 | } 71 | 72 | /* 73 | * State changing methods 74 | */ 75 | 76 | private void assignLimit(BigDecimal amount) { 77 | this.initialLimit = amount; 78 | } 79 | 80 | private void withdraw(BigDecimal amount) { 81 | this.usedLimit = usedLimit.add(amount); 82 | withdrawals++; 83 | } 84 | 85 | private void repay(BigDecimal amount) { 86 | usedLimit = usedLimit.subtract(amount); 87 | } 88 | 89 | private void closeCycle() { 90 | withdrawals = 0; 91 | } 92 | 93 | 94 | /* 95 | * Validation methods 96 | */ 97 | boolean notEnoughMoneyToWithdraw(BigDecimal amount) { 98 | return getAvailableLimit().compareTo(amount) < 0; 99 | } 100 | 101 | public BigDecimal getAvailableLimit() { 102 | return initialLimit.subtract(usedLimit); 103 | } 104 | 105 | /* 106 | * Conditions 107 | */ 108 | 109 | boolean tooManyWithdrawalsInCycle() { 110 | return withdrawals >= 45; 111 | } 112 | 113 | boolean isLimitAlreadyAssigned() { 114 | return initialLimit != null; 115 | } 116 | 117 | boolean isAccountOpen() { 118 | return true; 119 | } 120 | 121 | /* 122 | * Event sourcing methods 123 | */ 124 | public List pendingEvents() { 125 | return pendingEvents; 126 | } 127 | 128 | private void replay(UUID uuid, List events) { 129 | events.forEach(this::mutate); 130 | } 131 | 132 | private void mutate(DomainEvent event) { 133 | modelRunner.reactTo(event); 134 | } 135 | 136 | void apply(DomainEvent event) { 137 | modelRunner.reactTo(event); 138 | pendingEvents.add(event); 139 | } 140 | 141 | public void flushEvents() { 142 | pendingEvents.clear(); 143 | } 144 | 145 | Optional latestStep() { 146 | Optional latestStep = modelRunner.getLatestStep(); 147 | return latestStep; 148 | } 149 | } -------------------------------------------------------------------------------- /requirementsascodeextract/src/main/java/org/requirementsascode/extract/freemarker/methodmodel/SystemPartOfStep.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode.extract.freemarker.methodmodel; 2 | 3 | import static org.requirementsascode.extract.freemarker.methodmodel.util.Steps.getStepFromFreemarker; 4 | import static org.requirementsascode.extract.freemarker.methodmodel.util.Steps.getSystemActor; 5 | import static org.requirementsascode.extract.freemarker.methodmodel.util.Steps.hasSystemEvent; 6 | import static org.requirementsascode.extract.freemarker.methodmodel.util.Steps.hasSystemReaction; 7 | import static org.requirementsascode.extract.freemarker.methodmodel.util.Steps.hasSystemUser; 8 | import static org.requirementsascode.extract.freemarker.methodmodel.util.Words.getLowerCaseWordsOfClassName; 9 | 10 | import java.util.List; 11 | import java.util.Optional; 12 | import java.util.function.Function; 13 | 14 | import org.requirementsascode.AbstractActor; 15 | import org.requirementsascode.Behavior; 16 | import org.requirementsascode.Step; 17 | import org.requirementsascode.systemreaction.AbstractContinues; 18 | 19 | import freemarker.template.SimpleScalar; 20 | import freemarker.template.TemplateMethodModelEx; 21 | import freemarker.template.TemplateModelException; 22 | 23 | public class SystemPartOfStep implements TemplateMethodModelEx { 24 | private static final String ON_PREFIX = "On "; 25 | private static final String ON_POSTFIX = ": "; 26 | private static final String SYSTEM_POSTFIX = "."; 27 | 28 | @SuppressWarnings("rawtypes") 29 | @Override 30 | public Object exec(List arguments) throws TemplateModelException { 31 | if (arguments.size() != 1) { 32 | throw new TemplateModelException("Wrong number of arguments. Must be 1."); 33 | } 34 | 35 | Step step = getStepFromFreemarker(arguments.get(0)); 36 | 37 | String systemPartOfStep = getSystemPartOfStep(step); 38 | 39 | return new SimpleScalar(systemPartOfStep); 40 | } 41 | 42 | private String getSystemPartOfStep(Step step) { 43 | String systemPartOfStep = ""; 44 | if (hasSystemReaction(step)) { 45 | String on = getOn(step); 46 | String systemActorName = getSystemActor(step).getName(); 47 | String wordsOfSystemReactionClassName = getWordsOfSystemReactionClassName(step); 48 | String systemPublishString = getSystemPublishString(step); 49 | String publishToActorString = getPublishToActorString(step); 50 | String stepName = getStepName(step); 51 | systemPartOfStep = on + systemActorName + " " + systemPublishString + wordsOfSystemReactionClassName 52 | + publishToActorString + stepName + SYSTEM_POSTFIX; 53 | } 54 | return systemPartOfStep; 55 | } 56 | 57 | private String getOn(Step step) { 58 | String on = ""; 59 | 60 | if (hasSystemUser(step) && !hasSystemEvent(step)) { 61 | on = ON_PREFIX + step.getMessageClass().getSimpleName() + ON_POSTFIX; 62 | } 63 | return on; 64 | } 65 | 66 | private String getSystemPublishString(Step step) { 67 | Object systemReaction = step.getSystemReaction().getModelObject(); 68 | String systemPublishString = systemReaction instanceof Function ? "publishes " : ""; 69 | return systemPublishString; 70 | } 71 | 72 | private String getPublishToActorString(Step step) { 73 | Optional optionalPublishToBehavior = step.getPublishTo(); 74 | String publishToString = optionalPublishToBehavior 75 | .map(act -> " to " + nameOf(act)) 76 | .orElse(""); 77 | return publishToString; 78 | } 79 | 80 | private String nameOf(Behavior behavior) { 81 | final String name = behavior instanceof AbstractActor ? ((AbstractActor) behavior).getName() 82 | : behavior.getClass().getSimpleName(); 83 | return name; 84 | } 85 | 86 | private String getWordsOfSystemReactionClassName(Step step) { 87 | Object systemReaction = step.getSystemReaction().getModelObject(); 88 | Class systemReactionClass = systemReaction.getClass(); 89 | String wordsOfClassName = getLowerCaseWordsOfClassName(systemReactionClass); 90 | return wordsOfClassName; 91 | } 92 | 93 | private String getStepName(Step step) { 94 | String stepName = ""; 95 | if (hasSystemReaction(step)) { 96 | Object systemReaction = step.getSystemReaction().getModelObject(); 97 | if (systemReaction instanceof AbstractContinues) { 98 | stepName = " " + ((AbstractContinues) systemReaction).getStepName(); 99 | } 100 | } 101 | return stepName; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /requirementsascodecore/src/test/java/org/requirementsascode/ReactToTypesTest.java: -------------------------------------------------------------------------------- 1 | package org.requirementsascode; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import java.util.Set; 7 | 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | 11 | public class ReactToTypesTest extends AbstractTestCase { 12 | 13 | @BeforeEach 14 | public void setup() { 15 | setupWithRecordingModelRunner(); 16 | } 17 | 18 | @Test 19 | public void noEventsReactedToIfNotRunning() { 20 | Set> reactToTypes = modelRunner.getReactToTypes(); 21 | assertEquals(0, reactToTypes.size()); 22 | } 23 | 24 | @Test 25 | public void noEventsReactedToInEmptyModel() { 26 | Model model = modelBuilder.useCase(USE_CASE).build(); 27 | 28 | modelRunner.run(model); 29 | 30 | Set> reactToTypes = modelRunner.getReactToTypes(); 31 | assertEquals(0, reactToTypes.size()); 32 | } 33 | 34 | @Test 35 | public void singleEventTypeReactedTo() { 36 | Model model = modelBuilder.useCase(USE_CASE).basicFlow().step(CUSTOMER_ENTERS_TEXT).user(EntersText.class).system( 37 | displaysEnteredText()).build(); 38 | 39 | modelRunner.run(model); 40 | 41 | Set> reactToTypes = modelRunner.getReactToTypes(); 42 | assertEquals(1, reactToTypes.size()); 43 | 44 | Class eventTypeReactedTo = reactToTypes.iterator().next(); 45 | assertEquals(EntersText.class, eventTypeReactedTo); 46 | } 47 | 48 | @Test 49 | public void sameEventTypeReactedTo() { 50 | Model model = modelBuilder.useCase(USE_CASE).basicFlow().anytime().step(CUSTOMER_ENTERS_TEXT).user( 51 | EntersText.class).system(displaysEnteredText()).flow("Alternative Flow: Could react as well").anytime().step( 52 | CUSTOMER_ENTERS_ALTERNATIVE_TEXT).user(EntersText.class).system(displaysEnteredText()).build(); 53 | 54 | modelRunner.run(model); 55 | 56 | Set> reactToTypes = modelRunner.getReactToTypes(); 57 | assertEquals(1, reactToTypes.size()); 58 | 59 | Class eventTypeReactedTo = reactToTypes.iterator().next(); 60 | assertEquals(EntersText.class, eventTypeReactedTo); 61 | } 62 | 63 | @Test 64 | public void differentEventTypesReactedTo() { 65 | Model model = modelBuilder.useCase(USE_CASE).basicFlow().anytime().step(CUSTOMER_ENTERS_TEXT).user( 66 | EntersText.class).system(displaysEnteredText()).flow("Alternative Flow: Could react as well").anytime().step( 67 | CUSTOMER_ENTERS_NUMBER).user(EntersNumber.class).system(displaysEnteredNumber()).build(); 68 | 69 | modelRunner.run(model); 70 | 71 | Set> reactToTypes = modelRunner.getReactToTypes(); 72 | assertEquals(2, reactToTypes.size()); 73 | 74 | assertTrue(reactToTypes.contains(EntersText.class)); 75 | assertTrue(reactToTypes.contains(EntersNumber.class)); 76 | } 77 | 78 | @Test 79 | public void eventTypesReactedOnlyIfConditionFulfilled() { 80 | Model model = modelBuilder.useCase(USE_CASE).basicFlow().condition(() -> false).step(CUSTOMER_ENTERS_TEXT).user( 81 | EntersText.class).system(displaysEnteredText()).flow("Alternative Flow: Could react as well").anytime().step( 82 | CUSTOMER_ENTERS_NUMBER).user(EntersNumber.class).system(displaysEnteredNumber()).build(); 83 | 84 | modelRunner.run(model); 85 | 86 | Set> reactToTypes = modelRunner.getReactToTypes(); 87 | assertEquals(1, reactToTypes.size()); 88 | 89 | Class eventTypeReactedTo = reactToTypes.iterator().next(); 90 | assertEquals(EntersNumber.class, eventTypeReactedTo); 91 | } 92 | 93 | @Test 94 | public void eventTypesReactedOnlyIfFlowPositionIsRight() { 95 | Model model = modelBuilder.useCase(USE_CASE).basicFlow().step(CUSTOMER_ENTERS_TEXT).user(EntersText.class).system( 96 | displaysEnteredText()).flow("Alternative Flow: Could react as well").after(CUSTOMER_ENTERS_TEXT).step( 97 | CUSTOMER_ENTERS_NUMBER).user(EntersNumber.class).system(displaysEnteredNumber()).build(); 98 | 99 | modelRunner.run(model); 100 | 101 | Set> reactToTypes = modelRunner.getReactToTypes(); 102 | assertEquals(1, reactToTypes.size()); 103 | 104 | Class eventTypeReactedTo = reactToTypes.iterator().next(); 105 | assertEquals(EntersText.class, eventTypeReactedTo); 106 | 107 | modelRunner.reactTo(entersText()); 108 | 109 | reactToTypes = modelRunner.getReactToTypes(); 110 | assertEquals(1, reactToTypes.size()); 111 | 112 | eventTypeReactedTo = reactToTypes.iterator().next(); 113 | assertEquals(EntersNumber.class, eventTypeReactedTo); 114 | } 115 | } 116 | --------------------------------------------------------------------------------