diff --git a/spring-webflux/spring-webflux.gradle b/spring-webflux/spring-webflux.gradle index cf51de3776b..8a5ab4f8650 100644 --- a/spring-webflux/spring-webflux.gradle +++ b/spring-webflux/spring-webflux.gradle @@ -40,6 +40,8 @@ dependencies { optional("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}") optional("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}") optional("com.google.protobuf:protobuf-java-util:3.6.1") + optional("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1") + optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.1.1") testCompile("javax.xml.bind:jaxb-api:2.3.1") testCompile("com.fasterxml:aalto-xml:1.1.1") testCompile("org.hibernate:hibernate-validator:6.0.14.Final") diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/ClientResponseExtensions.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/ClientResponseExtensions.kt index e2e1da0b2d2..c3e1ef68299 100644 --- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/ClientResponseExtensions.kt +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/ClientResponseExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.web.reactive.function.client +import kotlinx.coroutines.reactive.awaitSingle import org.springframework.core.ParameterizedTypeReference import org.springframework.http.ResponseEntity import reactor.core.publisher.Flux @@ -64,3 +65,30 @@ inline fun ClientResponse.toEntity(): Mono> */ inline fun ClientResponse.toEntityList(): Mono>> = toEntityList(object : ParameterizedTypeReference() {}) + +/** + * Coroutines variant of [ClientResponse.bodyToMono]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +suspend inline fun ClientResponse.awaitBody(): T = + bodyToMono().awaitSingle() + +/** + * Coroutines variant of [ClientResponse.toEntity]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +suspend inline fun ClientResponse.awaitEntity(): ResponseEntity = + toEntity().awaitSingle() + +/** + * Coroutines variant of [ClientResponse.toEntityList]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +suspend inline fun ClientResponse.awaitEntityList(): ResponseEntity> = + toEntityList().awaitSingle() diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt index 264d129867b..0981190b83b 100644 --- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,9 @@ package org.springframework.web.reactive.function.client +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.reactive.awaitSingle +import kotlinx.coroutines.reactor.mono import org.reactivestreams.Publisher import org.springframework.core.ParameterizedTypeReference import org.springframework.web.reactive.function.client.WebClient.RequestBodySpec @@ -57,3 +60,30 @@ inline fun WebClient.ResponseSpec.bodyToMono(): Mono = */ inline fun WebClient.ResponseSpec.bodyToFlux(): Flux = bodyToFlux(object : ParameterizedTypeReference() {}) + +/** + * Coroutines variant of [WebClient.RequestHeadersSpec.exchange]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +suspend fun WebClient.RequestHeadersSpec>.awaitResponse(): ClientResponse = + exchange().awaitSingle() + +/** + * Coroutines variant of [WebClient.RequestBodySpec.body]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +inline fun WebClient.RequestBodySpec.body(crossinline supplier: suspend () -> T) + = body(GlobalScope.mono { supplier.invoke() }) + +/** + * Coroutines variant of [WebClient.ResponseSpec.bodyToMono]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +suspend inline fun WebClient.ResponseSpec.awaitBody() : T = + bodyToMono().awaitSingle() diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt new file mode 100644 index 00000000000..d157ac24ba1 --- /dev/null +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt @@ -0,0 +1,488 @@ +/* + * Copyright 2002-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.function.server + +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.reactor.mono +import org.springframework.core.io.Resource +import org.springframework.http.HttpMethod +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.web.reactive.function.server.RouterFunctions.nest +import java.net.URI + +/** + * Coroutines variant of [RouterFunctionDsl]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +open class CoRouterFunctionDsl(private val init: (CoRouterFunctionDsl.() -> Unit)): () -> RouterFunction { + + private val builder = RouterFunctions.route() + + /** + * Return a composed request predicate that tests against both this predicate AND + * the [other] predicate (String processed as a path predicate). When evaluating the + * composed predicate, if this predicate is `false`, then the [other] predicate is not + * evaluated. + * @see RequestPredicate.and + * @see RequestPredicates.path + */ + infix fun RequestPredicate.and(other: String): RequestPredicate = this.and(path(other)) + + /** + * Return a composed request predicate that tests against both this predicate OR + * the [other] predicate (String processed as a path predicate). When evaluating the + * composed predicate, if this predicate is `true`, then the [other] predicate is not + * evaluated. + * @see RequestPredicate.or + * @see RequestPredicates.path + */ + infix fun RequestPredicate.or(other: String): RequestPredicate = this.or(path(other)) + + /** + * Return a composed request predicate that tests against both this predicate (String + * processed as a path predicate) AND the [other] predicate. When evaluating the + * composed predicate, if this predicate is `false`, then the [other] predicate is not + * evaluated. + * @see RequestPredicate.and + * @see RequestPredicates.path + */ + infix fun String.and(other: RequestPredicate): RequestPredicate = path(this).and(other) + + /** + * Return a composed request predicate that tests against both this predicate (String + * processed as a path predicate) OR the [other] predicate. When evaluating the + * composed predicate, if this predicate is `true`, then the [other] predicate is not + * evaluated. + * @see RequestPredicate.or + * @see RequestPredicates.path + */ + infix fun String.or(other: RequestPredicate): RequestPredicate = path(this).or(other) + + /** + * Return a composed request predicate that tests against both this predicate AND + * the [other] predicate. When evaluating the composed predicate, if this + * predicate is `false`, then the [other] predicate is not evaluated. + * @see RequestPredicate.and + */ + infix fun RequestPredicate.and(other: RequestPredicate): RequestPredicate = this.and(other) + + /** + * Return a composed request predicate that tests against both this predicate OR + * the [other] predicate. When evaluating the composed predicate, if this + * predicate is `true`, then the [other] predicate is not evaluated. + * @see RequestPredicate.or + */ + infix fun RequestPredicate.or(other: RequestPredicate): RequestPredicate = this.or(other) + + /** + * Return a predicate that represents the logical negation of this predicate. + */ + operator fun RequestPredicate.not(): RequestPredicate = this.negate() + + /** + * Route to the given router function if the given request predicate applies. This + * method can be used to create *nested routes*, where a group of routes share a + * common path (prefix), header, or other request predicate. + * @see RouterFunctions.nest + */ + fun RequestPredicate.nest(r: (CoRouterFunctionDsl.() -> Unit)) { + builder.add(nest(this, CoRouterFunctionDsl(r).invoke())) + } + + + /** + * Route to the given router function if the given request predicate (String + * processed as a path predicate) applies. This method can be used to create + * *nested routes*, where a group of routes share a common path + * (prefix), header, or other request predicate. + * @see RouterFunctions.nest + * @see RequestPredicates.path + */ + fun String.nest(r: (CoRouterFunctionDsl.() -> Unit)) = path(this).nest(r) + + /** + * Route to the given handler function if the given request predicate applies. + * @see RouterFunctions.route + */ + fun GET(pattern: String, f: suspend (ServerRequest) -> ServerResponse) { + builder.GET(pattern, asHandlerFunction(f)) + } + + /** + * Return a [RequestPredicate] that matches if request's HTTP method is `GET` + * and the given `pattern` matches against the request path. + * @see RequestPredicates.GET + */ + fun GET(pattern: String): RequestPredicate = RequestPredicates.GET(pattern) + + /** + * Route to the given handler function if the given request predicate applies. + * @see RouterFunctions.route + */ + fun HEAD(pattern: String, f: suspend (ServerRequest) -> ServerResponse) { + builder.HEAD(pattern, asHandlerFunction(f)) + } + + /** + * Return a [RequestPredicate] that matches if request's HTTP method is `HEAD` + * and the given `pattern` matches against the request path. + * @see RequestPredicates.HEAD + */ + fun HEAD(pattern: String): RequestPredicate = RequestPredicates.HEAD(pattern) + + /** + * Route to the given handler function if the given `POST` predicate applies. + * @see RouterFunctions.route + */ + fun POST(pattern: String, f: suspend (ServerRequest) -> ServerResponse) { + builder.POST(pattern, asHandlerFunction(f)) + } + + /** + * Return a [RequestPredicate] that matches if request's HTTP method is `POST` + * and the given `pattern` matches against the request path. + * @see RequestPredicates.POST + */ + fun POST(pattern: String): RequestPredicate = RequestPredicates.POST(pattern) + + /** + * Route to the given handler function if the given `PUT` predicate applies. + * @see RouterFunctions.route + */ + fun PUT(pattern: String, f: suspend (ServerRequest) -> ServerResponse) { + builder.PUT(pattern, asHandlerFunction(f)) + } + + /** + * Return a [RequestPredicate] that matches if request's HTTP method is `PUT` + * and the given `pattern` matches against the request path. + * @see RequestPredicates.PUT + */ + fun PUT(pattern: String): RequestPredicate = RequestPredicates.PUT(pattern) + + /** + * Route to the given handler function if the given `PATCH` predicate applies. + * @see RouterFunctions.route + */ + fun PATCH(pattern: String, f: suspend (ServerRequest) -> ServerResponse) { + builder.PATCH(pattern, asHandlerFunction(f)) + } + + /** + * Return a [RequestPredicate] that matches if request's HTTP method is `PATCH` + * and the given `pattern` matches against the request path. + * @param pattern the path pattern to match against + * @return a predicate that matches if the request method is `PATCH` and if the given pattern + * matches against the request path + */ + fun PATCH(pattern: String): RequestPredicate = RequestPredicates.PATCH(pattern) + + /** + * Route to the given handler function if the given `DELETE` predicate applies. + * @see RouterFunctions.route + */ + fun DELETE(pattern: String, f: suspend (ServerRequest) -> ServerResponse) { + builder.DELETE(pattern, asHandlerFunction(f)) + } + + /** + * Return a [RequestPredicate] that matches if request's HTTP method is `DELETE` + * and the given `pattern` matches against the request path. + * @param pattern the path pattern to match against + * @return a predicate that matches if the request method is `DELETE` and if the given pattern + * matches against the request path + */ + fun DELETE(pattern: String): RequestPredicate = RequestPredicates.DELETE(pattern) + + /** + * Route to the given handler function if the given OPTIONS predicate applies. + * @see RouterFunctions.route + */ + fun OPTIONS(pattern: String, f: suspend (ServerRequest) -> ServerResponse) { + builder.OPTIONS(pattern, asHandlerFunction(f)) + } + + /** + * Return a [RequestPredicate] that matches if request's HTTP method is `OPTIONS` + * and the given `pattern` matches against the request path. + * @param pattern the path pattern to match against + * @return a predicate that matches if the request method is `OPTIONS` and if the given pattern + * matches against the request path + */ + fun OPTIONS(pattern: String): RequestPredicate = RequestPredicates.OPTIONS(pattern) + + /** + * Route to the given handler function if the given accept predicate applies. + * @see RouterFunctions.route + */ + fun accept(mediaType: MediaType, f: suspend (ServerRequest) -> ServerResponse) { + builder.add(RouterFunctions.route(RequestPredicates.accept(mediaType), asHandlerFunction(f))) + } + + /** + * Return a [RequestPredicate] that tests if the request's + * [accept][ServerRequest.Headers.accept] } header is + * [compatible][MediaType.isCompatibleWith] with any of the given media types. + * @param mediaType the media types to match the request's accept header against + * @return a predicate that tests the request's accept header against the given media types + */ + fun accept(vararg mediaType: MediaType): RequestPredicate = RequestPredicates.accept(*mediaType) + + /** + * Route to the given handler function if the given contentType predicate applies. + * @see RouterFunctions.route + */ + fun contentType(mediaType: MediaType, f: suspend (ServerRequest) -> ServerResponse) { + builder.add(RouterFunctions.route(RequestPredicates.contentType(mediaType), asHandlerFunction(f))) + } + + /** + * Return a [RequestPredicate] that tests if the request's + * [content type][ServerRequest.Headers.contentType] is + * [included][MediaType.includes] by any of the given media types. + * @param mediaTypes the media types to match the request's content type against + * @return a predicate that tests the request's content type against the given media types + */ + fun contentType(vararg mediaTypes: MediaType): RequestPredicate = RequestPredicates.contentType(*mediaTypes) + + /** + * Route to the given handler function if the given headers predicate applies. + * @see RouterFunctions.route + */ + fun headers(headersPredicate: (ServerRequest.Headers) -> Boolean, f: suspend (ServerRequest) -> ServerResponse) { + builder.add(RouterFunctions.route(RequestPredicates.headers(headersPredicate), asHandlerFunction(f))) + } + + /** + * Return a [RequestPredicate] that tests the request's headers against the given headers predicate. + * @param headersPredicate a predicate that tests against the request headers + * @return a predicate that tests against the given header predicate + */ + fun headers(headersPredicate: (ServerRequest.Headers) -> Boolean): RequestPredicate = + RequestPredicates.headers(headersPredicate) + + /** + * Route to the given handler function if the given method predicate applies. + * @see RouterFunctions.route + */ + fun method(httpMethod: HttpMethod, f: suspend (ServerRequest) -> ServerResponse) { + builder.add(RouterFunctions.route(RequestPredicates.method(httpMethod), asHandlerFunction(f))) + } + + /** + * Return a [RequestPredicate] that tests against the given HTTP method. + * @param httpMethod the HTTP method to match to + * @return a predicate that tests against the given HTTP method + */ + fun method(httpMethod: HttpMethod): RequestPredicate = RequestPredicates.method(httpMethod) + + /** + * Route to the given handler function if the given path predicate applies. + * @see RouterFunctions.route + */ + fun path(pattern: String, f: suspend (ServerRequest) -> ServerResponse) { + builder.add(RouterFunctions.route(RequestPredicates.path(pattern), asHandlerFunction(f))) + } + + /** + * Return a [RequestPredicate] that tests the request path against the given path pattern. + * @see RequestPredicates.path + */ + fun path(pattern: String): RequestPredicate = RequestPredicates.path(pattern) + + /** + * Route to the given handler function if the given pathExtension predicate applies. + * @see RouterFunctions.route + */ + fun pathExtension(extension: String, f: suspend (ServerRequest) -> ServerResponse) { + builder.add(RouterFunctions.route(RequestPredicates.pathExtension(extension), asHandlerFunction(f))) + } + + /** + * Return a [RequestPredicate] that matches if the request's path has the given extension. + * @param extension the path extension to match against, ignoring case + * @return a predicate that matches if the request's path has the given file extension + */ + fun pathExtension(extension: String): RequestPredicate = RequestPredicates.pathExtension(extension) + + /** + * Route to the given handler function if the given pathExtension predicate applies. + * @see RouterFunctions.route + */ + fun pathExtension(predicate: (String) -> Boolean, f: suspend (ServerRequest) -> ServerResponse) { + builder.add(RouterFunctions.route(RequestPredicates.pathExtension(predicate), asHandlerFunction(f))) + } + + /** + * Return a [RequestPredicate] that matches if the request's path matches the given + * predicate. + * @see RequestPredicates.pathExtension + */ + fun pathExtension(predicate: (String) -> Boolean): RequestPredicate = + RequestPredicates.pathExtension(predicate) + + /** + * Route to the given handler function if the given queryParam predicate applies. + * @see RouterFunctions.route + */ + fun queryParam(name: String, predicate: (String) -> Boolean, f: suspend (ServerRequest) -> ServerResponse) { + builder.add(RouterFunctions.route(RequestPredicates.queryParam(name, predicate), asHandlerFunction(f))) + } + + /** + * Return a [RequestPredicate] that tests the request's query parameter of the given name + * against the given predicate. + * @param name the name of the query parameter to test against + * @param predicate predicate to test against the query parameter value + * @return a predicate that matches the given predicate against the query parameter of the given name + * @see ServerRequest#queryParam + */ + fun queryParam(name: String, predicate: (String) -> Boolean): RequestPredicate = + RequestPredicates.queryParam(name, predicate) + + /** + * Route to the given handler function if the given request predicate applies. + * @see RouterFunctions.route + */ + operator fun RequestPredicate.invoke(f: suspend (ServerRequest) -> ServerResponse) { + builder.add(RouterFunctions.route(this, asHandlerFunction(f))) + } + + /** + * Route to the given handler function if the given predicate (String + * processed as a path predicate) applies. + * @see RouterFunctions.route + */ + operator fun String.invoke(f: suspend (ServerRequest) -> ServerResponse) { + builder.add(RouterFunctions.route(RequestPredicates.path(this), asHandlerFunction(f))) + } + + /** + * Route requests that match the given pattern to resources relative to the given root location. + * @see RouterFunctions.resources + */ + fun resources(path: String, location: Resource) { + builder.resources(path, location) + } + + /** + * Route to resources using the provided lookup function. If the lookup function provides a + * [Resource] for the given request, it will be it will be exposed using a + * [HandlerFunction] that handles GET, HEAD, and OPTIONS requests. + */ + fun resources(lookupFunction: suspend (ServerRequest) -> Resource?) { + builder.resources { + GlobalScope.mono { + lookupFunction.invoke(it) + } + } + } + + override fun invoke(): RouterFunction { + init() + return builder.build() + } + + private fun asHandlerFunction(init: suspend (ServerRequest) -> ServerResponse) = HandlerFunction { + GlobalScope.mono { + init(it) + } + } + + /** + * @see ServerResponse.from + */ + fun from(other: ServerResponse) = + ServerResponse.from(other) + + /** + * @see ServerResponse.created + */ + fun created(location: URI) = + ServerResponse.created(location) + + /** + * @see ServerResponse.ok + */ + fun ok() = ServerResponse.ok() + + /** + * @see ServerResponse.noContent + */ + fun noContent() = ServerResponse.noContent() + + /** + * @see ServerResponse.accepted + */ + fun accepted() = ServerResponse.accepted() + + /** + * @see ServerResponse.permanentRedirect + */ + fun permanentRedirect(location: URI) = ServerResponse.permanentRedirect(location) + + /** + * @see ServerResponse.temporaryRedirect + */ + fun temporaryRedirect(location: URI) = ServerResponse.temporaryRedirect(location) + + /** + * @see ServerResponse.seeOther + */ + fun seeOther(location: URI) = ServerResponse.seeOther(location) + + /** + * @see ServerResponse.badRequest + */ + fun badRequest() = ServerResponse.badRequest() + + /** + * @see ServerResponse.notFound + */ + fun notFound() = ServerResponse.notFound() + + /** + * @see ServerResponse.unprocessableEntity + */ + fun unprocessableEntity() = ServerResponse.unprocessableEntity() + + /** + * @see ServerResponse.status + */ + fun status(status: HttpStatus) = ServerResponse.status(status) + + /** + * @see ServerResponse.status + */ + fun status(status: Int) = ServerResponse.status(status) + +} + +operator fun RouterFunction.plus(other: RouterFunction) = + this.and(other) + +/** + * Coroutines variant of [router]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +fun coRouter(routes: (CoRouterFunctionDsl.() -> Unit)) = + CoRouterFunctionDsl(routes).invoke() \ No newline at end of file diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/RenderingResponseExtensions.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/RenderingResponseExtensions.kt new file mode 100644 index 00000000000..ffe7bf4f37f --- /dev/null +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/RenderingResponseExtensions.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2002-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.function.server + +import kotlinx.coroutines.reactive.awaitSingle + +/** + * Coroutines variant of [RenderingResponse.Builder.build]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +suspend fun RenderingResponse.Builder.buildAndAwait(): RenderingResponse = build().awaitSingle() \ No newline at end of file diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/ServerRequestExtensions.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/ServerRequestExtensions.kt index 0f0cc51e449..7b6f0344bb8 100644 --- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/ServerRequestExtensions.kt +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/ServerRequestExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,15 @@ package org.springframework.web.reactive.function.server +import kotlinx.coroutines.reactive.awaitFirstOrNull +import kotlinx.coroutines.reactive.awaitSingle import org.springframework.core.ParameterizedTypeReference +import org.springframework.http.codec.multipart.Part +import org.springframework.util.MultiValueMap +import org.springframework.web.server.WebSession import reactor.core.publisher.Flux import reactor.core.publisher.Mono +import java.security.Principal /** * Extension for [ServerRequest.bodyToMono] providing a `bodyToMono()` variant @@ -41,3 +47,48 @@ inline fun ServerRequest.bodyToMono(): Mono = */ inline fun ServerRequest.bodyToFlux(): Flux = bodyToFlux(object : ParameterizedTypeReference() {}) + +/** + * Coroutines variant of [ServerRequest.bodyToMono]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +suspend inline fun ServerRequest.awaitBody(): T? = + bodyToMono().awaitFirstOrNull() + +/** + * Coroutines variant of [ServerRequest.formData]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +suspend fun ServerRequest.awaitFormData(): MultiValueMap = + formData().awaitSingle() + +/** + * Coroutines variant of [ServerRequest.multipartData]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +suspend fun ServerRequest.awaitMultipartData(): MultiValueMap = + multipartData().awaitSingle() + +/** + * Coroutines variant of [ServerRequest.principal]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +suspend fun ServerRequest.awaitPrincipal(): Principal = + principal().awaitSingle() + +/** + * Coroutines variant of [ServerRequest.session]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +suspend fun ServerRequest.awaitSession(): WebSession = + session().awaitSingle() diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensions.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensions.kt index e2205d0f589..26eb4126b0a 100644 --- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensions.kt +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.web.reactive.function.server +import kotlinx.coroutines.reactive.awaitSingle import org.reactivestreams.Publisher import org.springframework.core.ParameterizedTypeReference import org.springframework.http.MediaType @@ -63,3 +64,42 @@ fun ServerResponse.BodyBuilder.xml() = contentType(MediaType.APPLICATION_XML) * @since 5.1 */ fun ServerResponse.BodyBuilder.html() = contentType(MediaType.TEXT_HTML) + +/** + * Coroutines variant of [ServerResponse.HeadersBuilder.build]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +suspend fun ServerResponse.HeadersBuilder>.buildAndAwait(): ServerResponse = + build().awaitSingle() + +/** + * Coroutines variant of [ServerResponse.BodyBuilder.syncBody]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +suspend fun ServerResponse.BodyBuilder.bodyAndAwait(body: Any): ServerResponse = + syncBody(body).awaitSingle() + + +/** + * Coroutines variant of [ServerResponse.BodyBuilder.syncBody] without the sync prefix since it is ok to use it within + * another suspendable function. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +suspend fun ServerResponse.BodyBuilder.renderAndAwait(name: String, vararg modelAttributes: String): ServerResponse = + render(name, *modelAttributes).awaitSingle() + +/** + * Coroutines variant of [ServerResponse.BodyBuilder.syncBody] without the sync prefix since it is ok to use it within + * another suspendable function. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +suspend fun ServerResponse.BodyBuilder.renderAndAwait(name: String, model: Map): ServerResponse = + render(name, model).awaitSingle() diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/server/ServerWebExchangeExtensions.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/server/ServerWebExchangeExtensions.kt new file mode 100644 index 00000000000..fc0612f84dc --- /dev/null +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/server/ServerWebExchangeExtensions.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.server + +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.reactive.awaitSingle +import kotlinx.coroutines.reactor.mono +import org.springframework.http.codec.multipart.Part +import org.springframework.util.MultiValueMap +import org.springframework.web.server.ServerWebExchange +import org.springframework.web.server.WebSession +import java.security.Principal + +/** + * Coroutines variant of [ServerWebExchange.getFormData]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +suspend fun ServerWebExchange.awaitFormData(): MultiValueMap = + this.formData.awaitSingle() + +/** + * Coroutines variant of [ServerWebExchange.getMultipartData]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +suspend fun ServerWebExchange.awaitMultipartData(): MultiValueMap = + this.multipartData.awaitSingle() + +/** + * Coroutines variant of [ServerWebExchange.getPrincipal]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +suspend fun ServerWebExchange.awaitPrincipal(): T = + this.getPrincipal().awaitSingle() + +/** + * Coroutines variant of [ServerWebExchange.getSession]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +suspend fun ServerWebExchange.awaitSession(): WebSession = + this.session.awaitSingle() + +/** + * Coroutines variant of [ServerWebExchange.Builder.principal]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +fun ServerWebExchange.Builder.principal(supplier: suspend () -> Principal): ServerWebExchange.Builder + = principal(GlobalScope.mono { supplier.invoke() }) diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/ClientResponseExtensionsTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/ClientResponseExtensionsTests.kt index 090ea5d3567..99cf8ff5b07 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/ClientResponseExtensionsTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/ClientResponseExtensionsTests.kt @@ -16,10 +16,16 @@ package org.springframework.web.reactive.function.client +import io.mockk.every import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals import org.junit.Test import org.springframework.core.ParameterizedTypeReference +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import reactor.core.publisher.Mono /** * Mock object based tests for [ClientResponse] Kotlin extensions. @@ -28,7 +34,7 @@ import org.springframework.core.ParameterizedTypeReference */ class ClientResponseExtensionsTests { - val response = mockk(relaxed = true) + private val response = mockk(relaxed = true) @Test fun `bodyToMono with reified type parameters`() { @@ -54,5 +60,34 @@ class ClientResponseExtensionsTests { verify { response.toEntityList(object : ParameterizedTypeReference>() {}) } } + @Test + fun awaitBody() { + val response = mockk() + every { response.bodyToMono() } returns Mono.just("foo") + runBlocking { + assertEquals("foo", response.awaitBody()) + } + } + + @Test + fun awaitEntity() { + val response = mockk() + val entity = ResponseEntity("foo", HttpStatus.OK) + every { response.toEntity() } returns Mono.just(entity) + runBlocking { + assertEquals(entity, response.awaitEntity()) + } + } + + @Test + fun awaitEntityList() { + val response = mockk() + val entity = ResponseEntity(listOf("foo"), HttpStatus.OK) + every { response.toEntityList() } returns Mono.just(entity) + runBlocking { + assertEquals(entity, response.awaitEntityList()) + } + } + class Foo } diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/WebClientExtensionsTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/WebClientExtensionsTests.kt index ddeacb649fd..8aec19bc571 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/WebClientExtensionsTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/WebClientExtensionsTests.kt @@ -16,11 +16,15 @@ package org.springframework.web.reactive.function.client +import io.mockk.every import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals import org.junit.Test import org.reactivestreams.Publisher import org.springframework.core.ParameterizedTypeReference +import reactor.core.publisher.Mono /** * Mock object based tests for [WebClient] Kotlin extensions @@ -29,9 +33,9 @@ import org.springframework.core.ParameterizedTypeReference */ class WebClientExtensionsTests { - val requestBodySpec = mockk(relaxed = true) + private val requestBodySpec = mockk(relaxed = true) - val responseSpec = mockk(relaxed = true) + private val responseSpec = mockk(relaxed = true) @Test @@ -53,5 +57,36 @@ class WebClientExtensionsTests { verify { responseSpec.bodyToFlux(object : ParameterizedTypeReference>() {}) } } + @Test + fun awaitResponse() { + val response = mockk() + every { requestBodySpec.exchange() } returns Mono.just(response) + runBlocking { + assertEquals(response, requestBodySpec.awaitResponse()) + } + } + + @Test + fun body() { + val headerSpec = mockk>() + val supplier: suspend () -> String = mockk() + every { requestBodySpec.body(ofType>()) } returns headerSpec + runBlocking { + requestBodySpec.body(supplier) + } + verify { + requestBodySpec.body(ofType>()) + } + } + + @Test + fun awaitBody() { + val spec = mockk() + every { spec.bodyToMono() } returns Mono.just("foo") + runBlocking { + assertEquals("foo", spec.awaitBody()) + } + } + class Foo } diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt new file mode 100644 index 00000000000..933d84f33ea --- /dev/null +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt @@ -0,0 +1,166 @@ +/* + * Copyright 2002-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.function.server + +import org.junit.Test +import org.springframework.core.io.ClassPathResource +import org.springframework.http.HttpHeaders.* +import org.springframework.http.HttpMethod.* +import org.springframework.http.MediaType.* +import org.springframework.web.reactive.function.server.MockServerRequest.builder +import reactor.test.StepVerifier +import java.net.URI + +/** + * Tests for [CoRouterFunctionDsl]. + * + * @author Sebastien Deleuze + */ +class CoRouterFunctionDslTests { + + @Test + fun header() { + val request = builder().header("bar", "bar").build() + StepVerifier.create(sampleRouter().route(request)) + .expectNextCount(1) + .verifyComplete() + } + + @Test + fun accept() { + val request = builder().uri(URI("/content")).header(ACCEPT, APPLICATION_ATOM_XML_VALUE).build() + StepVerifier.create(sampleRouter().route(request)) + .expectNextCount(1) + .verifyComplete() + } + + @Test + fun acceptAndPOST() { + val request = builder() + .method(POST) + .uri(URI("/api/foo/")) + .header(ACCEPT, APPLICATION_JSON_VALUE) + .build() + StepVerifier.create(sampleRouter().route(request)) + .expectNextCount(1) + .verifyComplete() + } + + @Test + fun contentType() { + val request = builder().uri(URI("/content")).header(CONTENT_TYPE, APPLICATION_OCTET_STREAM_VALUE).build() + StepVerifier.create(sampleRouter().route(request)) + .expectNextCount(1) + .verifyComplete() + } + + @Test + fun resourceByPath() { + val request = builder().uri(URI("/org/springframework/web/reactive/function/response.txt")).build() + StepVerifier.create(sampleRouter().route(request)) + .expectNextCount(1) + .verifyComplete() + } + + @Test + fun method() { + val request = builder().method(PATCH).build() + StepVerifier.create(sampleRouter().route(request)) + .expectNextCount(1) + .verifyComplete() + } + + @Test + fun path() { + val request = builder().uri(URI("/baz")).build() + StepVerifier.create(sampleRouter().route(request)) + .expectNextCount(1) + .verifyComplete() + } + + @Test + fun resource() { + val request = builder().uri(URI("/response.txt")).build() + StepVerifier.create(sampleRouter().route(request)) + .expectNextCount(1) + .verifyComplete() + } + + @Test + fun noRoute() { + val request = builder() + .uri(URI("/bar")) + .header(ACCEPT, APPLICATION_PDF_VALUE) + .header(CONTENT_TYPE, APPLICATION_PDF_VALUE) + .build() + StepVerifier.create(sampleRouter().route(request)) + .verifyComplete() + } + + @Test + fun rendering() { + val request = builder().uri(URI("/rendering")).build() + StepVerifier.create(sampleRouter().route(request).flatMap { it.handle(request) }) + .expectNextMatches { it is RenderingResponse} + .verifyComplete() + } + + @Test(expected = IllegalStateException::class) + fun emptyRouter() { + router { } + } + + + private fun sampleRouter() = coRouter { + (GET("/foo/") or GET("/foos/")) { req -> handle(req) } + "/api".nest { + POST("/foo/", ::handleFromClass) + PUT("/foo/", :: handleFromClass) + PATCH("/foo/") { + ok().buildAndAwait() + } + "/foo/" { handleFromClass(it) } + } + "/content".nest { + accept(APPLICATION_ATOM_XML, ::handle) + contentType(APPLICATION_OCTET_STREAM, ::handle) + } + method(PATCH, ::handle) + headers { it.accept().contains(APPLICATION_JSON) }.nest { + GET("/api/foo/", ::handle) + } + headers({ it.header("bar").isNotEmpty() }, ::handle) + resources("/org/springframework/web/reactive/function/**", + ClassPathResource("/org/springframework/web/reactive/function/response.txt")) + resources { + if (it.path() == "/response.txt") { + ClassPathResource("/org/springframework/web/reactive/function/response.txt") + } + else { + null + } + } + path("/baz", ::handle) + GET("/rendering") { RenderingResponse.create("index").buildAndAwait() } + } +} + +@Suppress("UNUSED_PARAMETER") +private suspend fun handleFromClass(req: ServerRequest) = ServerResponse.ok().buildAndAwait() + +@Suppress("UNUSED_PARAMETER") +private suspend fun handle(req: ServerRequest) = ServerResponse.ok().buildAndAwait() diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/RenderingResponseExtensionsTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/RenderingResponseExtensionsTests.kt new file mode 100644 index 00000000000..7790687f3a2 --- /dev/null +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/RenderingResponseExtensionsTests.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.function.server + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.runBlocking +import org.junit.Test +import reactor.core.publisher.Mono + +class RenderingResponseExtensionsTests { + + @Test + fun buildAndAwait() { + val builder = mockk() + val response = mockk() + every { builder.build() } returns Mono.just(response) + runBlocking { + builder.buildAndAwait() + } + verify { + builder.build() + } + } + +} \ No newline at end of file diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/RouterFunctionDslTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/RouterFunctionDslTests.kt index 1a5efcd08e6..ee2ea61ac30 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/RouterFunctionDslTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/RouterFunctionDslTests.kt @@ -27,7 +27,7 @@ import reactor.test.StepVerifier import java.net.URI /** - * Tests for [RouterFunction] Kotlin DSL. + * Tests for [RouterFunctionDsl]. * * @author Sebastien Deleuze */ @@ -161,7 +161,7 @@ class RouterFunctionDslTests { } @Suppress("UNUSED_PARAMETER") -fun handleFromClass(req: ServerRequest) = ServerResponse.ok().build() +private fun handleFromClass(req: ServerRequest) = ServerResponse.ok().build() @Suppress("UNUSED_PARAMETER") -fun handle(req: ServerRequest) = ServerResponse.ok().build() +private fun handle(req: ServerRequest) = ServerResponse.ok().build() diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/ServerRequestExtensionsTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/ServerRequestExtensionsTests.kt index 8937dcc3003..2574db294a8 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/ServerRequestExtensionsTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/ServerRequestExtensionsTests.kt @@ -16,13 +16,20 @@ package org.springframework.web.reactive.function.client +import io.mockk.every import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.junit.Test import org.springframework.core.ParameterizedTypeReference -import org.springframework.web.reactive.function.server.ServerRequest -import org.springframework.web.reactive.function.server.bodyToFlux -import org.springframework.web.reactive.function.server.bodyToMono +import org.springframework.http.codec.multipart.Part +import org.springframework.util.MultiValueMap +import org.springframework.web.reactive.function.server.* +import org.springframework.web.server.WebSession +import reactor.core.publisher.Mono +import java.security.Principal /** * Mock object based tests for [ServerRequest] Kotlin extensions. @@ -45,5 +52,58 @@ class ServerRequestExtensionsTests { verify { request.bodyToFlux(object : ParameterizedTypeReference>() {}) } } + @Test + fun awaitBody() { + every { request.bodyToMono() } returns Mono.just("foo") + runBlocking { + assertEquals("foo", request.awaitBody()) + } + } + + @Test + fun awaitBodyNull() { + every { request.bodyToMono() } returns Mono.empty() + runBlocking { + assertNull(request.awaitBody()) + } + } + + @Test + fun awaitFormData() { + val map = mockk>() + every { request.formData() } returns Mono.just(map) + runBlocking { + assertEquals(map, request.awaitFormData()) + } + } + + @Test + fun awaitMultipartData() { + val map = mockk>() + every { request.multipartData() } returns Mono.just(map) + runBlocking { + assertEquals(map, request.awaitMultipartData()) + } + } + + @Test + fun awaitPrincipal() { + val principal = mockk() + every { request.principal() } returns Mono.just(principal) + runBlocking { + assertEquals(principal, request.awaitPrincipal()) + } + } + + @Test + fun awaitSession() { + val session = mockk() + every { request.session() } returns Mono.just(session) + runBlocking { + assertEquals(session, request.awaitSession()) + } + } + + class Foo } diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensionsTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensionsTests.kt index ef4c4a6e388..307df20238e 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensionsTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensionsTests.kt @@ -16,12 +16,16 @@ package org.springframework.web.reactive.function.server +import io.mockk.every import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals import org.junit.Test import org.reactivestreams.Publisher import org.springframework.core.ParameterizedTypeReference import org.springframework.http.MediaType.* +import reactor.core.publisher.Mono /** * Mock object based tests for [ServerResponse] Kotlin extensions @@ -30,7 +34,7 @@ import org.springframework.http.MediaType.* */ class ServerResponseExtensionsTests { - val bodyBuilder = mockk(relaxed = true) + private val bodyBuilder = mockk(relaxed = true) @Test @@ -65,5 +69,53 @@ class ServerResponseExtensionsTests { verify { bodyBuilder.contentType(TEXT_HTML) } } + @Test + fun await() { + val response = mockk() + val builder = mockk>() + every { builder.build() } returns Mono.just(response) + runBlocking { + assertEquals(response, builder.buildAndAwait()) + } + } + + @Test + fun `bodyAndAwait with object parameter`() { + val response = mockk() + val body = "foo" + every { bodyBuilder.syncBody(ofType()) } returns Mono.just(response) + runBlocking { + bodyBuilder.bodyAndAwait(body) + } + verify { + bodyBuilder.syncBody(ofType()) + } + } + + @Test + fun `renderAndAwait with a vararg parameter`() { + val response = mockk() + every { bodyBuilder.render("foo", any(), any()) } returns Mono.just(response) + runBlocking { + bodyBuilder.renderAndAwait("foo", "bar", "baz") + } + verify { + bodyBuilder.render("foo", any(), any()) + } + } + + @Test + fun `renderAndAwait with a Map parameter`() { + val response = mockk() + val map = mockk>() + every { bodyBuilder.render("foo", map) } returns Mono.just(response) + runBlocking { + bodyBuilder.renderAndAwait("foo", map) + } + verify { + bodyBuilder.render("foo", map) + } + } + class Foo } diff --git a/src/docs/asciidoc/languages/kotlin.adoc b/src/docs/asciidoc/languages/kotlin.adoc index 691034ac39b..057117abe34 100644 --- a/src/docs/asciidoc/languages/kotlin.adoc +++ b/src/docs/asciidoc/languages/kotlin.adoc @@ -20,10 +20,10 @@ https://stackoverflow.com/questions/tagged/spring+kotlin[Stackoverflow] if you n [[kotlin-requirements]] == Requirements -The Spring Framework supports Kotlin 1.1+ and requires +The Spring Framework supports Kotlin 1.3+ and requires https://bintray.com/bintray/jcenter/org.jetbrains.kotlin%3Akotlin-stdlib[`kotlin-stdlib`] -(or one of its variants, such as https://bintray.com/bintray/jcenter/org.jetbrains.kotlin%3Akotlin-stdlib-jre8[`kotlin-stdlib-jre8`] -for Kotlin 1.1 or https://bintray.com/bintray/jcenter/org.jetbrains.kotlin%3Akotlin-stdlib-jdk8[`kotlin-stdlib-jdk8`] for Kotlin 1.2+) +(or one of its variants, such as +https://bintray.com/bintray/jcenter/org.jetbrains.kotlin%3Akotlin-stdlib-jdk8[`kotlin-stdlib-jdk8`]) and https://bintray.com/bintray/jcenter/org.jetbrains.kotlin%3Akotlin-reflect[`kotlin-reflect`] to be present on the classpath. They are provided by default if you bootstrap a Kotlin project on https://start.spring.io/#!language=kotlin[start.spring.io]. @@ -255,17 +255,19 @@ for more details and up-to-date information. == Web +=== WebFlux router DSL -=== WebFlux Functional DSL +Spring Framework comes with a Kotlin router DSL available in 2 flavors: -Spring Framework now comes with a -{doc-root}/spring-framework/docs/{spring-version}/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/-router-function-dsl/[Kotlin routing DSL] -that lets you use the <> to write clean and idiomatic Kotlin code, -as the following example shows: + - Reactive with {doc-root}/spring-framework/docs/{spring-version}/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/-router-function-dsl/[router { }] + - <> + +These DSL let you use the <> to write clean and idiomatic Kotlin code +to build a `RouterFunction` instance as the following example shows: [source,kotlin,indent=0] ---- - router { + val routes: RouterFunction = router { accept(TEXT_HTML).nest { GET("/") { ok().render("index") } GET("/sse") { ok().render("sse") } @@ -290,7 +292,38 @@ depending on dynamic data (for example, from a database). See https://github.com/mixitconf/mixit/tree/dafd5ccc92dfab6d9c306fcb60b28921a1ccbf79/src/main/kotlin/mixit/web/routes[MiXiT project routes] for a concrete example. +=== Coroutines +As of Spring Framework 5.2, https://kotlinlang.org/docs/reference/coroutines-overview.html[Coroutines support] +is provided via extensions for WebFlux client and server functional API. A dedicated +{doc-root}/spring-framework/docs/{spring-version}/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/-co-router-function-dsl/[`coRouter { }`] +router DSL is also available. + +Coroutines extensions use `await` prefix or `AndAwait` suffix, and most are using similar name than their Reactive +counterparts except `exchange` in `WebClient.RequestHeadersSpec` which translates to `awaitResponse`. + +[source,kotlin,indent=0] +---- +fun routes(userHandler: UserHandler): RouterFunction = coRouter { + GET("/", userHandler::listView) + GET("/api/user", userHandler::listApi) +} + +class UserHandler(private val client: WebClient) { + + suspend fun listApi(request: ServerRequest): ServerResponse = + ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).bodyAndAwait( + client.get().uri("...").awaitResponse().awaitBody()) + + suspend fun listView(request: ServerRequest): ServerResponse = + ServerResponse.ok().renderAndAwait("users", mapOf("users" to + client.get().uri("...").awaitResponse().awaitBody())) + +} +---- + +Read this https://medium.com/@elizarov/structured-concurrency-722d765aa952[structured concurrency blog post] +to understand how to run code concurrently with Coroutines. === Kotlin Script Templates