From ebc6ddd06ae1bec8b242131106899bc5c7335aca Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Thu, 13 Feb 2020 12:15:23 +0100 Subject: [PATCH] wip: first try at providing inspectable routes with minimal changes Refs #201, #2956 --- .../server/InspectableRouteSpec.scala | 43 +++++++++ .../akka/http/scaladsl/server/Directive.scala | 15 +++ .../scaladsl/server/InspectableRoute.scala | 93 +++++++++++++++++++ .../http/scaladsl/server/RequestContext.scala | 3 + .../scaladsl/server/RequestContextImpl.scala | 25 +++-- .../scaladsl/server/RouteConcatenation.scala | 18 +--- .../server/directives/MethodDirectives.scala | 16 ++-- .../directives/ParameterDirectives.scala | 4 +- 8 files changed, 181 insertions(+), 36 deletions(-) create mode 100644 akka-http-tests/src/test/scala/akka/http/scaladsl/server/InspectableRouteSpec.scala create mode 100644 akka-http/src/main/scala/akka/http/scaladsl/server/InspectableRoute.scala diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/InspectableRouteSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/InspectableRouteSpec.scala new file mode 100644 index 00000000000..bceb45d1e8a --- /dev/null +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/InspectableRouteSpec.scala @@ -0,0 +1,43 @@ +package akka.http.scaladsl.server + +import akka.http.impl.util.AkkaSpecWithMaterializer +import Directives._ +import DirectiveRoute.addByNameNullaryApply +import DynamicDirective._ + +class InspectableRouteSpec extends AkkaSpecWithMaterializer { + "Routes" should { + "be inspectable" should { + "routes from method directives" in { + val route = get { post { complete("ok") } } + route shouldBe an[InspectableRoute] + println(route) + } + "route alternatives with ~" in { + val route = get { complete("ok") } ~ post { complete("ok") } + route shouldBe an[InspectableRoute] + println(route) + } + "route alternatives with concat" in { + val route = + concat( + get { complete("ok") }, + post { complete("ok") } + ) + route shouldBe an[InspectableRoute] + } + "even for routes with extractions" in { + val route = + parameters("name").static { name => + get { + dynamic { implicit exC => + complete(s"Hello ${name: String}") + } + } + } + route shouldBe an[InspectableRoute] + println(route) + } + } + } +} diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/Directive.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/Directive.scala index 261f2b94194..00aa9564d5a 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/Directive.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/Directive.scala @@ -13,6 +13,15 @@ import akka.http.scaladsl.util.FastFuture import akka.http.scaladsl.util.FastFuture._ import akka.http.impl.util._ +final case class DirectiveMetaInformation(name: String) + +// FIXME: in the best case we can move that into Directive.apply for less indirection +private class DirectiveWithChangedMetaInformation[L](original: Directive[L], _metaInformation: DirectiveMetaInformation)(implicit ev: Tuple[L]) extends Directive[L] { + override def tapply(f: L => Route): Route = original.tapply(f) + override def metaInformation: Option[DirectiveMetaInformation] = Some(_metaInformation) + override def withMetaInformation(newInformation: DirectiveMetaInformation): Directive[L] = new DirectiveWithChangedMetaInformation[L](original, newInformation) +} + /** * A directive that provides a tuple of values of type `L` to create an inner route. */ @@ -27,6 +36,12 @@ abstract class Directive[L](implicit val ev: Tuple[L]) { */ def tapply(f: L => Route): Route //#basic + + def metaInformation: Option[DirectiveMetaInformation] = None + def withMetaInformation(newInformation: DirectiveMetaInformation): Directive[L] = + new DirectiveWithChangedMetaInformation[L](this, newInformation) + def named(name: String): Directive[L] = withMetaInformation(DirectiveMetaInformation(name)) + /** * Joins two directives into one which runs the second directive if the first one rejects. */ diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/InspectableRoute.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/InspectableRoute.scala new file mode 100644 index 00000000000..dceb582eaac --- /dev/null +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/InspectableRoute.scala @@ -0,0 +1,93 @@ +package akka.http.scaladsl.server + +import akka.http.scaladsl.util.FastFuture +import FastFuture._ + +import scala.concurrent.Future +import scala.runtime.ScalaRunTime + +sealed trait InspectableRoute extends Route with Product { + def name: String + def children: Seq[Route] + + // FIXME: only there to help intellij + def apply(ctx: RequestContext): Future[RouteResult] + + override def toString(): String = ScalaRunTime._toString(this) +} +final case class AlternativeRoutes(alternatives: Seq[Route]) extends InspectableRoute { + def name: String = "concat" + def children: Seq[Route] = alternatives + + def apply(ctx: RequestContext): Future[RouteResult] = { + import ctx.executionContext + def tryNext(remaining: List[Route], rejections: Vector[Rejection]): Future[RouteResult] = remaining match { + case head :: tail => + head(ctx).fast.flatMap { + case x: RouteResult.Complete => FastFuture.successful(x) + case RouteResult.Rejected(newRejections) => tryNext(tail, rejections ++ newRejections) + } + case Nil => FastFuture.successful(RouteResult.Rejected(rejections)) + } + tryNext(alternatives.toList, Vector.empty) + } +} +sealed trait DirectiveRoute extends InspectableRoute { + def implementation: Route + def directiveName: String + def child: Route + + def apply(ctx: RequestContext): Future[RouteResult] = implementation(ctx) + def children: Seq[Route] = child :: Nil + def name: String = s"Directive($directiveName)" +} + +object DirectiveRoute { + def wrap(implementation: Route, child: Route, directiveName: String): DirectiveRoute = implementation match { + case i: Impl => + i.copy(child = child, directiveName = directiveName) + case x => Impl(x, child, directiveName) + } + + implicit def addByNameNullaryApply(directive: Directive0): Route => Route = + inner => { + val impl = directive.tapply(_ => inner) + wrap(impl, inner, directive.metaInformation.fold("")(_.name)) + } + + private final case class Impl( + implementation: Route, + child: Route, + directiveName: String) extends DirectiveRoute +} + +sealed trait ExtractionToken[+T] +object ExtractionToken { + // syntax sugar + implicit def autoExtract[T](token: ExtractionToken[T])(implicit ctx: ExtractionContext): T = ctx.extract(token) +} +sealed trait ExtractionContext { + def extract[T](token: ExtractionToken[T]): T +} +object DynamicDirective { + import Directives._ + def dynamic: Directive1[ExtractionContext] = extractRequestContext.flatMap { ctx => + provide { + new ExtractionContext { + override def extract[T](token: ExtractionToken[T]): T = ctx.tokenValue(token) + } + } + } + + implicit class AddStatic[T](d: Directive1[T]) { + def static: Directive1[ExtractionToken[T]] = + Directive { innerCons => + val tok = new ExtractionToken[T] {} + val inner = innerCons(Tuple1(tok)) + val real = d { t => ctx => + inner(ctx.addTokenValue(tok, t)) + } + DirectiveRoute.wrap(real, inner, d.metaInformation.fold("")(_.name)) + } + } +} diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/RequestContext.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/RequestContext.scala index aec32dde0d9..e6eebf8ca51 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/RequestContext.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/RequestContext.scala @@ -135,4 +135,7 @@ trait RequestContext { * Removes a potentially existing Accept header from the request headers. */ def withAcceptAll: RequestContext + + def tokenValue[T](token: ExtractionToken[T]): T + def addTokenValue[T](token: ExtractionToken[T], value: T): RequestContext } diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/RequestContextImpl.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/RequestContextImpl.scala index fd54fd1af79..6a2c358506d 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/RequestContextImpl.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/RequestContextImpl.scala @@ -23,6 +23,7 @@ import scala.concurrent.{ ExecutionContextExecutor, Future } private[http] class RequestContextImpl( val request: HttpRequest, val unmatchedPath: Uri.Path, + val tokenValues: Map[ExtractionToken[_], Any], val executionContext: ExecutionContextExecutor, val materializer: Materializer, val log: LoggingAdapter, @@ -30,10 +31,10 @@ private[http] class RequestContextImpl( val parserSettings: ParserSettings) extends RequestContext { def this(request: HttpRequest, log: LoggingAdapter, settings: RoutingSettings, parserSettings: ParserSettings)(implicit ec: ExecutionContextExecutor, materializer: Materializer) = - this(request, request.uri.path, ec, materializer, log, settings, parserSettings) + this(request, request.uri.path, Map.empty, ec, materializer, log, settings, parserSettings) def this(request: HttpRequest, log: LoggingAdapter, settings: RoutingSettings)(implicit ec: ExecutionContextExecutor, materializer: Materializer) = - this(request, request.uri.path, ec, materializer, log, settings, ParserSettings(ActorMaterializerHelper.downcast(materializer).system)) + this(request, request.uri.path, Map.empty, ec, materializer, log, settings, ParserSettings(ActorMaterializerHelper.downcast(materializer).system)) def reconfigure(executionContext: ExecutionContextExecutor, materializer: Materializer, log: LoggingAdapter, settings: RoutingSettings): RequestContext = copy(executionContext = executionContext, materializer = materializer, log = log, routingSettings = settings) @@ -106,15 +107,19 @@ private[http] class RequestContextImpl( case _ => this } + override def tokenValue[T](token: ExtractionToken[T]): T = tokenValues(token).asInstanceOf[T] + override def addTokenValue[T](token: ExtractionToken[T], value: T): RequestContext = copy(tokenValues = tokenValues + (token -> value)) + private def copy( - request: HttpRequest = request, - unmatchedPath: Uri.Path = unmatchedPath, - executionContext: ExecutionContextExecutor = executionContext, - materializer: Materializer = materializer, - log: LoggingAdapter = log, - routingSettings: RoutingSettings = settings, - parserSettings: ParserSettings = parserSettings) = - new RequestContextImpl(request, unmatchedPath, executionContext, materializer, log, routingSettings, parserSettings) + request: HttpRequest = request, + unmatchedPath: Uri.Path = unmatchedPath, + tokenValues: Map[ExtractionToken[_], Any] = tokenValues, + executionContext: ExecutionContextExecutor = executionContext, + materializer: Materializer = materializer, + log: LoggingAdapter = log, + routingSettings: RoutingSettings = settings, + parserSettings: ParserSettings = parserSettings) = + new RequestContextImpl(request, unmatchedPath, tokenValues, executionContext, materializer, log, routingSettings, parserSettings) override def toString: String = s"""RequestContext($request, $unmatchedPath, [more settings])""" 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 9898dd5ac3f..09612503de0 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,10 +4,6 @@ package akka.http.scaladsl.server -import akka.http.scaladsl.server.Directives.reject -import akka.http.scaladsl.util.FastFuture -import akka.http.scaladsl.util.FastFuture._ - /** * @groupname concat Route concatenation * @groupprio concat 300 @@ -29,7 +25,7 @@ trait RouteConcatenation { * @param routes subroutes to concatenate * @return the concatenated route */ - def concat(routes: Route*): Route = routes.foldLeft[Route](reject)(_ ~ _) + def concat(routes: Route*): Route = AlternativeRoutes(routes.toList) } object RouteConcatenation extends RouteConcatenation { @@ -39,16 +35,6 @@ object RouteConcatenation extends RouteConcatenation { * Returns a Route that chains two Routes. If the first Route rejects the request the second route is given a * chance to act upon the request. */ - def ~(other: Route): Route = { ctx => - import ctx.executionContext - route(ctx).fast.flatMap { - case x: RouteResult.Complete => FastFuture.successful(x) - case RouteResult.Rejected(outerRejections) => - other(ctx).fast.map { - case x: RouteResult.Complete => x - case RouteResult.Rejected(innerRejections) => RouteResult.Rejected(outerRejections ++ innerRejections) - } - } - } + def ~(other: Route): Route = concat(route, other) } } diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/MethodDirectives.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/MethodDirectives.scala index 8afbb5a81d9..2db0c9ea5eb 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/MethodDirectives.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/MethodDirectives.scala @@ -84,7 +84,7 @@ trait MethodDirectives { extractMethod.flatMap[Unit] { case `httpMethod` => pass case _ => reject(MethodRejection(httpMethod)) - } & cancelRejections(classOf[MethodRejection]) + } & cancelRejections(classOf[MethodRejection]) named s"method($httpMethod)" //#method /** @@ -114,12 +114,12 @@ object MethodDirectives extends MethodDirectives { BasicDirectives.extract(_.request.method) // format: OFF - private val _delete : Directive0 = method(DELETE) - private val _get : Directive0 = method(GET) - private val _head : Directive0 = method(HEAD) - private val _options: Directive0 = method(OPTIONS) - private val _patch : Directive0 = method(PATCH) - private val _post : Directive0 = method(POST) - private val _put : Directive0 = method(PUT) + private val _delete : Directive0 = method(DELETE) named "delete" + private val _get : Directive0 = method(GET) named "get" + private val _head : Directive0 = method(HEAD) named "head" + private val _options: Directive0 = method(OPTIONS) named "options" + private val _patch : Directive0 = method(PATCH) named "patch" + private val _post : Directive0 = method(POST) named "post" + private val _put : Directive0 = method(PUT) named "put" // format: ON } diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/ParameterDirectives.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/ParameterDirectives.scala index 98061ead3b7..6bda41f057a 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/ParameterDirectives.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/ParameterDirectives.scala @@ -84,10 +84,10 @@ object ParameterDirectives extends ParameterDirectives { def apply(): Out } object ParamMagnet { - implicit def apply[T](value: T)(implicit pdef: ParamDef[T]): ParamMagnet { type Out = pdef.Out } = + implicit def apply[T, O](value: T)(implicit pdef: ParamDef[T] { type Out = Directive[O] }): ParamMagnet { type Out = pdef.Out } = new ParamMagnet { type Out = pdef.Out - def apply() = pdef(value) + def apply() = pdef(value) named "parameters" } }