Skip to content

Commit

Permalink
Server logic accepts a single tuple parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
adamw committed Mar 24, 2019
1 parent fb78069 commit 6a081cd
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 173 deletions.
24 changes: 14 additions & 10 deletions doc/server/akkahttp.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,13 @@ This adds extension methods to the `Endpoint` type: `toDirective`, `toRoute` and
require the logic of the endpoint to be given as a function of type:

```scala
[I as function arguments] => Future[Either[E, O]]
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:
it expects a function of type `I => Future[O]`.

```scala
[I as function arguments] => Future[O]
```

Note that the function doesn't take the tuple `I` directly as input, but instead this is converted to a function of the
appropriate arity. For example:
For example:

```scala
import tapir._
Expand All @@ -42,11 +37,20 @@ def countCharacters(s: String): Future[Either[Unit, Int]] =
val countCharactersEndpoint: Endpoint[String, Unit, Int, Nothing] =
endpoint.in(stringBody).out(plainBody[Int])

val countCharactersRoute: Route = countCharactersEndpoint.toRoute(countCharacters _)
val countCharactersRoute: Route = countCharactersEndpoint.toRoute(countCharacters)
```

Note that these functions take one argument, which is a tuple of type `I`. This means that functions which take multiple
arguments need to be converted to a function using a single argument using `.tupled`:

```scala
def logic(s: String, i: Int): Future[Either[Unit, String]] = ???
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.
other routes. The tapir-generated `Route`/`Directive` captures from the request only what is described by the endpoint.

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,
Expand Down
20 changes: 11 additions & 9 deletions doc/server/http4s.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,11 @@ This adds two extension methods to the `Endpoint` type: `toRoutes` and `toRoutes
logic of the endpoint to be given as a function of type:

```scala
[I as function arguments] => F[Either[E, O]]
I => F[Either[E, O]]
```

where `F[_]` is the chosen effect type. The second recovers errors from failed effects, and hence requires that `E` is
a subclass of `Throwable` (an exception); it expects a function of type:

```scala
[I as function arguments] => F[O]
```

Note that the function doesn't take the tuple `I` directly as input, but instead this is converted to a function of the
appropriate arity. For example:
a subclass of `Throwable` (an exception); it expects a function of type `I => F[O]`. For example:

```scala
import tapir._
Expand All @@ -50,6 +43,15 @@ val countCharactersRoutes: HttpRoutes[IO] =
countCharactersEndpoint.toRoutes(countCharacters _)
```

Note that these functions take one argument, which is a tuple of type `I`. This means that functions which take multiple
arguments need to be converted to a function using a single argument using `.tupled`:

```scala
def logic(s: String, i: Int): IO[Either[Unit, String]] = ???
val anEndpoint: Endpoint[(String, Int), Unit, String, Nothing] = ???
val aRoute: Route = anEndpoint.toRoute((logic _).tupled)
```

The created `HttpRoutes` are the usual http4s `Kleisli`-based transformation of a `Request` to a `Response`, and can
be further composed using http4s middlewares or request-transforming functions. The tapir-generated `HttpRoutes`
captures from the request only what is described by the endpoint.
Expand Down
6 changes: 3 additions & 3 deletions playground/src/main/scala/tapir/example/BooksExample.scala
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ object BooksExample extends App with StrictLogging {

// interpreting the endpoint description and converting it to an akka-http route, providing the logic which
// should be run when the endpoint is invoked.
addBook.toRoute(bookAddLogic _) ~
booksListing.toRoute(bookListingLogic _) ~
booksListingByGenre.toRoute(bookListingByGenreLogic _)
addBook.toRoute((bookAddLogic _).tupled) ~
booksListing.toRoute(bookListingLogic) ~
booksListingByGenre.toRoute(bookListingByGenreLogic)
}

def startServer(route: Route, yaml: String): Unit = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import akka.http.scaladsl.server._
import akka.http.scaladsl.server.util.{Tuple => AkkaTuple}
import tapir._
import tapir.model.StatusCodes
import tapir.typelevel.{ParamsAsArgs, ParamsToTuple}
import tapir.typelevel.ParamsToTuple

import scala.concurrent.Future

Expand All @@ -18,10 +18,9 @@ class EndpointToAkkaServer(serverOptions: AkkaHttpServerOptions) {
}
}

def toRoute[I, E, O, FN[_]](e: Endpoint[I, E, O, AkkaStream])(logic: FN[Future[Either[E, O]]])(
implicit paramsAsArgs: ParamsAsArgs.Aux[I, FN]): Route = {
def toRoute[I, E, O](e: Endpoint[I, E, O, AkkaStream])(logic: I => Future[Either[E, O]]): Route = {
toDirective1(e) { values =>
onSuccess(paramsAsArgs.applyFn(logic, values)) {
onSuccess(logic(values)) {
case Left(v) => outputToRoute(StatusCodes.BadRequest, e.errorOutput, v)
case Right(v) => outputToRoute(StatusCodes.Ok, e.output, v)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,12 @@ trait TapirAkkaHttpServer {
def toDirective[T](implicit paramsToTuple: ParamsToTuple.Aux[I, T], akkaHttpOptions: AkkaHttpServerOptions): Directive[T] =
new EndpointToAkkaServer(akkaHttpOptions).toDirective(e)

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

def toRouteRecoverErrors[FN[_]](logic: FN[Future[O]])(implicit paramsAsArgs: ParamsAsArgs.Aux[I, FN],
serverOptions: AkkaHttpServerOptions,
eIsThrowable: E <:< Throwable,
eClassTag: ClassTag[E]): Route = {
def toRouteRecoverErrors(logic: I => Future[O])(implicit serverOptions: AkkaHttpServerOptions,
eIsThrowable: E <:< Throwable,
eClassTag: ClassTag[E]): Route = {

def reifyFailedFuture(f: Future[O]): Future[Either[E, O]] = {
import ExecutionContext.Implicits.global
Expand All @@ -30,7 +28,7 @@ trait TapirAkkaHttpServer {
}

new EndpointToAkkaServer(serverOptions)
.toRoute(e)(paramsAsArgs.andThen[Future[O], Future[Either[E, O]]](logic, reifyFailedFuture))
.toRoute(e)(logic.andThen(reifyFailedFuture))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import cats.data.NonEmptyList
import cats.effect.{IO, Resource}
import tapir.Endpoint
import tapir.server.tests.ServerTests
import tapir.typelevel.ParamsAsArgs

import scala.concurrent.duration._
import scala.concurrent.{Await, Future}
Expand All @@ -32,14 +31,12 @@ class AkkaHttpServerTests extends ServerTests[Future, AkkaStream, Route] {
super.afterAll()
}

override def route[I, E, O, FN[_]](e: Endpoint[I, E, O, AkkaStream], fn: FN[Future[Either[E, O]]])(
implicit paramsAsArgs: ParamsAsArgs.Aux[I, FN]): Route = {
override def route[I, E, O](e: Endpoint[I, E, O, AkkaStream], fn: I => Future[Either[E, O]]): Route = {
e.toRoute(fn)
}

override def routeRecoverErrors[I, E <: Throwable, O, FN[_]](e: Endpoint[I, E, O, AkkaStream], fn: FN[Future[O]])(
implicit paramsAsArgs: ParamsAsArgs.Aux[I, FN],
eClassTag: ClassTag[E]): Route = {
override def routeRecoverErrors[I, E <: Throwable, O](e: Endpoint[I, E, O, AkkaStream], fn: I => Future[O])(
implicit eClassTag: ClassTag[E]): Route = {
e.toRouteRecoverErrors(fn)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@ import org.http4s.{EntityBody, Headers, HttpRoutes, Request, Response, Status}
import tapir.internal.SeqToParams
import tapir.internal.server.{DecodeInputs, DecodeInputsResult, InputValues}
import tapir.server.DecodeFailureHandling
import tapir.typelevel.ParamsAsArgs
import tapir.{DecodeFailure, DecodeResult, Endpoint, EndpointIO, EndpointInput}

class EndpointToHttp4sServer[F[_]: Sync: ContextShift](serverOptions: Http4sServerOptions[F]) {

def toRoutes[I, E, O, FN[_]](e: Endpoint[I, E, O, EntityBody[F]])(logic: FN[F[Either[E, O]]])(
implicit paramsAsArgs: ParamsAsArgs.Aux[I, FN]): HttpRoutes[F] = {
def toRoutes[I, E, O](e: Endpoint[I, E, O, EntityBody[F]])(logic: I => F[Either[E, O]]): HttpRoutes[F] = {

val service: HttpRoutes[F] = HttpRoutes[F] { req: Request[F] =>
def decodeBody(result: DecodeInputsResult): F[DecodeInputsResult] = {
Expand All @@ -36,14 +34,12 @@ class EndpointToHttp4sServer[F[_]: Sync: ContextShift](serverOptions: Http4sServ

def valuesToResponse(values: DecodeInputsResult.Values): F[Response[F]] = {
val i = SeqToParams(InputValues(e.input, values.values)).asInstanceOf[I]
paramsAsArgs
.applyFn(logic, i)
.map {
case Right(result) =>
makeResponse(Status.Ok, e.output, result)
case Left(err) =>
makeResponse(Status.BadRequest, e.errorOutput, err)
}
logic(i).map {
case Right(result) =>
makeResponse(Status.Ok, e.output, result)
case Left(err) =>
makeResponse(Status.BadRequest, e.errorOutput, err)
}
}

OptionT(decodeBody(DecodeInputs(e.input, new Http4sDecodeInputsContext[F](req))).flatMap {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,23 @@ import scala.reflect.ClassTag

trait TapirHttp4sServer {
implicit class RichHttp4sHttpEndpoint[I, E, O, F[_]](e: Endpoint[I, E, O, EntityBody[F]]) {
def toRoutes[FN[_]](logic: FN[F[Either[E, O]]])(implicit paramsAsArgs: ParamsAsArgs.Aux[I, FN],
serverOptions: Http4sServerOptions[F],
fs: Sync[F],
fcs: ContextShift[F]): HttpRoutes[F] = {
def toRoutes(
logic: I => F[Either[E, O]])(implicit serverOptions: Http4sServerOptions[F], fs: Sync[F], fcs: ContextShift[F]): HttpRoutes[F] = {
new EndpointToHttp4sServer(serverOptions).toRoutes(e)(logic)
}

def toRouteRecoverErrors[FN[_]](logic: FN[F[O]])(implicit paramsAsArgs: ParamsAsArgs.Aux[I, FN],
serverOptions: Http4sServerOptions[F],
fs: Sync[F],
fcs: ContextShift[F],
eIsThrowable: E <:< Throwable,
eClassTag: ClassTag[E]): HttpRoutes[F] = {
def toRouteRecoverErrors(logic: I => F[O])(implicit serverOptions: Http4sServerOptions[F],
fs: Sync[F],
fcs: ContextShift[F],
eIsThrowable: E <:< Throwable,
eClassTag: ClassTag[E]): HttpRoutes[F] = {
def reifyFailedF(f: F[O]): F[Either[E, O]] = {
f.map(Right(_): Either[E, O]).recover {
case e: Throwable if eClassTag.runtimeClass.isInstance(e) => Left(e.asInstanceOf[E]): Either[E, O]
}
}

new EndpointToHttp4sServer(serverOptions)
.toRoutes(e)(paramsAsArgs.andThen[F[O], F[Either[E, O]]](logic, reifyFailedF))
new EndpointToHttp4sServer(serverOptions).toRoutes(e)(logic.andThen(reifyFailedF))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import org.http4s.server.blaze.BlazeServerBuilder
import org.http4s.syntax.kleisli._
import org.http4s.{EntityBody, HttpRoutes, Request, Response}
import tapir.server.tests.ServerTests
import tapir.typelevel.ParamsAsArgs
import tapir.Endpoint

import scala.concurrent.ExecutionContext
Expand All @@ -22,14 +21,12 @@ class Http4sServerTests extends ServerTests[IO, EntityBody[IO], HttpRoutes[IO]]
override def pureResult[T](t: T): IO[T] = IO.pure(t)
override def suspendResult[T](t: => T): IO[T] = IO.apply(t)

override def route[I, E, O, FN[_]](e: Endpoint[I, E, O, EntityBody[IO]], fn: FN[IO[Either[E, O]]])(
implicit paramsAsArgs: ParamsAsArgs.Aux[I, FN]): HttpRoutes[IO] = {
override def route[I, E, O](e: Endpoint[I, E, O, EntityBody[IO]], fn: I => IO[Either[E, O]]): HttpRoutes[IO] = {
e.toRoutes(fn)
}

override def routeRecoverErrors[I, E <: Throwable, O, FN[_]](e: Endpoint[I, E, O, EntityBody[IO]], fn: FN[IO[O]])(
implicit paramsAsArgs: ParamsAsArgs.Aux[I, FN],
eClassTag: ClassTag[E]): HttpRoutes[IO] = {
override def routeRecoverErrors[I, E <: Throwable, O](e: Endpoint[I, E, O, EntityBody[IO]], fn: I => IO[O])(
implicit eClassTag: ClassTag[E]): HttpRoutes[IO] = {
e.toRouteRecoverErrors(fn)
}

Expand Down

0 comments on commit 6a081cd

Please sign in to comment.