Skip to content

Commit

Permalink
wip: first try at providing inspectable routes with minimal changes
Browse files Browse the repository at this point in the history
  • Loading branch information
jrudolph committed Feb 13, 2020
1 parent 16cb71c commit 78b1885
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 36 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package akka.http.scaladsl.server

import akka.http.impl.util.AkkaSpecWithMaterializer
import Directives._
import DynamicDirective._
import DirectiveRoute.addByNameNullaryApply

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 =
// semantic change here: the whole routing tree is evaluated eagerly because of the alternative
// `DirectiveRoute.addByNameNullaryApply` imported above
concat(
get { complete("ok") },
post { complete("ok") }
)
route shouldBe an[InspectableRoute]
}
"for routes with extractions" in {
val route =
// if you use static, it lifts the value into a token, that's the main API change for users
parameters("name").static { name: ExtractionToken[String] =>
get {
// you can only access token values inside of dynamic blocks
// it's not possible to inspect inner routes of dynamic blocks
dynamic { implicit extractionCtx =>
// with an implicit ExtractionContext in scope you can access token values using an implicit conversion
complete(s"Hello ${name: String}")
}
}
}
route shouldBe an[InspectableRoute]
println(route)
}
}
}
}
15 changes: 15 additions & 0 deletions akka-http/src/main/scala/akka/http/scaladsl/server/Directive.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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("<unknown>")(_.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("<unknown>")(_.name))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,18 @@ 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,
val settings: RoutingSettings,
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)
Expand Down Expand Up @@ -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])"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}

Expand Down

0 comments on commit 78b1885

Please sign in to comment.