Add turnstile reactive docs

- Relates #742
This commit is contained in:
Janne Valkealahti
2019-05-17 16:04:20 +01:00
parent 345b27b2b2
commit efbd72b4d1
6 changed files with 237 additions and 18 deletions

View File

@@ -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/'

View File

@@ -0,0 +1,123 @@
[[statemachine-examples-turnstilereactive]]
== Turnstile Reactive
Turnstile reactive is an enhacement to <<statemachine-examples-turnstile>> 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"
}
]
----
====

View File

@@ -12,6 +12,8 @@ normal build cycle. This chapter includes the following samples:
<<statemachine-examples-turnstile>>
<<statemachine-examples-turnstilereactive>>
<<statemachine-examples-showcase>>
<<statemachine-examples-cdplayer>>
@@ -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[]

View File

@@ -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<States, Events> stateMachine;
//end::snippetA[]
//tag::snippetB[]
@GetMapping("/state")
public Mono<States> state() {
return Mono.justOrEmpty(stateMachine.getState().getId());
return Mono.defer(() -> Mono.justOrEmpty(stateMachine.getState().getId()));
}
//end::snippetB[]
@PostMapping("/event")
public Flux<ResultType> event(@RequestBody Mono<EventData> eventData) {
//tag::snippetC[]
@PostMapping("/events")
public Flux<EventResult> events(@RequestBody Flux<EventData> 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<States, Events> result;
EventResult(StateMachineEventResult<States, Events> result) {
this.result = result;
}
public ResultType getResultType() {
return result.getResultType();
}
public Events getEvent() {
return result.getMessage().getPayload();
}
}
}

View File

@@ -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");
}
}

View File

@@ -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"
}
]