From b3c85512a33d865a1d2ce98550df74d49790b059 Mon Sep 17 00:00:00 2001 From: Alan Johnson Date: Tue, 5 Apr 2016 09:36:46 -0400 Subject: [PATCH] htp #19678 add variadic concat route combinator * add variadic route concatenation function * add `concat` to `~` warning bubble in docs * fix grammar of warning bubble, and clarify --- .../http/routing-dsl/directives/index.rst | 4 +-- .../akka/http/impl/util/StreamUtilsSpec.scala | 3 +- .../scaladsl/server/BasicRouteSpecs.scala | 36 +++++++++++++++++++ .../scaladsl/server/RouteConcatenation.scala | 12 +++++++ 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/akka-docs/rst/scala/http/routing-dsl/directives/index.rst b/akka-docs/rst/scala/http/routing-dsl/directives/index.rst index cf5945c7b5..e0082a338a 100644 --- a/akka-docs/rst/scala/http/routing-dsl/directives/index.rst +++ b/akka-docs/rst/scala/http/routing-dsl/directives/index.rst @@ -138,8 +138,8 @@ transformations, both (or either) on the request and on the response side. Composing Directives -------------------- -.. note:: Gotcha: forgetting the ``~`` (tilde) character in between directives can often result in perfectly valid - Scala code that compiles but lead to your composed directive only containing the up to where ``~`` is missing. +.. note:: Gotcha: forgetting the ``~`` (tilde) character in between directives can result in perfectly valid + Scala code that compiles but does not work as expected. What would be intended as a single expression would actually be multiple expressions, and only the final one would be used as the result of the parent directive. Alternatively, you might choose to use the ``concat`` combinator. ``concat(a, b, c)`` is the same as ``a ~ b ~ c``. As you have seen from the examples presented so far the "normal" way of composing directives is nesting. Let's take a look at this concrete example: diff --git a/akka-http-core/src/test/scala/akka/http/impl/util/StreamUtilsSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/util/StreamUtilsSpec.scala index 1293372687..f9174f98c5 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/util/StreamUtilsSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/util/StreamUtilsSpec.scala @@ -4,7 +4,7 @@ package akka.http.impl.util import akka.stream.ActorMaterializer -import akka.stream.scaladsl.{Sink, Source} +import akka.stream.scaladsl.{ Sink, Source } import akka.testkit.AkkaSpec import scala.concurrent.Await @@ -34,7 +34,6 @@ class StreamUtilsSpec extends AkkaSpec { Await.ready(whenCompleted, 3.seconds).value shouldBe Some(Failure(ex)) } - "downstream cancels" in { val (newSource, whenCompleted) = StreamUtils.captureTermination(Source(List(1, 2, 3))) diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/BasicRouteSpecs.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/BasicRouteSpecs.scala index 40d53b14e0..3e24e1dab9 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/BasicRouteSpecs.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/BasicRouteSpecs.scala @@ -35,6 +35,42 @@ class BasicRouteSpecs extends RoutingSpec { } } + "routes created by the 'sequence' directive" should { + "reject with zero arguments, since no routes were matched" in { + Get() ~> { + concat() + } ~> check { rejections shouldEqual Seq() } + } + "yield the first sub route if it succeeded" in { + Get() ~> { + concat( + get { complete("first") }, + get { complete("second") }) + } ~> check { responseAs[String] shouldEqual "first" } + } + "yield the second sub route if the first did not succeed" in { + Get() ~> { + concat( + post { complete("first") }, + get { complete("second") }) + } ~> check { responseAs[String] shouldEqual "second" } + } + "collect rejections from both sub routes" in { + Delete() ~> { + concat( + get { completeOk }, + put { completeOk }) + } ~> check { rejections shouldEqual Seq(MethodRejection(GET), MethodRejection(PUT)) } + } + "clear rejections that have already been 'overcome' by previous directives" in { + Put() ~> { + concat( + put { parameter('yeah) { echoComplete } }, + get { completeOk }) + } ~> check { rejection shouldEqual MissingQueryParamRejection("yeah") } + } + } + "Route conjunction" should { val stringDirective = provide("The cat") val intDirective = provide(42) diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/RouteConcatenation.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/RouteConcatenation.scala index 80b0b83385..0e7cd19bd8 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/RouteConcatenation.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/RouteConcatenation.scala @@ -4,6 +4,7 @@ package akka.http.scaladsl.server +import akka.http.scaladsl.server.Directives.reject import akka.http.scaladsl.util.FastFuture import akka.http.scaladsl.util.FastFuture._ @@ -18,6 +19,17 @@ trait RouteConcatenation { */ implicit def enhanceRouteWithConcatenation(route: Route): RouteConcatenation.RouteWithConcatenation = new RouteConcatenation.RouteWithConcatenation(route: Route) + + /** + * Tries the supplied routes in sequence, returning the result of the first route that doesn't reject the request. + * This is an alternative to direct usage of the infix ~ operator. The ~ can be prone to programmer error, because if + * it is omitted, the program will still be syntactically correct, but will not actually attempt to match multiple + * routes, as intended. + * + * @param routes subroutes to concatenate + * @return the concatenated route + */ + def concat(routes: Route*): Route = routes.foldLeft[Route](reject)(_ ~ _) } object RouteConcatenation extends RouteConcatenation {