Add Coroutines support for WebClient and WebFlux.fn
This commit is the first part of a more complete Coroutines
support coming in Spring Framework 5.2. It introduces suspendable
Kotlin extensions for Mono based methods in WebFlux classes like
WebClient, ServerRequest, ServerResponse as well as a Coroutines
router usable via `coRouter { }`.
Coroutines extensions use `await` prefix or `AndAwait` suffix,
and most are using names close to their Reactive counterparts,
except `exchange` in `WebClient.RequestHeadersSpec`
which translates to `awaitResponse`.
Upcoming expected changes are:
- Leverage `Dispatchers.Unconfined` (Kotlin/kotlinx.coroutines#972)
- Expose extensions for `Flux` based API (Kotlin/kotlinx.coroutines#254)
- Introduce interop with `CoroutineContext` (Kotlin/kotlinx.coroutines#284)
- Support Coroutines in `ReactiveAdapterRegistry`
- Support Coroutines for WebFlux annotated controllers
- Fix return type of Kotlin suspending functions (gh-21058)
See gh-19975
This commit is contained in:
parent
04bb114f05
commit
19f792db66
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 <reified T : Any> ClientResponse.toEntity(): Mono<ResponseEntity<T>>
|
|||
*/
|
||||
inline fun <reified T : Any> ClientResponse.toEntityList(): Mono<ResponseEntity<List<T>>> =
|
||||
toEntityList(object : ParameterizedTypeReference<T>() {})
|
||||
|
||||
/**
|
||||
* Coroutines variant of [ClientResponse.bodyToMono].
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.2
|
||||
*/
|
||||
suspend inline fun <reified T : Any> ClientResponse.awaitBody(): T =
|
||||
bodyToMono<T>().awaitSingle()
|
||||
|
||||
/**
|
||||
* Coroutines variant of [ClientResponse.toEntity].
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.2
|
||||
*/
|
||||
suspend inline fun <reified T : Any> ClientResponse.awaitEntity(): ResponseEntity<T> =
|
||||
toEntity<T>().awaitSingle()
|
||||
|
||||
/**
|
||||
* Coroutines variant of [ClientResponse.toEntityList].
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.2
|
||||
*/
|
||||
suspend inline fun <reified T : Any> ClientResponse.awaitEntityList(): ResponseEntity<List<T>> =
|
||||
toEntityList<T>().awaitSingle()
|
||||
|
|
|
|||
|
|
@ -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 <reified T : Any> WebClient.ResponseSpec.bodyToMono(): Mono<T> =
|
|||
*/
|
||||
inline fun <reified T : Any> WebClient.ResponseSpec.bodyToFlux(): Flux<T> =
|
||||
bodyToFlux(object : ParameterizedTypeReference<T>() {})
|
||||
|
||||
/**
|
||||
* Coroutines variant of [WebClient.RequestHeadersSpec.exchange].
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.2
|
||||
*/
|
||||
suspend fun WebClient.RequestHeadersSpec<out WebClient.RequestHeadersSpec<*>>.awaitResponse(): ClientResponse =
|
||||
exchange().awaitSingle()
|
||||
|
||||
/**
|
||||
* Coroutines variant of [WebClient.RequestBodySpec.body].
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.2
|
||||
*/
|
||||
inline fun <reified T: Any> 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 <reified T : Any> WebClient.ResponseSpec.awaitBody() : T =
|
||||
bodyToMono<T>().awaitSingle()
|
||||
|
|
|
|||
|
|
@ -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<ServerResponse> {
|
||||
|
||||
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<ServerResponse> {
|
||||
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 <T: ServerResponse> RouterFunction<T>.plus(other: RouterFunction<T>) =
|
||||
this.and(other)
|
||||
|
||||
/**
|
||||
* Coroutines variant of [router].
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.2
|
||||
*/
|
||||
fun coRouter(routes: (CoRouterFunctionDsl.() -> Unit)) =
|
||||
CoRouterFunctionDsl(routes).invoke()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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<Foo>()` variant
|
||||
|
|
@ -41,3 +47,48 @@ inline fun <reified T : Any> ServerRequest.bodyToMono(): Mono<T> =
|
|||
*/
|
||||
inline fun <reified T : Any> ServerRequest.bodyToFlux(): Flux<T> =
|
||||
bodyToFlux(object : ParameterizedTypeReference<T>() {})
|
||||
|
||||
/**
|
||||
* Coroutines variant of [ServerRequest.bodyToMono].
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.2
|
||||
*/
|
||||
suspend inline fun <reified T : Any> ServerRequest.awaitBody(): T? =
|
||||
bodyToMono<T>().awaitFirstOrNull()
|
||||
|
||||
/**
|
||||
* Coroutines variant of [ServerRequest.formData].
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.2
|
||||
*/
|
||||
suspend fun ServerRequest.awaitFormData(): MultiValueMap<String, String> =
|
||||
formData().awaitSingle()
|
||||
|
||||
/**
|
||||
* Coroutines variant of [ServerRequest.multipartData].
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.2
|
||||
*/
|
||||
suspend fun ServerRequest.awaitMultipartData(): MultiValueMap<String, Part> =
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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<out 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<String, *>): ServerResponse =
|
||||
render(name, model).awaitSingle()
|
||||
|
|
|
|||
|
|
@ -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<String, String> =
|
||||
this.formData.awaitSingle()
|
||||
|
||||
/**
|
||||
* Coroutines variant of [ServerWebExchange.getMultipartData].
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.2
|
||||
*/
|
||||
suspend fun ServerWebExchange.awaitMultipartData(): MultiValueMap<String, Part> =
|
||||
this.multipartData.awaitSingle()
|
||||
|
||||
/**
|
||||
* Coroutines variant of [ServerWebExchange.getPrincipal].
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.2
|
||||
*/
|
||||
suspend fun <T : Principal> ServerWebExchange.awaitPrincipal(): T =
|
||||
this.getPrincipal<T>().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() })
|
||||
|
|
@ -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<ClientResponse>(relaxed = true)
|
||||
private val response = mockk<ClientResponse>(relaxed = true)
|
||||
|
||||
@Test
|
||||
fun `bodyToMono with reified type parameters`() {
|
||||
|
|
@ -54,5 +60,34 @@ class ClientResponseExtensionsTests {
|
|||
verify { response.toEntityList(object : ParameterizedTypeReference<List<Foo>>() {}) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun awaitBody() {
|
||||
val response = mockk<ClientResponse>()
|
||||
every { response.bodyToMono<String>() } returns Mono.just("foo")
|
||||
runBlocking {
|
||||
assertEquals("foo", response.awaitBody<String>())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun awaitEntity() {
|
||||
val response = mockk<ClientResponse>()
|
||||
val entity = ResponseEntity("foo", HttpStatus.OK)
|
||||
every { response.toEntity<String>() } returns Mono.just(entity)
|
||||
runBlocking {
|
||||
assertEquals(entity, response.awaitEntity<String>())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun awaitEntityList() {
|
||||
val response = mockk<ClientResponse>()
|
||||
val entity = ResponseEntity(listOf("foo"), HttpStatus.OK)
|
||||
every { response.toEntityList<String>() } returns Mono.just(entity)
|
||||
runBlocking {
|
||||
assertEquals(entity, response.awaitEntityList<String>())
|
||||
}
|
||||
}
|
||||
|
||||
class Foo
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<WebClient.RequestBodySpec>(relaxed = true)
|
||||
private val requestBodySpec = mockk<WebClient.RequestBodySpec>(relaxed = true)
|
||||
|
||||
val responseSpec = mockk<WebClient.ResponseSpec>(relaxed = true)
|
||||
private val responseSpec = mockk<WebClient.ResponseSpec>(relaxed = true)
|
||||
|
||||
|
||||
@Test
|
||||
|
|
@ -53,5 +57,36 @@ class WebClientExtensionsTests {
|
|||
verify { responseSpec.bodyToFlux(object : ParameterizedTypeReference<List<Foo>>() {}) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun awaitResponse() {
|
||||
val response = mockk<ClientResponse>()
|
||||
every { requestBodySpec.exchange() } returns Mono.just(response)
|
||||
runBlocking {
|
||||
assertEquals(response, requestBodySpec.awaitResponse())
|
||||
}
|
||||
}
|
||||
|
||||
@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>()
|
||||
every { spec.bodyToMono<String>() } returns Mono.just("foo")
|
||||
runBlocking {
|
||||
assertEquals("foo", spec.awaitBody<String>())
|
||||
}
|
||||
}
|
||||
|
||||
class Foo
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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<RenderingResponse.Builder>()
|
||||
val response = mockk<RenderingResponse>()
|
||||
every { builder.build() } returns Mono.just(response)
|
||||
runBlocking {
|
||||
builder.buildAndAwait()
|
||||
}
|
||||
verify {
|
||||
builder.build()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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<List<Foo>>() {}) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun awaitBody() {
|
||||
every { request.bodyToMono<String>() } returns Mono.just("foo")
|
||||
runBlocking {
|
||||
assertEquals("foo", request.awaitBody<String>())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun awaitBodyNull() {
|
||||
every { request.bodyToMono<String>() } returns Mono.empty()
|
||||
runBlocking {
|
||||
assertNull(request.awaitBody<String>())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun awaitFormData() {
|
||||
val map = mockk<MultiValueMap<String, String>>()
|
||||
every { request.formData() } returns Mono.just(map)
|
||||
runBlocking {
|
||||
assertEquals(map, request.awaitFormData())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun awaitMultipartData() {
|
||||
val map = mockk<MultiValueMap<String, Part>>()
|
||||
every { request.multipartData() } returns Mono.just(map)
|
||||
runBlocking {
|
||||
assertEquals(map, request.awaitMultipartData())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun awaitPrincipal() {
|
||||
val principal = mockk<Principal>()
|
||||
every { request.principal() } returns Mono.just(principal)
|
||||
runBlocking {
|
||||
assertEquals(principal, request.awaitPrincipal())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun awaitSession() {
|
||||
val session = mockk<WebSession>()
|
||||
every { request.session() } returns Mono.just(session)
|
||||
runBlocking {
|
||||
assertEquals(session, request.awaitSession())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Foo
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ServerResponse.BodyBuilder>(relaxed = true)
|
||||
private val bodyBuilder = mockk<ServerResponse.BodyBuilder>(relaxed = true)
|
||||
|
||||
|
||||
@Test
|
||||
|
|
@ -65,5 +69,53 @@ class ServerResponseExtensionsTests {
|
|||
verify { bodyBuilder.contentType(TEXT_HTML) }
|
||||
}
|
||||
|
||||
@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
|
||||
fun `renderAndAwait with a vararg parameter`() {
|
||||
val response = mockk<ServerResponse>()
|
||||
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<ServerResponse>()
|
||||
val map = mockk<Map<String, *>>()
|
||||
every { bodyBuilder.render("foo", map) } returns Mono.just(response)
|
||||
runBlocking {
|
||||
bodyBuilder.renderAndAwait("foo", map)
|
||||
}
|
||||
verify {
|
||||
bodyBuilder.render("foo", map)
|
||||
}
|
||||
}
|
||||
|
||||
class Foo
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <<web-reactive#webflux-fn,WebFlux functional API>> 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 { }]
|
||||
- <<Coroutines>>
|
||||
|
||||
These DSL let you use the <<web-reactive#webflux-fn,WebFlux functional API>> 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<ServerResponse> = 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<ServerResponse> = 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<User>())
|
||||
|
||||
suspend fun listView(request: ServerRequest): ServerResponse =
|
||||
ServerResponse.ok().renderAndAwait("users", mapOf("users" to
|
||||
client.get().uri("...").awaitResponse().awaitBody<User>()))
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue