Add body methods with Object parameter to WebFlux

The commit deprecates syncBody(Object) in favor of body(Object)
which has the same behavior in ServerResponse, WebClient and
WebTestClient. It also adds body(Object, Class) and
body(Object, ParameterizedTypeReference) methods in order to support
any reactive type that can be adapted to a Publisher via
ReactiveAdapterRegistry. Related BodyInserters#fromProducer
methods are provided as well.

Shadowed Kotlin body<T>() extensions are deprecated in favor of
bodyWithType<T>() ones, including dedicated Publisher<T> and
Flow<T> variants. Coroutines extensions are adapted as well, and
body(Object) can now be used with suspending functions.

Closes gh-23212
This commit is contained in:
Sebastien Deleuze
2019-07-07 21:03:41 +02:00
parent 0fbc9bf461
commit 2b4d6ce354
33 changed files with 781 additions and 270 deletions

View File

@@ -30,6 +30,7 @@ import java.util.Map;
import java.util.Optional;
import com.fasterxml.jackson.annotation.JsonView;
import io.reactivex.Single;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Flux;
@@ -159,6 +160,51 @@ public class BodyInsertersTests {
.verify();
}
@Test
public void ofProducerWithMono() {
Mono<User> body = Mono.just(new User("foo", "bar"));
BodyInserter<?, ReactiveHttpOutputMessage> inserter = BodyInserters.fromProducer(body, User.class);
MockServerHttpResponse response = new MockServerHttpResponse();
Mono<Void> result = inserter.insert(response, this.context);
StepVerifier.create(result).expectComplete().verify();
StepVerifier.create(response.getBodyAsString())
.expectNext("{\"username\":\"foo\",\"password\":\"bar\"}")
.expectComplete()
.verify();
}
@Test
public void ofProducerWithFlux() {
Flux<String> body = Flux.just("foo");
BodyInserter<?, ReactiveHttpOutputMessage> inserter = BodyInserters.fromProducer(body, String.class);
MockServerHttpResponse response = new MockServerHttpResponse();
Mono<Void> result = inserter.insert(response, this.context);
StepVerifier.create(result).expectComplete().verify();
StepVerifier.create(response.getBody())
.consumeNextWith(buf -> {
String actual = DataBufferTestUtils.dumpString(buf, UTF_8);
assertThat(actual).isEqualTo("foo");
})
.expectComplete()
.verify();
}
@Test
public void ofProducerWithSingle() {
Single<User> body = Single.just(new User("foo", "bar"));
BodyInserter<?, ReactiveHttpOutputMessage> inserter = BodyInserters.fromProducer(body, User.class);
MockServerHttpResponse response = new MockServerHttpResponse();
Mono<Void> result = inserter.insert(response, this.context);
StepVerifier.create(result).expectComplete().verify();
StepVerifier.create(response.getBodyAsString())
.expectNext("{\"username\":\"foo\",\"password\":\"bar\"}")
.expectComplete()
.verify();
}
@Test
public void ofPublisher() {
Flux<String> body = Flux.just("foo");

View File

@@ -61,7 +61,7 @@ public class MultipartIntegrationTests extends AbstractRouterFunctionIntegration
Mono<ClientResponse> result = webClient
.post()
.uri("http://localhost:" + this.port + "/multipartData")
.syncBody(generateBody())
.body(generateBody())
.exchange();
StepVerifier
@@ -75,7 +75,7 @@ public class MultipartIntegrationTests extends AbstractRouterFunctionIntegration
Mono<ClientResponse> result = webClient
.post()
.uri("http://localhost:" + this.port + "/parts")
.syncBody(generateBody())
.body(generateBody())
.exchange();
StepVerifier
@@ -89,7 +89,7 @@ public class MultipartIntegrationTests extends AbstractRouterFunctionIntegration
Mono<String> result = webClient
.post()
.uri("http://localhost:" + this.port + "/transferTo")
.syncBody(generateBody())
.body(generateBody())
.retrieve()
.bodyToMono(String.class);
@@ -169,7 +169,7 @@ public class MultipartIntegrationTests extends AbstractRouterFunctionIntegration
Path tempFile = Files.createTempFile("MultipartIntegrationTests", null);
return part.transferTo(tempFile)
.then(ServerResponse.ok()
.syncBody(tempFile.toString()));
.body(tempFile.toString()));
}
catch (Exception e) {
return Mono.error(e);

View File

@@ -186,7 +186,7 @@ public class DefaultWebClientTests {
WebClient client = this.builder.build();
assertThatIllegalArgumentException().isThrownBy(() ->
client.post().uri("https://example.com").syncBody(mono));
client.post().uri("https://example.com").body(mono));
}
@Test

View File

@@ -354,7 +354,7 @@ public class WebClientIntegrationTests {
.uri("/pojo/capitalize")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.syncBody(new Pojo("foofoo", "barbar"))
.body(new Pojo("foofoo", "barbar"))
.retrieve()
.bodyToMono(Pojo.class);

View File

@@ -24,6 +24,7 @@ import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import io.reactivex.Single;
import org.junit.Test;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
@@ -76,6 +77,14 @@ public class DefaultEntityResponseBuilderTests {
assertThat(response.entity()).isSameAs(body);
}
@Test
public void fromProducer() {
Single<String> body = Single.just("foo");
ParameterizedTypeReference<String> typeReference = new ParameterizedTypeReference<String>() {};
EntityResponse<Single<String>> response = EntityResponse.fromProducer(body, typeReference).build().block();
assertThat(response.entity()).isSameAs(body);
}
@Test
public void status() {
String body = "foo";

View File

@@ -308,7 +308,7 @@ public class DefaultServerResponseBuilderTests {
public void copyCookies() {
Mono<ServerResponse> serverResponse = ServerResponse.ok()
.cookie(ResponseCookie.from("foo", "bar").build())
.syncBody("body");
.body("body");
assertThat(serverResponse.block().cookies().isEmpty()).isFalse();
@@ -360,7 +360,7 @@ public class DefaultServerResponseBuilderTests {
Mono<Void> mono = Mono.empty();
assertThatIllegalArgumentException().isThrownBy(() ->
ServerResponse.ok().syncBody(mono));
ServerResponse.ok().body(mono));
}
@Test
@@ -368,7 +368,7 @@ public class DefaultServerResponseBuilderTests {
String etag = "\"foo\"";
ServerResponse responseMono = ServerResponse.ok()
.eTag(etag)
.syncBody("bar")
.body("bar")
.block();
MockServerHttpRequest request = MockServerHttpRequest.get("https://example.com")
@@ -392,7 +392,7 @@ public class DefaultServerResponseBuilderTests {
ServerResponse responseMono = ServerResponse.ok()
.lastModified(oneMinuteBeforeNow)
.syncBody("bar")
.body("bar")
.block();
MockServerHttpRequest request = MockServerHttpRequest.get("https://example.com")

View File

@@ -33,8 +33,8 @@ public class InvalidHttpMethodIntegrationTests extends AbstractRouterFunctionInt
@Override
protected RouterFunction<?> routerFunction() {
return RouterFunctions.route(RequestPredicates.GET("/"),
request -> ServerResponse.ok().syncBody("FOO"))
.andRoute(RequestPredicates.all(), request -> ServerResponse.ok().syncBody("BAR"));
request -> ServerResponse.ok().body("FOO"))
.andRoute(RequestPredicates.all(), request -> ServerResponse.ok().body("BAR"));
}
@Test

View File

@@ -125,7 +125,7 @@ public class NestedRouteIntegrationTests extends AbstractRouterFunctionIntegrati
public Mono<ServerResponse> pattern(ServerRequest request) {
String pattern = matchingPattern(request).getPatternString();
return ServerResponse.ok().syncBody(pattern);
return ServerResponse.ok().body(pattern);
}
@SuppressWarnings("unchecked")

View File

@@ -85,7 +85,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes
Mono<ClientResponse> result = webClient
.post()
.uri("/requestPart")
.syncBody(generateBody())
.body(generateBody())
.exchange();
StepVerifier
@@ -99,7 +99,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes
Mono<String> result = webClient
.post()
.uri("/requestBodyMap")
.syncBody(generateBody())
.body(generateBody())
.retrieve()
.bodyToMono(String.class);
@@ -113,7 +113,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes
Mono<String> result = webClient
.post()
.uri("/requestBodyFlux")
.syncBody(generateBody())
.body(generateBody())
.retrieve()
.bodyToMono(String.class);
@@ -127,7 +127,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes
Mono<String> result = webClient
.post()
.uri("/filePartFlux")
.syncBody(generateBody())
.body(generateBody())
.retrieve()
.bodyToMono(String.class);
@@ -141,7 +141,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes
Mono<String> result = webClient
.post()
.uri("/filePartMono")
.syncBody(generateBody())
.body(generateBody())
.retrieve()
.bodyToMono(String.class);
@@ -155,7 +155,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes
Flux<String> result = webClient
.post()
.uri("/transferTo")
.syncBody(generateBody())
.body(generateBody())
.retrieve()
.bodyToFlux(String.class);
@@ -183,7 +183,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes
Mono<String> result = webClient
.post()
.uri("/modelAttribute")
.syncBody(generateBody())
.body(generateBody())
.retrieve()
.bodyToMono(String.class);

View File

@@ -27,6 +27,7 @@ import org.junit.Test
import org.reactivestreams.Publisher
import org.springframework.core.ParameterizedTypeReference
import reactor.core.publisher.Mono
import java.util.concurrent.CompletableFuture
/**
* Mock object based tests for [WebClient] Kotlin extensions
@@ -41,9 +42,9 @@ class WebClientExtensionsTests {
@Test
fun `RequestBodySpec#body with Publisher and reified type parameters`() {
fun `RequestBodySpec#bodyWithType with Publisher and reified type parameters`() {
val body = mockk<Publisher<List<Foo>>>()
requestBodySpec.body(body)
requestBodySpec.bodyWithType(body)
verify { requestBodySpec.body(body, object : ParameterizedTypeReference<List<Foo>>() {}) }
}
@@ -51,8 +52,16 @@ class WebClientExtensionsTests {
@FlowPreview
fun `RequestBodySpec#body with Flow and reified type parameters`() {
val body = mockk<Flow<List<Foo>>>()
requestBodySpec.body(body)
verify { requestBodySpec.body(ofType<Publisher<List<Foo>>>(), object : ParameterizedTypeReference<List<Foo>>() {}) }
requestBodySpec.bodyWithType(body)
verify { requestBodySpec.body(ofType<Any>(), object : ParameterizedTypeReference<List<Foo>>() {}) }
}
@Test
@FlowPreview
fun `RequestBodySpec#body with CompletableFuture and reified type parameters`() {
val body = mockk<CompletableFuture<List<Foo>>>()
requestBodySpec.bodyWithType<List<Foo>>(body)
verify { requestBodySpec.body(ofType<Any>(), object : ParameterizedTypeReference<List<Foo>>() {}) }
}
@Test
@@ -83,19 +92,6 @@ class WebClientExtensionsTests {
}
}
@Test
fun body() {
val headerSpec = mockk<WebClient.RequestHeadersSpec<*>>()
val supplier: suspend () -> String = mockk()
every { requestBodySpec.body(ofType<Mono<String>>()) } returns headerSpec
runBlocking {
requestBodySpec.body(supplier)
}
verify {
requestBodySpec.body(ofType<Mono<String>>())
}
}
@Test
fun awaitBody() {
val spec = mockk<WebClient.ResponseSpec>()

View File

@@ -19,6 +19,7 @@ package org.springframework.web.reactive.function.server
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import io.reactivex.Flowable
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.runBlocking
@@ -28,12 +29,14 @@ import org.reactivestreams.Publisher
import org.springframework.core.ParameterizedTypeReference
import org.springframework.http.MediaType.*
import reactor.core.publisher.Mono
import java.util.concurrent.CompletableFuture
/**
* Mock object based tests for [ServerResponse] Kotlin extensions
*
* @author Sebastien Deleuze
*/
@Suppress("UnassignedFluxMonoInstance")
class ServerResponseExtensionsTests {
private val bodyBuilder = mockk<ServerResponse.BodyBuilder>(relaxed = true)
@@ -42,10 +45,51 @@ class ServerResponseExtensionsTests {
@Test
fun `BodyBuilder#body with Publisher and reified type parameters`() {
val body = mockk<Publisher<List<Foo>>>()
bodyBuilder.body(body)
bodyBuilder.bodyWithType(body)
verify { bodyBuilder.body(body, object : ParameterizedTypeReference<List<Foo>>() {}) }
}
@Test
fun `BodyBuilder#body with CompletableFuture and reified type parameters`() {
val body = mockk<CompletableFuture<List<Foo>>>()
bodyBuilder.bodyWithType<List<Foo>>(body)
verify { bodyBuilder.body(body, object : ParameterizedTypeReference<List<Foo>>() {}) }
}
@Test
fun `BodyBuilder#body with Flowable and reified type parameters`() {
val body = mockk<Flowable<List<Foo>>>()
bodyBuilder.bodyWithType(body)
verify { bodyBuilder.body(body, object : ParameterizedTypeReference<List<Foo>>() {}) }
}
@Test
fun `BodyBuilder#bodyAndAwait with object parameter`() {
val response = mockk<ServerResponse>()
val body = "foo"
every { bodyBuilder.body(ofType<String>()) } returns Mono.just(response)
runBlocking {
bodyBuilder.bodyAndAwait(body)
}
verify {
bodyBuilder.body(ofType<String>())
}
}
@Test
@FlowPreview
fun `BodyBuilder#bodyAndAwait with flow parameter`() {
val response = mockk<ServerResponse>()
val body = mockk<Flow<List<Foo>>>()
every { bodyBuilder.body(ofType<Flow<List<Foo>>>(), object : ParameterizedTypeReference<List<Foo>>() {}) } returns Mono.just(response)
runBlocking {
bodyBuilder.bodyAndAwait(body)
}
verify {
bodyBuilder.body(ofType<Flow<List<Foo>>>(), object : ParameterizedTypeReference<List<Foo>>() {})
}
}
@Test
fun `BodyBuilder#json`() {
bodyBuilder.json()
@@ -71,42 +115,7 @@ class ServerResponseExtensionsTests {
}
@Test
fun await() {
val response = mockk<ServerResponse>()
val builder = mockk<ServerResponse.HeadersBuilder<*>>()
every { builder.build() } returns Mono.just(response)
runBlocking {
assertEquals(response, builder.buildAndAwait())
}
}
@Test
fun `bodyAndAwait with object parameter`() {
val response = mockk<ServerResponse>()
val body = "foo"
every { bodyBuilder.syncBody(ofType<String>()) } returns Mono.just(response)
runBlocking {
bodyBuilder.bodyAndAwait(body)
}
verify {
bodyBuilder.syncBody(ofType<String>())
}
}
@Test
@FlowPreview
fun bodyFlowAndAwait() {
val response = mockk<ServerResponse>()
val body = mockk<Flow<List<Foo>>>()
every { bodyBuilder.body(ofType<Publisher<List<Foo>>>()) } returns Mono.just(response)
runBlocking {
bodyBuilder.bodyFlowAndAwait(body)
}
verify { bodyBuilder.body(ofType<Publisher<List<Foo>>>(), object : ParameterizedTypeReference<List<Foo>>() {}) }
}
@Test
fun `renderAndAwait with a vararg parameter`() {
fun `BodyBuilder#renderAndAwait with a vararg parameter`() {
val response = mockk<ServerResponse>()
every { bodyBuilder.render("foo", any(), any()) } returns Mono.just(response)
runBlocking {
@@ -118,7 +127,7 @@ class ServerResponseExtensionsTests {
}
@Test
fun `renderAndAwait with a Map parameter`() {
fun `BodyBuilder#renderAndAwait with a Map parameter`() {
val response = mockk<ServerResponse>()
val map = mockk<Map<String, *>>()
every { bodyBuilder.render("foo", map) } returns Mono.just(response)
@@ -130,5 +139,15 @@ class ServerResponseExtensionsTests {
}
}
@Test
fun `HeadersBuilder#buildAndAwait`() {
val response = mockk<ServerResponse>()
val builder = mockk<ServerResponse.HeadersBuilder<*>>()
every { builder.build() } returns Mono.just(response)
runBlocking {
assertEquals(response, builder.buildAndAwait())
}
}
class Foo
}