Skip to content

Commit

Permalink
Merge pull request #751 from erikvanoosten/todirective
Browse files Browse the repository at this point in the history
Improved ToDirective
  • Loading branch information
adamw authored Sep 16, 2020
2 parents b7bec7e + 72ed18c commit aa3f46b
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 17 deletions.
62 changes: 53 additions & 9 deletions doc/server/akkahttp.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,18 @@ Now import the package:
import sttp.tapir.server.akkahttp._
```

This adds extension methods to the `Endpoint` type: `toDirective`, `toRoute` and `toRouteRecoverErrors`. The first two
require the logic of the endpoint to be given as a function of type:
This adds extension methods to the `Endpoint` type: `toRoute`, `toRouteRecoverErrors` and `toDirective`.

## using `toRoute` and `toRouteRecoverErrors`

Method `toRoute` requires the logic of the endpoint to be given as a function of type:

```scala
I => Future[Either[E, O]]
```

The third recovers errors from failed futures, and hence requires that `E` is a subclass of `Throwable` (an exception);
it expects a function of type `I => Future[O]`.
Method `toRouteRecoverErrors` recovers errors from failed futures, and hence requires that `E` is a subclass of
`Throwable` (an exception); it expects a function of type `I => Future[O]`.

For example:

Expand Down Expand Up @@ -61,12 +64,39 @@ val anEndpoint: Endpoint[(String, Int), Unit, String, Nothing] = ???
val aRoute: Route = anEndpoint.toRoute((logic _).tupled)
```

The created `Route`/`Directive` can then be further combined with other akka-http directives, for example nested within
other routes. The tapir-generated `Route`/`Directive` captures from the request only what is described by the endpoint.
## using `toDirective`

Method `toDirective` splits parsing the input and encoding the output. The directive provides the
input parameters, type `I`, and a function that can be used to encode the output.

For example:

```scala
import sttp.tapir._
import sttp.tapir.server.akkahttp._
import scala.concurrent.Future
import akka.http.scaladsl.server.Route

def countCharacters(s: String): Future[Either[Unit, Int]] =
Future.successful(Right[Unit, Int](s.length))

val countCharactersEndpoint: Endpoint[String, Unit, Int, Nothing] =
endpoint.in(stringBody).out(plainBody[Int])

val countCharactersRoute: Route = countCharactersEndpoint.toDirective { (input, completion) =>
completion(countCharacters(input))
}
```

## Combining directives

It's completely feasible that some part of the input is read using akka-http directives, and the rest
using tapir endpoint descriptions; or, that the tapir-generated route is wrapped in e.g. a metrics route. Moreover,
"edge-case endpoints", which require some special logic not expressible using tapir, can be always implemented directly
The tapir-generated `Route`/`Directive` captures from the request only what is described by the endpoint. Combine
with other akka-http directives to add additional behavior, or get more information from the request.

For example, wrap the tapir-generated route in a metrics route, or nest a security directive in the
tapir-generated directive.

Edge-case endpoints, which require special logic not expressible using tapir, can be implemented directly
using akka-http. For example:

```scala mdoc:compile-only
Expand All @@ -86,6 +116,20 @@ val myRoute: Route = metricsDirective {
}
```

Note that `Route`s can only be nested within other directives. `Directive`s can nest in other directives
and can also contain nested directives. For example:

```scala
val countCharactersRoute: Route =
authenticateBasic("realm", authenticator) {
countCharactersEndpoint.toDirective { (input, completion) =>
authorizeUserFor(input) {
completion(countCharacters(input))
}
}
}
```

## Streaming

The akka-http interpreter accepts streaming bodies of type `Source[ByteString, Any]`, which can be used both for sending
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,45 @@ import akka.http.scaladsl.model.{MediaType => _}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import akka.http.scaladsl.server.directives.RouteDirectives
import akka.http.scaladsl.server.util.{Tuple => AkkaTuple}
import sttp.tapir._
import sttp.tapir.monad.FutureMonadError
import sttp.tapir.server.{ServerDefaults, ServerEndpoint}
import sttp.tapir.typelevel.ParamsToTuple

import scala.concurrent.Future
import scala.util.{Failure, Success}

class EndpointToAkkaServer(serverOptions: AkkaHttpServerOptions) {
def toDirective[I, E, O, T](e: Endpoint[I, E, O, AkkaStream])(implicit paramsToTuple: ParamsToTuple.Aux[I, T]): Directive[T] = {
implicit val tIsAkkaTuple: AkkaTuple[T] = AkkaTuple.yes
toDirective1(e).flatMap { values => tprovide(paramsToTuple.toTuple(values)) }

/**
* Converts the endpoint to a directive that -for matching requests- decodes the input parameters
* and provides those input parameters and a function. The function can be called to complete the request.
*
* Example usage:
* {{{
* def logic(input: I): Future[Either[E, O] = ???
*
* endpoint.toDirectiveIC { (input, completion) =>
* securityDirective {
* completion(logic(input))
* }
* }
* }}}
*
* If type `I` is a tuple, and `logic` has 1 parameter per tuple member, use {{{completion((logic _).tupled(input))}}}
*/
def toDirective[I, E, O](e: Endpoint[I, E, O, AkkaStream]): Directive[(I, Future[Either[E, O]] => Route)] = {
toDirective1(e).flatMap { (values: I) =>
extractLog.flatMap { log =>
val completion: Future[Either[E, O]] => Route = result => onComplete(result) {
case Success(Left(v)) => OutputToAkkaRoute(ServerDefaults.StatusCodes.error.code, e.errorOutput, v)
case Success(Right(v)) => OutputToAkkaRoute(ServerDefaults.StatusCodes.success.code, e.output, v)
case Failure(t) =>
serverOptions.logRequestHandling.logicException(e, t)(log)
throw t
}
tprovide((values, completion))
}
}
}

def toRoute[I, E, O](se: ServerEndpoint[I, E, O, AkkaStream, Future]): Route = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import scala.reflect.ClassTag

trait TapirAkkaHttpServer {
implicit class RichAkkaHttpEndpoint[I, E, O](e: Endpoint[I, E, O, AkkaStream]) {
def toDirective[T](implicit paramsToTuple: ParamsToTuple.Aux[I, T], akkaHttpOptions: AkkaHttpServerOptions): Directive[T] =
new EndpointToAkkaServer(akkaHttpOptions).toDirective(e)
def toDirective(implicit serverOptions: AkkaHttpServerOptions): Directive[(I, Future[Either[E, O]] => Route)] =
new EndpointToAkkaServer(serverOptions).toDirective(e)

def toRoute(logic: I => Future[Either[E, O]])(implicit serverOptions: AkkaHttpServerOptions): Route =
new EndpointToAkkaServer(serverOptions).toRoute(e.serverLogic(logic))
Expand All @@ -24,7 +24,7 @@ trait TapirAkkaHttpServer {
}

implicit class RichAkkaHttpServerEndpoint[I, E, O](serverEndpoint: ServerEndpoint[I, E, O, AkkaStream, Future]) {
def toDirective[T](implicit paramsToTuple: ParamsToTuple.Aux[I, T], akkaHttpOptions: AkkaHttpServerOptions): Directive[T] =
def toDirective(implicit akkaHttpOptions: AkkaHttpServerOptions): Directive[(I, Future[Either[E, O]] => Route)] =
new EndpointToAkkaServer(akkaHttpOptions).toDirective(serverEndpoint.endpoint)

def toRoute(implicit serverOptions: AkkaHttpServerOptions): Route =
Expand Down

0 comments on commit aa3f46b

Please sign in to comment.