Skip to content

Commit

Permalink
Improve inference, make examples in docs compiled
Browse files Browse the repository at this point in the history
  • Loading branch information
adamw committed Sep 16, 2020
1 parent aa3f46b commit ae1d25e
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import sttp.tapir.{FieldName, Schema, SchemaType}
import SchemaMagnoliaDerivation.deriveInProgress

import scala.collection.mutable
import scala.language.experimental.macros

trait SchemaMagnoliaDerivation {
type Typeclass[T] = Schema[T]
Expand Down
31 changes: 25 additions & 6 deletions doc/server/akkahttp.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import sttp.tapir.server.akkahttp._

This adds extension methods to the `Endpoint` type: `toRoute`, `toRouteRecoverErrors` and `toDirective`.

## using `toRoute` and `toRouteRecoverErrors`
## Using `toRoute` and `toRouteRecoverErrors`

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

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

## using `toDirective`
## 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
```scala mdoc:compile-only
import sttp.tapir._
import sttp.tapir.server.akkahttp._
import scala.concurrent.Future
Expand Down Expand Up @@ -119,11 +119,30 @@ 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
```scala mdoc:compile-only
import akka.http.scaladsl.server.Directive0
import akka.http.scaladsl.server.Directives.Authenticator
import akka.http.scaladsl.server.Directives.authenticateBasic
import akka.http.scaladsl.server.Route
import sttp.tapir._
import sttp.tapir.server.akkahttp._

import scala.concurrent.Future

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])

case class User(email: String)
val authenticator: Authenticator[User] = ???
def authorizationDirective(user: User, input: String): Directive0 = ???

val countCharactersRoute: Route =
authenticateBasic("realm", authenticator) {
authenticateBasic("realm", authenticator) { user =>
countCharactersEndpoint.toDirective { (input, completion) =>
authorizeUserFor(input) {
authorizationDirective(user, input) {
completion(countCharacters(input))
}
}
Expand Down
4 changes: 2 additions & 2 deletions generated-doc/out/endpoint/customtypes.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ be derived automatically.

### Sealed traits / coproducts

Tapir supports schema generation for coproduct types (sealed trait hierarchies) of the box, but they need to be defined
by hand (as implicit values). To properly reflect the schema in [OpenAPI](../openapi.md) documentation, a
Tapir supports schema generation for coproduct types (sealed trait hierarchies) out of the box, but they need to be defined
by hand as `implicit` values. To properly reflect the schema in [OpenAPI](../openapi.md) documentation, a
discriminator object can be specified.

For example, given following coproduct:
Expand Down
81 changes: 72 additions & 9 deletions generated-doc/out/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))
}
```

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
## Combining directives

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
Expand All @@ -86,6 +116,39 @@ 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
import akka.http.scaladsl.server.Directive0
import akka.http.scaladsl.server.Directives.Authenticator
import akka.http.scaladsl.server.Directives.authenticateBasic
import akka.http.scaladsl.server.Route
import sttp.tapir._
import sttp.tapir.server.akkahttp._

import scala.concurrent.Future

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])

case class User(email: String)
val authenticator: Authenticator[User] = ???
def authorizationDirective(user: User, input: String): Directive0 = ???

val countCharactersRoute: Route =
authenticateBasic("realm", authenticator) { user =>
countCharactersEndpoint.toDirective { (input, completion) =>
authorizationDirective(user, 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 @@ -14,32 +14,33 @@ import scala.util.{Failure, Success}
class EndpointToAkkaServer(serverOptions: AkkaHttpServerOptions) {

/**
* 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))}}}
*/
* 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.toDirective { (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
}
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))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,39 @@ package sttp.tapir.server.akkahttp
import akka.http.scaladsl.server._
import sttp.tapir.Endpoint
import sttp.tapir.server.ServerEndpoint
import sttp.tapir.typelevel.{ParamsToTuple, ReplaceFirstInTuple}
import sttp.tapir.typelevel.ReplaceFirstInTuple

import scala.concurrent.{ExecutionContext, Future}
import scala.reflect.ClassTag

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

def toRoute(logic: I => Future[Either[E, O]])(implicit serverOptions: AkkaHttpServerOptions): Route =
def toRoute(logic: I => Future[Either[E, O]]): Route =
new EndpointToAkkaServer(serverOptions).toRoute(e.serverLogic(logic))

def toRouteRecoverErrors(
logic: I => Future[O]
)(implicit serverOptions: AkkaHttpServerOptions, eIsThrowable: E <:< Throwable, eClassTag: ClassTag[E]): Route = {
)(implicit eIsThrowable: E <:< Throwable, eClassTag: ClassTag[E]): Route = {
new EndpointToAkkaServer(serverOptions).toRoute(e.serverLogicRecoverErrors(logic))
}
}

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

def toRoute(implicit serverOptions: AkkaHttpServerOptions): Route =
new EndpointToAkkaServer(serverOptions).toRoute(serverEndpoint)
def toRoute: Route = new EndpointToAkkaServer(serverOptions).toRoute(serverEndpoint)
}

implicit class RichAkkaHttpServerEndpoints(serverEndpoints: List[ServerEndpoint[_, _, _, AkkaStream, Future]]) {
def toRoute(implicit serverOptions: AkkaHttpServerOptions): Route = {
implicit class RichAkkaHttpServerEndpoints(serverEndpoints: List[ServerEndpoint[_, _, _, AkkaStream, Future]])(implicit
serverOptions: AkkaHttpServerOptions
) {
def toRoute: Route = {
new EndpointToAkkaServer(serverOptions).toRoute(serverEndpoints)
}
}
Expand Down

0 comments on commit ae1d25e

Please sign in to comment.