diff --git a/build.gradle b/build.gradle index 84d45bdd..691577ca 100644 --- a/build.gradle +++ b/build.gradle @@ -608,7 +608,7 @@ configure(rootProject) { stylesheet: 'spring.css', 'linkcss': true, 'icons': 'font', - 'sectanchors': '', + 'sectanchors': '', // use provided highlighter 'source-highlighter=highlight.js', 'highlightjsdir=js/highlight', @@ -635,6 +635,7 @@ configure(rootProject) { from 'spring-statemachine-samples/washer/src/main/java/' from 'spring-statemachine-samples/tasks/src/main/java/' from 'spring-statemachine-samples/turnstile/src/main/java/' + from 'spring-statemachine-samples/turnstilereactive/src/main/java/' from 'spring-statemachine-samples/showcase/src/main/java/' from 'spring-statemachine-samples/cdplayer/src/main/java/' from 'spring-statemachine-samples/persist/src/main/java/' diff --git a/docs/src/reference/asciidoc/sm-examples-turnstilereactive.adoc b/docs/src/reference/asciidoc/sm-examples-turnstilereactive.adoc new file mode 100644 index 00000000..8d8f2fcf --- /dev/null +++ b/docs/src/reference/asciidoc/sm-examples-turnstilereactive.adoc @@ -0,0 +1,123 @@ +[[statemachine-examples-turnstilereactive]] +== Turnstile Reactive + +Turnstile reactive is an enhacement to <> sample using +same _StateMachine_ concept and adding a reactive web layer communicating reactively with +a _StateMachine_ reactive interfaces. + +`StateMachineController` is a simple `@RestController` where we autowire our `StateMachine`. + +==== +[source,java,indent=0] +---- +include::samples/demo/turnstilereactive/StateMachineController.java[tags=snippetA] +---- +==== + +We create first mapping to return a machine state. As state doesn't come out from +a machine reactively, we can _defer_ it so that when a returned `Mono` is subscribed, +actual state is requested. + +==== +[source,java,indent=0] +---- +include::samples/demo/turnstilereactive/StateMachineController.java[tags=snippetB] +---- +==== + +To send a single event or multiple events to a machine we can use a `Flux` in both +incoming and outgoing layers. `EventResult` here is just for this sample and simply +wraps `ResultType` and event. + +==== +[source,java,indent=0] +---- +include::samples/demo/turnstilereactive/StateMachineController.java[tags=snippetC] +---- +==== + +You can use the following command to run the sample: + +==== +[source,text,subs="verbatim,attributes"] +---- +$ java -jar spring-statemachine-samples-turnstilereactive-{revnumber}.jar +---- +==== + +Example of getting a state: +==== +[source,bash] +---- +GET http://localhost:8080/state +---- +==== + +Would then response: +==== +[source,text] +---- +"LOCKED" +---- +==== + +Example of sending an event: +==== +[source,bash] +---- +POST http://localhost:8080/events +content-type: application/json + +{ + "event": "COIN" +} +---- +==== + +Would then response: +==== +[source,json] +---- +[ + { + "event": "COIN", + "resultType": "ACCEPTED" + } +] +---- +==== + +You can post multiple events: +==== +[source,bash] +---- +POST http://localhost:8080/events +content-type: application/json + +[ + { + "event": "COIN" + }, + { + "event": "PUSH" + } +] +---- +==== + +Response then contains results for both events: +==== +[source,json] +---- +[ + { + "event": "COIN", + "resultType": "ACCEPTED" + }, + { + "event": "PUSH", + "resultType": "ACCEPTED" + } +] +---- +==== diff --git a/docs/src/reference/asciidoc/sm-examples.adoc b/docs/src/reference/asciidoc/sm-examples.adoc index 23d9faa1..cf4de004 100644 --- a/docs/src/reference/asciidoc/sm-examples.adoc +++ b/docs/src/reference/asciidoc/sm-examples.adoc @@ -12,6 +12,8 @@ normal build cycle. This chapter includes the following samples: <> +<> + <> <> @@ -63,6 +65,7 @@ build of this document, meaning that, if you build samples from master, you have files with a `BUILD-SNAPSHOT` postfix. include::sm-examples-turnstile.adoc[] +include::sm-examples-turnstilereactive.adoc[] include::sm-examples-showcase.adoc[] include::sm-examples-cdplayer.adoc[] include::sm-examples-tasks.adoc[] diff --git a/spring-statemachine-samples/turnstilereactive/src/main/java/demo/turnstilereactive/StateMachineController.java b/spring-statemachine-samples/turnstilereactive/src/main/java/demo/turnstilereactive/StateMachineController.java index 9c2aaf50..aabe2fa0 100644 --- a/spring-statemachine-samples/turnstilereactive/src/main/java/demo/turnstilereactive/StateMachineController.java +++ b/spring-statemachine-samples/turnstilereactive/src/main/java/demo/turnstilereactive/StateMachineController.java @@ -18,6 +18,7 @@ package demo.turnstilereactive; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.support.MessageBuilder; import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.StateMachineEventResult; import org.springframework.statemachine.StateMachineEventResult.ResultType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -32,22 +33,28 @@ import reactor.core.publisher.Mono; @RestController public class StateMachineController { + //tag::snippetA[] @Autowired private StateMachine stateMachine; + //end::snippetA[] + //tag::snippetB[] @GetMapping("/state") public Mono state() { - return Mono.justOrEmpty(stateMachine.getState().getId()); + return Mono.defer(() -> Mono.justOrEmpty(stateMachine.getState().getId())); } + //end::snippetB[] - @PostMapping("/event") - public Flux event(@RequestBody Mono eventData) { + //tag::snippetC[] + @PostMapping("/events") + public Flux events(@RequestBody Flux eventData) { return eventData .filter(ed -> ed.getEvent() != null) .map(ed -> MessageBuilder.withPayload(ed.getEvent()).build()) - .flatMapMany(m -> stateMachine.sendEvent(Mono.just(m))) - .map(r -> r.getResultType()); + .flatMap(m -> stateMachine.sendEvent(Mono.just(m))) + .map(EventResult::new); } + //end::snippetC[] public static class EventData { private Events event; @@ -60,4 +67,21 @@ public class StateMachineController { this.event = event; } } + + public static class EventResult { + + private final StateMachineEventResult result; + + EventResult(StateMachineEventResult result) { + this.result = result; + } + + public ResultType getResultType() { + return result.getResultType(); + } + + public Events getEvent() { + return result.getMessage().getPayload(); + } + } } diff --git a/spring-statemachine-samples/turnstilereactive/src/test/java/demo/turnstilereactive/TurnstileReactiveTests.java b/spring-statemachine-samples/turnstilereactive/src/test/java/demo/turnstilereactive/TurnstileReactiveTests.java index 034e1a05..bf245244 100644 --- a/spring-statemachine-samples/turnstilereactive/src/test/java/demo/turnstilereactive/TurnstileReactiveTests.java +++ b/spring-statemachine-samples/turnstilereactive/src/test/java/demo/turnstilereactive/TurnstileReactiveTests.java @@ -45,17 +45,50 @@ public class TurnstileReactiveTests { } @Test - public void testEvent() { - webClient.post().uri("/event").contentType(MediaType.APPLICATION_JSON) - .body(Mono.just("{\"event\":\"PUSH\"}"), String.class).exchange() - .expectBody(String.class).value(containsString("DENIED")); - webClient.post().uri("/event").contentType(MediaType.APPLICATION_JSON) - .body(Mono.just("{\"event\":\"COIN\"}"), String.class).exchange() - .expectBody(String.class).value(containsString("ACCEPTED")); - webClient.get().uri("/state").exchange() - .expectBody(String.class).value(containsString("UNLOCKED")); - webClient.post().uri("/event").contentType(MediaType.APPLICATION_JSON) - .body(Mono.just("{\"event\":null}"), String.class).exchange() - .expectBody(String.class).value(containsString("[]")); + public void testPushDenied() { + webClient.post().uri("/events") + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just("{\"event\":\"PUSH\"}"), String.class) + .exchange() + .expectBody() + .jsonPath("$.length()").isEqualTo(1) + .jsonPath("$[0].event").isEqualTo("PUSH") + .jsonPath("$[0].resultType").isEqualTo("DENIED"); + } + + @Test + public void testCoinAccepted() { + webClient.post().uri("/events") + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just("{\"event\":\"COIN\"}"), String.class) + .exchange() + .expectBody() + .jsonPath("$.length()").isEqualTo(1) + .jsonPath("$[0].event").isEqualTo("COIN") + .jsonPath("$[0].resultType").isEqualTo("ACCEPTED"); + } + + @Test + public void testNullEvent() { + webClient.post().uri("/events") + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just("{\"event\":null}"), String.class) + .exchange() + .expectBody() + .jsonPath("$.length()").isEqualTo(0); + } + + @Test + public void testCoinPushAccepted() { + webClient.post().uri("/events") + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just("[{\"event\":\"COIN\"},{\"event\":\"PUSH\"}]"), String.class) + .exchange() + .expectBody() + .jsonPath("$.length()").isEqualTo(2) + .jsonPath("$[0].event").isEqualTo("COIN") + .jsonPath("$[0].resultType").isEqualTo("ACCEPTED") + .jsonPath("$[1].event").isEqualTo("PUSH") + .jsonPath("$[1].resultType").isEqualTo("ACCEPTED"); } } diff --git a/spring-statemachine-samples/turnstilereactive/src/test/resources/test.http b/spring-statemachine-samples/turnstilereactive/src/test/resources/test.http new file mode 100644 index 00000000..c33abc3e --- /dev/null +++ b/spring-statemachine-samples/turnstilereactive/src/test/resources/test.http @@ -0,0 +1,35 @@ +### to ease using vscode rest-client extension +GET http://localhost:8080/state +### +POST http://localhost:8080/events +content-type: application/json + +{ + "event": "PUSH" +} +### +POST http://localhost:8080/events +content-type: application/json + +{ + "event": "COIN" +} +### +POST http://localhost:8080/events +content-type: application/json + +{ + "event": "PUSH" +} +### +POST http://localhost:8080/events + +content-type: application/json +[ + { + "event": "COIN" + }, + { + "event": "PUSH" + } +]