From 02409f74b912c51a666c5afc664470da432f130c Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Fri, 13 Jan 2017 16:57:47 +0100 Subject: [PATCH] Polish Kotlin routing DSL This commit changes RouterFunctionDsl {...} (request) to route(request) {...}, remove uneeded methods, add missing ones and add tests useful to validate the DSL syntax and behavior. Issue: SPR-15065 --- ...tionDsl.kt => RouterFunctionExtensions.kt} | 71 +++++----- .../server/RouterFunctionExtensionsTests.kt | 129 ++++++++++++++++++ 2 files changed, 165 insertions(+), 35 deletions(-) rename spring-web-reactive/src/main/kotlin/org/springframework/web/reactive/function/server/{RouterFunctionDsl.kt => RouterFunctionExtensions.kt} (66%) create mode 100644 spring-web-reactive/src/test/kotlin/org/springframework/web/reactive/function/server/RouterFunctionExtensionsTests.kt diff --git a/spring-web-reactive/src/main/kotlin/org/springframework/web/reactive/function/server/RouterFunctionDsl.kt b/spring-web-reactive/src/main/kotlin/org/springframework/web/reactive/function/server/RouterFunctionExtensions.kt similarity index 66% rename from spring-web-reactive/src/main/kotlin/org/springframework/web/reactive/function/server/RouterFunctionDsl.kt rename to spring-web-reactive/src/main/kotlin/org/springframework/web/reactive/function/server/RouterFunctionExtensions.kt index 484b962847f..5f54f916903 100644 --- a/spring-web-reactive/src/main/kotlin/org/springframework/web/reactive/function/server/RouterFunctionDsl.kt +++ b/spring-web-reactive/src/main/kotlin/org/springframework/web/reactive/function/server/RouterFunctionExtensions.kt @@ -1,14 +1,16 @@ package org.springframework.web.reactive.function.server import org.springframework.core.io.Resource +import org.springframework.http.HttpMethod +import org.springframework.http.MediaType import reactor.core.publisher.Mono /** - * Provide a router DSL for [RouterFunctions] and [RouterFunction] in order to be able to + * Provide a routing DSL for [RouterFunctions] and [RouterFunction] in order to be able to * write idiomatic Kotlin code as below: * * * ```kotlin - * fun route(request: ServerRequest) = RouterFunctionDsl { + * fun route(request: ServerRequest) = route(request) { * accept(TEXT_HTML).apply { * (GET("/user/") or GET("/users/")) { findAllView() } * GET("/user/{login}") { findViewById() } @@ -18,16 +20,19 @@ import reactor.core.publisher.Mono * POST("/api/user/") { create() } * POST("/api/user/{login}") { findOne() } * } - * } (request) + * } * ``` * * @since 5.0 * @author Sebastien Deleuze * @author Yevhenii Melnyk */ -class RouterFunctionDsl { +fun RouterFunction<*>.route(request: ServerRequest, configure: RouterDsl.() -> Unit) = + RouterDsl().apply(configure).invoke(request) - val children = mutableListOf() +class RouterDsl { + + val children = mutableListOf() val routes = mutableListOf>() operator fun RequestPredicate.invoke(f: () -> HandlerFunction) { @@ -40,66 +45,63 @@ class RouterFunctionDsl { operator fun RequestPredicate.not(): RequestPredicate = this.negate() - fun GET(pattern: String): RequestPredicate { - return RequestPredicates.GET(pattern) - } - fun GET(pattern: String, f: () -> HandlerFunction) { routes += RouterFunctions.route(RequestPredicates.GET(pattern), f()) } - fun HEAD(pattern: String): RequestPredicate { - return RequestPredicates.HEAD(pattern) - } - fun HEAD(pattern: String, f: () -> HandlerFunction) { routes += RouterFunctions.route(RequestPredicates.HEAD(pattern), f()) } - fun POST(pattern: String): RequestPredicate { - return RequestPredicates.POST(pattern) - } - fun POST(pattern: String, f: () -> HandlerFunction) { routes += RouterFunctions.route(RequestPredicates.POST(pattern), f()) } - fun PUT(pattern: String): RequestPredicate { - return RequestPredicates.PUT(pattern) - } - fun PUT(pattern: String, f: () -> HandlerFunction) { routes += RouterFunctions.route(RequestPredicates.PUT(pattern), f()) } - fun PATCH(pattern: String): RequestPredicate { - return RequestPredicates.PATCH(pattern) - } - fun PATCH(pattern: String, f: () -> HandlerFunction) { routes += RouterFunctions.route(RequestPredicates.PATCH(pattern), f()) } - fun DELETE(pattern: String): RequestPredicate { - return RequestPredicates.DELETE(pattern) - } - fun DELETE(pattern: String, f: () -> HandlerFunction) { routes += RouterFunctions.route(RequestPredicates.DELETE(pattern), f()) } - fun OPTIONS(pattern: String): RequestPredicate { - return RequestPredicates.OPTIONS(pattern) - } - fun OPTIONS(pattern: String, f: () -> HandlerFunction) { routes += RouterFunctions.route(RequestPredicates.OPTIONS(pattern), f()) } + fun accept(vararg mediaType: MediaType, f: () -> HandlerFunction) { + routes += RouterFunctions.route(RequestPredicates.accept(*mediaType), f()) + } + + fun contentType(vararg mediaType: MediaType, f: () -> HandlerFunction) { + routes += RouterFunctions.route(RequestPredicates.contentType(*mediaType), f()) + } + + fun headers(headerPredicate: (ServerRequest.Headers)->Boolean, f: () -> HandlerFunction) { + routes += RouterFunctions.route(RequestPredicates.headers(headerPredicate), f()) + } + + fun method(httpMethod: HttpMethod, f: () -> HandlerFunction) { + routes += RouterFunctions.route(RequestPredicates.method(httpMethod), f()) + } + + fun path(pattern: String, f: () -> HandlerFunction) { + routes += RouterFunctions.route(RequestPredicates.path(pattern), f()) + } + fun resources(path: String, location: Resource) { routes += RouterFunctions.resources(path, location) } + fun resources(lookupFunction: (ServerRequest) -> Mono) { + routes += RouterFunctions.resources(lookupFunction) + } + + @Suppress("UNCHECKED_CAST") fun router(): RouterFunction { return routes().reduce(RouterFunction<*>::and) as RouterFunction @@ -117,6 +119,5 @@ class RouterFunctionDsl { } return allRoutes } -} -fun RouterFunctionDsl(configure: RouterFunctionDsl.()->Unit) = RouterFunctionDsl().apply(configure) +} diff --git a/spring-web-reactive/src/test/kotlin/org/springframework/web/reactive/function/server/RouterFunctionExtensionsTests.kt b/spring-web-reactive/src/test/kotlin/org/springframework/web/reactive/function/server/RouterFunctionExtensionsTests.kt new file mode 100644 index 00000000000..0c226c558ce --- /dev/null +++ b/spring-web-reactive/src/test/kotlin/org/springframework/web/reactive/function/server/RouterFunctionExtensionsTests.kt @@ -0,0 +1,129 @@ +package org.springframework.web.reactive.function.server + +import org.junit.Test +import org.springframework.core.io.ClassPathResource +import org.springframework.http.HttpHeaders.ACCEPT +import org.springframework.http.HttpHeaders.CONTENT_TYPE +import org.springframework.http.HttpMethod +import org.springframework.http.HttpMethod.PATCH +import org.springframework.http.HttpMethod.POST +import org.springframework.http.MediaType.* +import org.springframework.web.reactive.function.server.MockServerRequest.builder +import org.springframework.web.reactive.function.server.RequestPredicates.* +import org.springframework.web.reactive.function.server.ServerResponse.ok +import reactor.core.publisher.Mono +import reactor.test.StepVerifier +import java.net.URI + +class RouterFunctionExtensionsTests { + + @Test + fun header() { + val request = builder().header("bar", "bar").build() + StepVerifier.create(FooController().route(request)) + .expectNextCount(1) + .verifyComplete() + } + + @Test + fun accept() { + val request = builder().header(ACCEPT, APPLICATION_ATOM_XML_VALUE).build() + StepVerifier.create(FooController().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(FooController().route(request)) + .expectNextCount(1) + .verifyComplete() + } + + @Test + fun contentType() { + val request = builder().header(CONTENT_TYPE, APPLICATION_OCTET_STREAM_VALUE).build() + StepVerifier.create(FooController().route(request)) + .expectNextCount(1) + .verifyComplete() + } + + @Test + fun resourceByPath() { + val request = builder().uri(URI("/org/springframework/web/reactive/function/response.txt")).build() + StepVerifier.create(FooController().route(request)) + .expectNextCount(1) + .verifyComplete() + } + + @Test + fun method() { + val request = builder().method(PATCH).build() + StepVerifier.create(FooController().route(request)) + .expectNextCount(1) + .verifyComplete() + } + + @Test + fun path() { + val request = builder().uri(URI("/baz")).build() + StepVerifier.create(FooController().route(request)) + .expectNextCount(1) + .verifyComplete() + } + + @Test + fun resource() { + val request = builder().uri(URI("/response.txt")).build() + StepVerifier.create(FooController().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(FooController().route(request)) + .verifyComplete() + } + + + class FooController : RouterFunction { + + override fun route(request: ServerRequest) = route(request) { + (GET("/foo/") or GET("/foos/")) { handle() } + accept(APPLICATION_JSON).apply { + POST("/api/foo/") { handle() } + PUT("/api/foo/") { handle() } + DELETE("/api/foo/") { handle() } + } + accept(APPLICATION_ATOM_XML) { handle() } + contentType(APPLICATION_OCTET_STREAM) { handle() } + method(HttpMethod.PATCH) { handle() } + headers({ it.accept().contains(APPLICATION_JSON) }).apply { + 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") { + Mono.just(ClassPathResource("/org/springframework/web/reactive/function/response.txt")) + } + else { + Mono.empty() + } + } + path("/baz") { handle() } + } + + fun handle() = HandlerFunction { ok().build() } + + } + +}