Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved ToDirective #751

Merged
merged 8 commits into from
Sep 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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