diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/Route.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/Route.java index 3ee3eab9..bacd72fc 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/Route.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/Route.java @@ -97,6 +97,10 @@ public class Route implements Ordered { return this; } + public Predicate getPredicate() { + return this.predicate; + } + public Builder predicate(Predicate predicate) { this.predicate = predicate; return this; diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/BooleanSpec.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/BooleanSpec.java index 52e790fa..672df435 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/BooleanSpec.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/BooleanSpec.java @@ -28,8 +28,12 @@ public class BooleanSpec extends GatewayFilterSpec { enum Operator { AND, OR, NEGATE } + final Predicate predicate; + public BooleanSpec(Route.Builder routeBuilder, RouteLocatorBuilder.Builder builder) { super(routeBuilder, builder); + // save current predicate useful in kotlin dsl + predicate = routeBuilder.getPredicate(); } public BooleanOpSpec and() { @@ -66,7 +70,7 @@ public class BooleanSpec extends GatewayFilterSpec { case NEGATE: this.routeBuilder.negate(); } - return gatewayFilterBuilder(); + return createBooleanSpec(); } } } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java index b0bf442a..46457952 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java @@ -46,10 +46,10 @@ public class PredicateSpec extends UriSpec { public BooleanSpec predicate(Predicate predicate) { this.routeBuilder.predicate(predicate); - return gatewayFilterBuilder(); + return createBooleanSpec(); } - protected BooleanSpec gatewayFilterBuilder() { + protected BooleanSpec createBooleanSpec() { return new BooleanSpec(this.routeBuilder, this.builder); } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/RouteLocatorBuilder.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/RouteLocatorBuilder.java index bc20fc4e..25127b4d 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/RouteLocatorBuilder.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/RouteLocatorBuilder.java @@ -87,7 +87,7 @@ public class RouteLocatorBuilder { private final Route.Builder routeBuilder = Route.builder(); private final Builder builder; - private RouteSpec(Builder builder) { + RouteSpec(Builder builder) { this.builder = builder; } diff --git a/spring-cloud-gateway-core/src/main/kotlin/org/springframework/cloud/gateway/route/GatewayDsl.kt b/spring-cloud-gateway-core/src/main/kotlin/org/springframework/cloud/gateway/route/GatewayDsl.kt index 6cd30ddb..428a75f1 100644 --- a/spring-cloud-gateway-core/src/main/kotlin/org/springframework/cloud/gateway/route/GatewayDsl.kt +++ b/spring-cloud-gateway-core/src/main/kotlin/org/springframework/cloud/gateway/route/GatewayDsl.kt @@ -35,13 +35,17 @@ import java.util.function.Predicate * ``` * * @author Biju Kunjummen + * @deprecated see [org.springframework.cloud.gateway.route.builder.RouteDsl] */ +@Deprecated("see RouteDsl") fun gateway(routeLocator: RouteLocatorDsl.() -> Unit) = RouteLocatorDsl().apply(routeLocator).build() /** * Provider for [RouteLocator] DSL functionality + * @deprecated see [org.springframework.cloud.gateway.route.builder.RouteDsl] */ +@Deprecated("see RouteDsl") class RouteLocatorDsl { private val routes = mutableListOf() diff --git a/spring-cloud-gateway-core/src/main/kotlin/org/springframework/cloud/gateway/route/builder/RouteDsl.kt b/spring-cloud-gateway-core/src/main/kotlin/org/springframework/cloud/gateway/route/builder/RouteDsl.kt new file mode 100644 index 00000000..b9fb9189 --- /dev/null +++ b/spring-cloud-gateway-core/src/main/kotlin/org/springframework/cloud/gateway/route/builder/RouteDsl.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2013-2017 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.cloud.gateway.route.builder + +import org.springframework.cloud.gateway.route.RouteLocator +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder.RouteSpec +import java.util.function.Predicate + +/** + * A Kotlin based DSL to configure a [RouteLocator] + * + * Example: + * ``` + * @Autowired + * lateinit var builder: RouteLocatorBuilder + * + * val routeLocator = builder.routes { + * route(id = "test") { + * host("**.abc.org") and path("/image/png") + * filters { + * add(addResponseHeader("X-TestHeader", "foobar")) + * } + * uri("http://httpbin.org:80") + * } + * } + * ``` + * + * @author Biju Kunjummen + * @author Spencer Gibb + */ +fun RouteLocatorBuilder.routes(routeLocator: RouteLocatorDsl.() -> Unit): RouteLocator { + return RouteLocatorDsl(this).apply(routeLocator).build() +} + + +/** + * Provider for [RouteLocator] DSL functionality + */ +class RouteLocatorDsl(val builder: RouteLocatorBuilder) { + private val routes = builder.routes() + + /** + * DSL to add a route to the [RouteLocator] + * + * @see [Route.Builder] + */ + fun route(id: String? = null, order: Int = 0, uri: String? = null, init: PredicateSpec.() -> Unit) { + val predicateSpec = if (id == null) { + RouteSpec(routes).randomId() + } else { + RouteSpec(routes).id(id) + } + predicateSpec.order(order) + predicateSpec.apply(init) + if (uri != null) { + predicateSpec.uri(uri) + } + } + + fun build(): RouteLocator { + return routes.build() + } + +} + +/** + * Extension method to add filters {} block to dsl + */ +fun PredicateSpec.filters(init: GatewayFilterSpec.() -> Unit) { + val booleanSpec = createBooleanSpec() + booleanSpec.apply(init) +} + + +/** + * A helper to return a composed [Predicate] that tests against this [Predicate] AND the [other] predicate + */ +infix fun BooleanSpec.and(other: BooleanSpec) = + this.routeBuilder.predicate(this.predicate.and(other.predicate)) + +/** + * A helper to return a composed [Predicate] that tests against this [Predicate] OR the [other] predicate + */ +infix fun BooleanSpec.or(other: BooleanSpec) = + this.routeBuilder.predicate(this.predicate.or(other.predicate)) + + diff --git a/spring-cloud-gateway-core/src/test/kotlin/org/springframework/cloud/gateway/route/builder/RouteDslTests.kt b/spring-cloud-gateway-core/src/test/kotlin/org/springframework/cloud/gateway/route/builder/RouteDslTests.kt new file mode 100644 index 00000000..a40bc2ae --- /dev/null +++ b/spring-cloud-gateway-core/src/test/kotlin/org/springframework/cloud/gateway/route/builder/RouteDslTests.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2013-2017 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.cloud.gateway.route.builder + +import org.junit.Test +import org.junit.runner.RunWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.annotation.Configuration +import org.springframework.mock.http.server.reactive.MockServerHttpRequest +import org.springframework.mock.web.server.MockServerWebExchange +import org.springframework.test.context.junit4.SpringRunner +import org.springframework.web.server.ServerWebExchange +import reactor.test.StepVerifier +import java.net.URI + +@RunWith(SpringRunner::class) +@SpringBootTest(classes = arrayOf(Config::class)) +class RouteDslTests { + + @Autowired + lateinit var builder: RouteLocatorBuilder + + @Test + fun sampleRouteDsl() { + val routeLocator = builder.routes { + route(id = "test") { + host("**.abc.org") and path("/image/png") + filters { + addResponseHeader("X-TestHeader", "foobar") + } + uri("http://httpbin.org:80") + } + + route(id = "test2") { + path("/image/webp") or path("/image/anotherone") + filters { + addResponseHeader("X-AnotherHeader", "baz") + addResponseHeader("X-AnotherHeader-2", "baz-2") + } + uri("https://httpbin.org:443") + } + } + + StepVerifier + .create(routeLocator.routes) + .expectNextMatches({ r -> + r.id == "test" && r.filters.size == 1 && r.uri == URI.create("http://httpbin.org:80") + }) + .expectNextMatches({ r -> + r.id == "test2" && r.filters.size == 2 && r.uri == URI.create("https://httpbin.org:443") + }) + .expectComplete() + .verify() + + val sampleExchange: ServerWebExchange = MockServerWebExchange.from(MockServerHttpRequest.get("/image/webp") + .header("Host", "test.abc.org").build()) + + val filteredRoutes = routeLocator.routes.filter({ r -> r.predicate.test(sampleExchange) }) + + StepVerifier.create(filteredRoutes) + .expectNextMatches({ r -> + r.id == "test2" && r.filters.size == 2 && r.uri == URI.create("https://httpbin.org:443") + }) + .expectComplete() + .verify() + } + + @Test + fun dslWithFunctionParameters() { + val routerLocator = builder.routes { + route(id = "test", order = 10, uri = "http://httpbin.org") { + host("**.abc.org") + } + } + + StepVerifier.create(routerLocator.routes) + .expectNextMatches({ r -> + r.id == "test" && + r.uri == URI.create("http://httpbin.org") && + r.order == 10 && + r.id == "test" && + r.predicate.test(MockServerWebExchange + .from(MockServerHttpRequest + .get("/someuri").header("Host", "test.abc.org"))) + }) + .expectComplete() + .verify() + } +} + +@Configuration +@EnableAutoConfiguration +open class Config {} \ No newline at end of file