├── .gitignore ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src ├── main ├── java │ └── io │ │ └── projectreactor │ │ └── demos │ │ └── in │ │ └── action │ │ ├── InActionApplication.java │ │ ├── testing │ │ ├── Debugging.java │ │ ├── Testing.java │ │ └── support │ │ │ ├── FluxSource.java │ │ │ ├── Trigger1.java │ │ │ └── Trigger2.java │ │ ├── webclient │ │ └── ClientExample.java │ │ └── webflux │ │ └── WebFluxController.java └── resources │ └── application.properties └── test └── java └── io └── projectreactor └── demos └── in └── action ├── InActionApplicationTests.java └── testing └── TestingTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build/ 3 | /out/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | 6 | ### STS ### 7 | .apt_generated 8 | .classpath 9 | .factorypath 10 | .project 11 | .settings 12 | .springBeans 13 | 14 | ### IntelliJ IDEA ### 15 | .idea 16 | *.iws 17 | *.iml 18 | *.ipr 19 | 20 | ### NetBeans ### 21 | nbproject/private/ 22 | build/ 23 | nbbuild/ 24 | dist/ 25 | nbdist/ 26 | .nb-gradle/ -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '2.0.0.M6' 4 | } 5 | repositories { 6 | mavenCentral() 7 | maven { url "https://repo.spring.io/snapshot" } 8 | maven { url "https://repo.spring.io/milestone" } 9 | } 10 | dependencies { 11 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 12 | } 13 | } 14 | 15 | apply plugin: 'java' 16 | apply plugin: 'eclipse' 17 | apply plugin: 'org.springframework.boot' 18 | apply plugin: 'io.spring.dependency-management' 19 | 20 | group = 'io.projectreactor.demos' 21 | version = '0.0.1-SNAPSHOT' 22 | sourceCompatibility = 1.8 23 | 24 | repositories { 25 | mavenCentral() 26 | maven { url "https://repo.spring.io/snapshot" } 27 | maven { url "https://repo.spring.io/milestone" } 28 | } 29 | 30 | 31 | dependencies { 32 | compile('org.springframework.boot:spring-boot-starter-webflux') 33 | testCompile('org.springframework.boot:spring-boot-starter-test') 34 | testCompile('io.projectreactor:reactor-test') 35 | } 36 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonbasle-demos/s1p-reactor-in-action/d7d7dc488157cc1fe24a8d49920d64a59cc0deca/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jul 28 13:37:07 BST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.2-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/main/java/io/projectreactor/demos/in/action/InActionApplication.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.demos.in.action; 2 | 3 | import java.util.function.Function; 4 | 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.context.annotation.Bean; 8 | 9 | @SpringBootApplication 10 | public class InActionApplication { 11 | 12 | public static void main(String[] args) { 13 | SpringApplication.run(InActionApplication.class, args); 14 | } 15 | 16 | @Bean 17 | public static Function divideTenBy() { 18 | return i -> 10 / i; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/io/projectreactor/demos/in/action/testing/Debugging.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.demos.in.action.testing; 2 | 3 | import io.projectreactor.demos.in.action.InActionApplication; 4 | import reactor.core.publisher.Flux; 5 | 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.context.ConfigurableApplicationContext; 8 | import org.springframework.stereotype.Service; 9 | 10 | /** 11 | * @author Simon Baslé 12 | */ 13 | @Service 14 | public class Debugging { 15 | 16 | private final Flux source; 17 | 18 | public Debugging(Flux source) { 19 | this.source = source; 20 | } 21 | 22 | public Flux numbers() { 23 | return source; 24 | } 25 | 26 | public static void main(String[] args) { 27 | ConfigurableApplicationContext ctx = 28 | SpringApplication.run(InActionApplication.class, args); 29 | 30 | ctx.getBean(Debugging.class) 31 | .numbers() 32 | .subscribe(System.out::println, e -> { 33 | e.printStackTrace(System.out); 34 | SpringApplication.exit(ctx); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/io/projectreactor/demos/in/action/testing/Testing.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.demos.in.action.testing; 2 | 3 | import java.time.Duration; 4 | import java.util.ArrayList; 5 | import java.util.Arrays; 6 | import java.util.Collections; 7 | import java.util.LinkedHashSet; 8 | import java.util.List; 9 | import java.util.Set; 10 | import java.util.function.Function; 11 | 12 | import reactor.core.publisher.Flux; 13 | 14 | import org.springframework.stereotype.Service; 15 | 16 | /** 17 | * @author Simon Baslé 18 | */ 19 | @Service 20 | public class Testing { 21 | 22 | static final Set NAMES = new LinkedHashSet<>(Arrays.asList( 23 | "Victor","Simon","Rick","Morty","Beth","Jerry","Summer" 24 | )); 25 | 26 | public Flux tenToZero() { 27 | return Flux.range(0, 11) 28 | .map(i -> 10 - i); 29 | } 30 | 31 | public Flux operateOnTenToZero(Function operation) { 32 | return tenToZero().map(operation); 33 | } 34 | 35 | public Flux namesPerSecond() { 36 | List randomizedNames = new ArrayList<>(NAMES); 37 | Collections.reverse(randomizedNames); 38 | 39 | return Flux.fromIterable(randomizedNames) 40 | .delayElements(Duration.ofSeconds(1)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/io/projectreactor/demos/in/action/testing/support/FluxSource.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.demos.in.action.testing.support; 2 | 3 | import java.util.function.Function; 4 | 5 | import reactor.core.publisher.Flux; 6 | 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Conditional; 9 | import org.springframework.stereotype.Component; 10 | 11 | /** 12 | * @author Simon Baslé 13 | */ 14 | @Component 15 | public class FluxSource { 16 | 17 | private Function operation; 18 | 19 | public FluxSource(Function operation) { 20 | this.operation = operation; 21 | } 22 | 23 | @Conditional(Trigger1.class) 24 | @Bean 25 | public Flux flux1() { 26 | return Flux.just(1, 2, 0, 3, 4) 27 | .map(operation) 28 | .checkpoint("flux1", true); 29 | } 30 | 31 | @Conditional(Trigger2.class) 32 | @Bean 33 | public Flux flux2() { 34 | return Flux.range(1, 10) 35 | .map(i -> 10 - i) 36 | .map(operation) 37 | .checkpoint("flux2"); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/io/projectreactor/demos/in/action/testing/support/Trigger1.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.demos.in.action.testing.support; 2 | 3 | import org.springframework.context.annotation.Condition; 4 | import org.springframework.context.annotation.ConditionContext; 5 | import org.springframework.core.type.AnnotatedTypeMetadata; 6 | 7 | /** 8 | * @author Simon Baslé 9 | */ 10 | public class Trigger1 implements Condition { 11 | 12 | @Override 13 | public boolean matches(ConditionContext context, 14 | AnnotatedTypeMetadata metadata) { 15 | return (System.currentTimeMillis() / 1_000) % 2 == 0; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/io/projectreactor/demos/in/action/testing/support/Trigger2.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.demos.in.action.testing.support; 2 | 3 | import org.springframework.context.annotation.Condition; 4 | import org.springframework.context.annotation.ConditionContext; 5 | import org.springframework.core.type.AnnotatedTypeMetadata; 6 | 7 | /** 8 | * @author Simon Baslé 9 | */ 10 | public class Trigger2 implements Condition { 11 | 12 | @Override 13 | public boolean matches(ConditionContext context, 14 | AnnotatedTypeMetadata metadata) { 15 | return (System.currentTimeMillis() / 1_000) % 2 != 0; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/io/projectreactor/demos/in/action/webclient/ClientExample.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.demos.in.action.webclient; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | import reactor.core.publisher.Flux; 7 | import reactor.core.publisher.Mono; 8 | 9 | import org.springframework.http.MediaType; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.PathVariable; 12 | import org.springframework.web.bind.annotation.RequestParam; 13 | import org.springframework.web.bind.annotation.RestController; 14 | import org.springframework.web.reactive.function.client.WebClient; 15 | 16 | /** 17 | * @author Simon Baslé 18 | */ 19 | @RestController 20 | public class ClientExample { 21 | 22 | private WebClient swapi = WebClient.create("http://swapi.co/api/"); 23 | 24 | private Flux findCharacters(String characterName) { 25 | //noinspection unchecked 26 | return swapi.get() 27 | .uri("/people/?search={charName}", characterName) 28 | .retrieve() 29 | .bodyToMono(Map.class) 30 | .flatMapMany(results -> Flux.fromIterable( 31 | (List) results.get("results"))); 32 | } 33 | 34 | private Mono getCharacter(int characterId) { 35 | return swapi.get() 36 | .uri("/people/{id}", characterId) 37 | .retrieve() 38 | .bodyToMono(Map.class); 39 | } 40 | 41 | private Flux fetchMovieTitles(Map characterJson) { 42 | if (characterJson.containsKey("films")) { 43 | @SuppressWarnings("unchecked") 44 | Iterable movieUris = (Iterable) characterJson.get("films"); 45 | return Flux.fromIterable(movieUris) 46 | .concatMap(url -> swapi.get().uri(url) 47 | .retrieve() 48 | .bodyToMono(Map.class) 49 | .map(map -> String.valueOf(map.get("title"))) 50 | ); 51 | } 52 | else { 53 | return Flux.error(new IllegalArgumentException("passed Map does not represent a character")); 54 | } 55 | } 56 | 57 | 58 | 59 | @GetMapping(value = "/api/starwars/person/{charId}" 60 | , produces = MediaType.TEXT_EVENT_STREAM_VALUE 61 | ) 62 | public Flux charInMoviesById(@PathVariable int charId) { 63 | return getCharacter(charId) 64 | .flatMapMany(json -> fetchMovieTitles(json) 65 | .map(title -> title + "\n") 66 | .startWith("Character " + charId + " is " + json.get("name") + 67 | ", appears in movies:\n") 68 | .concatWith(Mono.just("and that's it!\n\n")) 69 | ); 70 | } 71 | 72 | @GetMapping(value = "/api/starwars/person") 73 | public Flux charInMoviesByName(@RequestParam String name) { 74 | return findCharacters(name) 75 | .flatMapSequential(json -> fetchMovieTitles(json) 76 | .map(title -> title + "\n") 77 | .startWith("Character " + json.get("name") + 78 | " appears in movies:\n") 79 | .concatWith(Mono.just("and that's it!\n\n")) 80 | ); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/io/projectreactor/demos/in/action/webflux/WebFluxController.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.demos.in.action.webflux; 2 | 3 | import io.projectreactor.demos.in.action.testing.Testing; 4 | import reactor.core.publisher.Flux; 5 | import reactor.core.publisher.Mono; 6 | 7 | import org.springframework.http.MediaType; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.PathVariable; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RequestParam; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | /** 15 | * @author Simon Baslé 16 | */ 17 | @RestController 18 | @RequestMapping("/api") 19 | public class WebFluxController { 20 | 21 | Testing service; 22 | 23 | public WebFluxController(Testing service) { 24 | this.service = service; 25 | } 26 | 27 | @GetMapping("/numbers") 28 | public Flux numbers(@RequestParam(required = false) String operation) { 29 | if (operation == null) 30 | return service.tenToZero(); 31 | 32 | switch (operation) { 33 | case "divide": 34 | return service.tenToZero() 35 | .filter(i -> i != 0) 36 | .map(i -> 100 / i); 37 | case "multiply": 38 | return service.operateOnTenToZero(i -> 100 * i); 39 | default: 40 | throw new UnsupportedOperationException("Unknown operation \"" + operation + "\"," + " try \"divide\" or \"multiply\""); 41 | } 42 | } 43 | 44 | @GetMapping("/numbers/{index}") 45 | public Mono number(@PathVariable int index) { 46 | return service.tenToZero() 47 | .elementAt(index); 48 | } 49 | 50 | @GetMapping(value = "/names", produces = MediaType.TEXT_EVENT_STREAM_VALUE) 51 | public Flux names() { 52 | return service.namesPerSecond(); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonbasle-demos/s1p-reactor-in-action/d7d7dc488157cc1fe24a8d49920d64a59cc0deca/src/main/resources/application.properties -------------------------------------------------------------------------------- /src/test/java/io/projectreactor/demos/in/action/InActionApplicationTests.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.demos.in.action; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class InActionApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/io/projectreactor/demos/in/action/testing/TestingTest.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.demos.in.action.testing; 2 | 3 | import java.time.Duration; 4 | import java.util.ArrayList; 5 | import java.util.Objects; 6 | 7 | import org.junit.Test; 8 | import reactor.test.StepVerifier; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | /** 13 | * @author Simon Baslé 14 | */ 15 | public class TestingTest { 16 | 17 | private Testing test = new Testing(); 18 | 19 | @Test 20 | public void tenToZero() { 21 | StepVerifier.create(test.tenToZero()) 22 | .expectNext(10) 23 | .expectNext(9, 8, 7, 6, 5) 24 | .expectNextCount(4) 25 | .expectNext(0) 26 | .verifyComplete(); 27 | } 28 | 29 | @Test 30 | public void multiplyByTenToZero() { 31 | StepVerifier.create(test.operateOnTenToZero(i -> 10 * i)) 32 | .expectSubscription() 33 | .expectNextCount(5) 34 | .assertNext(v -> assertThat(v).isEqualTo(50)) 35 | .expectNext(40, 30, 20, 10, 0) 36 | .verifyComplete(); 37 | } 38 | 39 | @Test 40 | public void divideByTenToZeroErrors() { 41 | StepVerifier.create(test.operateOnTenToZero(i -> 10 / i)) 42 | .expectNext(1) 43 | .expectNextCount(4) 44 | .expectNext(2) 45 | .expectNextCount(2) 46 | .expectNext(5, 10) 47 | .verifyErrorSatisfies(e -> assertThat(e).isInstanceOf(ArithmeticException.class) 48 | .hasMessage("/ by zero")); 49 | } 50 | 51 | @Test 52 | public void namesPerSecond() { 53 | StepVerifier.create(test.namesPerSecond() 54 | .doOnNext(System.out::println)) 55 | .recordWith(ArrayList::new) 56 | .thenConsumeWhile(Objects::nonNull) 57 | .consumeRecordedWith(l -> assertThat(Testing.NAMES).containsAll(l)) 58 | .verifyComplete(); 59 | } 60 | 61 | @Test 62 | public void namesPerSecondWithoutTime() { 63 | StepVerifier.withVirtualTime(() -> test.namesPerSecond() 64 | .doOnNext(System.out::println)) 65 | .expectSubscription() 66 | .expectNoEvent(Duration.ofSeconds(1)) 67 | .expectNext("Summer") 68 | .thenAwait(Duration.ofSeconds(2)) 69 | .expectNextCount(2) 70 | .thenAwait(Duration.ofMinutes(1)) 71 | .expectNext("Morty", "Rick", "Simon", "Victor") 72 | .verifyComplete(); 73 | } 74 | } --------------------------------------------------------------------------------