From ae1d25ed87ded9ae1f69c6eb9d4bb639a1aa8f39 Mon Sep 17 00:00:00 2001 From: adamw Date: Wed, 16 Sep 2020 10:03:27 +0200 Subject: [PATCH] Improve inference, make examples in docs compiled --- .../internal/SchemaMagnoliaDerivation.scala | 1 - doc/server/akkahttp.md | 31 +++++-- generated-doc/out/endpoint/customtypes.md | 4 +- generated-doc/out/server/akkahttp.md | 81 ++++++++++++++++--- .../akkahttp/EndpointToAkkaServer.scala | 47 +++++------ .../server/akkahttp/TapirAkkaHttpServer.scala | 27 ++++--- 6 files changed, 138 insertions(+), 53 deletions(-) diff --git a/core/src/main/scala/sttp/tapir/generic/internal/SchemaMagnoliaDerivation.scala b/core/src/main/scala/sttp/tapir/generic/internal/SchemaMagnoliaDerivation.scala index e15df30548..092dabe588 100644 --- a/core/src/main/scala/sttp/tapir/generic/internal/SchemaMagnoliaDerivation.scala +++ b/core/src/main/scala/sttp/tapir/generic/internal/SchemaMagnoliaDerivation.scala @@ -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] diff --git a/doc/server/akkahttp.md b/doc/server/akkahttp.md index ffcf60338a..2db6e95ac4 100644 --- a/doc/server/akkahttp.md +++ b/doc/server/akkahttp.md @@ -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: @@ -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 @@ -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)) } } diff --git a/generated-doc/out/endpoint/customtypes.md b/generated-doc/out/endpoint/customtypes.md index f5dc445be1..21d1c194d4 100644 --- a/generated-doc/out/endpoint/customtypes.md +++ b/generated-doc/out/endpoint/customtypes.md @@ -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: diff --git a/generated-doc/out/server/akkahttp.md b/generated-doc/out/server/akkahttp.md index 070292cc36..0a31c79add 100644 --- a/generated-doc/out/server/akkahttp.md +++ b/generated-doc/out/server/akkahttp.md @@ -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: @@ -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 @@ -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 diff --git a/server/akka-http-server/src/main/scala/sttp/tapir/server/akkahttp/EndpointToAkkaServer.scala b/server/akka-http-server/src/main/scala/sttp/tapir/server/akkahttp/EndpointToAkkaServer.scala index 5a6d720948..ae273ab98c 100644 --- a/server/akka-http-server/src/main/scala/sttp/tapir/server/akkahttp/EndpointToAkkaServer.scala +++ b/server/akka-http-server/src/main/scala/sttp/tapir/server/akkahttp/EndpointToAkkaServer.scala @@ -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)) } } diff --git a/server/akka-http-server/src/main/scala/sttp/tapir/server/akkahttp/TapirAkkaHttpServer.scala b/server/akka-http-server/src/main/scala/sttp/tapir/server/akkahttp/TapirAkkaHttpServer.scala index 6beae540c7..1c18c13733 100644 --- a/server/akka-http-server/src/main/scala/sttp/tapir/server/akkahttp/TapirAkkaHttpServer.scala +++ b/server/akka-http-server/src/main/scala/sttp/tapir/server/akkahttp/TapirAkkaHttpServer.scala @@ -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) } }