From 13e31c8c854a5690b34fb018d0e9594e24bbcf3b Mon Sep 17 00:00:00 2001 From: Vladimir Kostyukov Date: Sun, 27 Mar 2016 10:49:37 -0700 Subject: [PATCH] Introduce ServiceBuilder --- core/src/main/scala/io/finch/Endpoint.scala | 28 ++--------- .../main/scala/io/finch/ServiceBuilder.scala | 34 +++++++++++++ .../scala/io/finch/internal/ToResponse.scala | 2 + .../scala/io/finch/internal/ToService.scala | 49 +++++++++++++++++++ .../test/scala/io/finch/EndToEndSpec.scala | 18 ++++--- .../src/main/scala/io/finch/div/Main.scala | 5 +- .../src/main/scala/io/finch/eval/Main.scala | 4 +- .../src/main/scala/io/finch/oauth2/Main.scala | 4 +- .../main/scala/io/finch/streaming/Main.scala | 20 +++----- .../src/main/scala/io/finch/todo/Main.scala | 15 +++--- .../main/scala/io/finch/todo/package.scala | 4 +- .../src/main/scala/io/finch/wrk/Finch.scala | 3 +- 12 files changed, 125 insertions(+), 61 deletions(-) create mode 100644 core/src/main/scala/io/finch/ServiceBuilder.scala create mode 100644 core/src/main/scala/io/finch/internal/ToService.scala diff --git a/core/src/main/scala/io/finch/Endpoint.scala b/core/src/main/scala/io/finch/Endpoint.scala index ef00a7566..26c4547df 100644 --- a/core/src/main/scala/io/finch/Endpoint.scala +++ b/core/src/main/scala/io/finch/Endpoint.scala @@ -4,7 +4,7 @@ import scala.reflect.ClassTag import cats.Alternative import com.twitter.finagle.Service -import com.twitter.finagle.http.{Cookie, Request, Response, Status} +import com.twitter.finagle.http.{Cookie, Request, Response} import com.twitter.util.{Future, Return, Throw, Try} import io.catbird.util.Rerunnable import io.finch.internal._ @@ -260,30 +260,10 @@ trait Endpoint[A] { self => /** * Converts this endpoint to a Finagle service `Request => Future[Response]` that serves JSON. */ + @deprecated("Use ServiceBuilder().respond[ContentType](endpoint).toService instead", "0.11") final def toService(implicit - tro: ToResponse.Aux[Output[A], Witness.`"application/json"`.T] - ): Service[Request, Response] = toServiceAs[Witness.`"application/json"`.T] - - /** - * Converts this endpoint to a Finagle service `Request => Future[Response]` that serves custom - * content-type `CT`. - */ - final def toServiceAs[CT <: String](implicit - tro: ToResponse.Aux[Output[A], CT] - ): Service[Request, Response] = new Service[Request, Response] { - - private[this] val basicEndpointHandler: PartialFunction[Throwable, Output[Nothing]] = { - case e: io.finch.Error => Output.failure(e, Status.BadRequest) - } - - private[this] val safeEndpoint = self.handle(basicEndpointHandler) - - def apply(req: Request): Future[Response] = safeEndpoint(Input(req)) match { - case Some((remainder, output)) if remainder.isEmpty => - output.map(oa => oa.toResponse[CT](req.version)).run - case _ => Future.value(Response(req.version, Status.NotFound)) - } - } + ts: ToService[Endpoint[A] :: HNil, Application.Json :: HNil] + ): Service[Request, Response] = ServiceBuilder().respond[Application.Json](this).toService /** * Recovers from any exception occurred in this endpoint by creating a new endpoint that will diff --git a/core/src/main/scala/io/finch/ServiceBuilder.scala b/core/src/main/scala/io/finch/ServiceBuilder.scala new file mode 100644 index 000000000..a3a929cbe --- /dev/null +++ b/core/src/main/scala/io/finch/ServiceBuilder.scala @@ -0,0 +1,34 @@ +package io.finch + +import com.twitter.finagle.Service +import com.twitter.finagle.http.{Request, Response} +import io.finch.internal.ToService +import shapeless._ + +/** + * Captures the `HList` of endpoints as well as the `HList` of their content-types in order to do + * the `toService` conversion. + * + * {{{ + * + * val api: Service[Request, Response] = ServiceBuilder() + * .respond[Application.Json](getUser :+: postUser) + * .respond[Text.Plain](healthcheck) + * .toService + * }}} + */ +case class ServiceBuilder[ES <: HList, CTS <: HList](endpoints: ES) { self => + + class Respond[CT <: String] { + def apply[E](e: Endpoint[E]): ServiceBuilder[Endpoint[E] :: ES, CT :: CTS] = + self.copy(e :: self.endpoints) + } + + def respond[CT <: String]: Respond[CT] = new Respond[CT] + + def toService(implicit ts: ToService[ES, CTS]): Service[Request, Response] = ts(endpoints) +} + +object ServiceBuilder { + def apply(): ServiceBuilder[HNil, HNil] = ServiceBuilder(HNil) +} diff --git a/core/src/main/scala/io/finch/internal/ToResponse.scala b/core/src/main/scala/io/finch/internal/ToResponse.scala index 5a1ffd9f4..7ad7bea6e 100644 --- a/core/src/main/scala/io/finch/internal/ToResponse.scala +++ b/core/src/main/scala/io/finch/internal/ToResponse.scala @@ -23,6 +23,8 @@ trait LowPriorityToResponseInstances { def apply(a: A): Response = fn(a) } + implicit def responseToResponse[CT <: String]: Aux[Response, CT] = instance(identity) + private[this] def asyncStreamResponseBuilder[A, CT <: String](writer: A => Buf)(implicit w: Witness.Aux[CT] ): Aux[AsyncStream[A], CT] = instance { as => diff --git a/core/src/main/scala/io/finch/internal/ToService.scala b/core/src/main/scala/io/finch/internal/ToService.scala new file mode 100644 index 000000000..16ca2f53f --- /dev/null +++ b/core/src/main/scala/io/finch/internal/ToService.scala @@ -0,0 +1,49 @@ +package io.finch.internal + +import com.twitter.finagle.Service +import com.twitter.finagle.http.{Request, Response, Status } +import com.twitter.util.Future +import io.finch._ +import shapeless._ + +import scala.annotation.implicitNotFound + +@implicitNotFound(""" +You can only convert an endpoint into a Finagle service if its result type is one of the following: + + * 'com.twitter.finagle.http.Response' + * a value of type 'A' with 'io.finch.Encode' instance available (for requested Content-Type) + * a 'shapeless.Coproduct' made up of some combination of the above + +Make sure this rule evaluates for every endpoint passed to 'respond' method on 'ServiceBuilder'. +""") +trait ToService[ES <: HList, CTS <: HList] { + def apply(endpoints: ES): Service[Request, Response] +} + +object ToService { + + implicit val hnilToService: ToService[HNil, HNil] = new ToService[HNil, HNil] { + def apply(endpoints: HNil): Service[Request, Response] = + Service.mk(_ => Future.value(Response(Status.NotFound))) + } + + implicit def hconsToService[A, EH <: Endpoint[A], ET <: HList, CTH <: String, CTT <: HList](implicit + trH: ToResponse.Aux[Output[A], CTH], + tsT: ToService[ET, CTT] + ): ToService[Endpoint[A] :: ET, CTH :: CTT] = new ToService[Endpoint[A] :: ET, CTH :: CTT] { + def apply(endpoints: Endpoint[A] :: ET): Service[Request, Response] = new Service[Request, Response] { + private[this] val basicEndpointHandler: PartialFunction[Throwable, Output[Nothing]] = { + case e: io.finch.Error => Output.failure(e, Status.BadRequest) + } + + private[this] val safeEndpoint = endpoints.head.handle(basicEndpointHandler) + + def apply(req: Request): Future[Response] = safeEndpoint(Input(req)) match { + case Some((remainder, output)) if remainder.isEmpty => + output.map(o => o.toResponse[CTH](req.version)).run + case _ => tsT(endpoints.tail)(req) + } + } + } +} diff --git a/core/src/test/scala/io/finch/EndToEndSpec.scala b/core/src/test/scala/io/finch/EndToEndSpec.scala index 166c422c3..93b8002df 100644 --- a/core/src/test/scala/io/finch/EndToEndSpec.scala +++ b/core/src/test/scala/io/finch/EndToEndSpec.scala @@ -4,7 +4,6 @@ import com.twitter.finagle.Service import com.twitter.finagle.http.{Request, Response, Status} import com.twitter.io.Buf import com.twitter.util.{Await, Return} -import shapeless.Witness class EndToEndSpec extends FinchSpec { @@ -22,12 +21,14 @@ class EndToEndSpec extends FinchSpec { implicit val encodeException: Encode.TextPlain[Exception] = Encode.text(_ => Buf.Utf8("ERR!")) - val service: Service[Request, Response] = ( - get("foo" :: string) { s: String => Ok(Foo(s)) } :+: - get("bar") { Created("bar") } :+: - get("baz") { BadRequest(new IllegalArgumentException("foo")): Output[Unit] } :+: - get("qux" :: param("foo").as[Foo]) { f: Foo => Created(f) } - ).toServiceAs[Witness.`"text/plain"`.T] + val service: Service[Request, Response] = ServiceBuilder() + .respond[Text.Plain]( + get("foo" :: string) { s: String => Ok(Foo(s)) } :+: + get("bar") { Created("bar") } :+: + get("baz") { BadRequest(new IllegalArgumentException("foo")): Output[Unit] } :+: + get("qux" :: param("foo").as[Foo]) { f: Foo => Created(f) } + ) + .toService val rep1 = Await.result(service(Request("/foo/bar"))) rep1.contentString shouldBe "bar" @@ -48,7 +49,8 @@ class EndToEndSpec extends FinchSpec { it should "convert value Endpoints into Services" in { val e: Endpoint[String] = get("foo") { Created("bar") } - val s: Service[Request, Response] = e.toServiceAs[Witness.`"text/plain"`.T] + val s: Service[Request, Response] = + ServiceBuilder().respond[Text.Plain](e).toService val rep = Await.result(s(Request("/foo"))) rep.contentString shouldBe "bar" diff --git a/examples/src/main/scala/io/finch/div/Main.scala b/examples/src/main/scala/io/finch/div/Main.scala index bb225177c..c2732ba00 100644 --- a/examples/src/main/scala/io/finch/div/Main.scala +++ b/examples/src/main/scala/io/finch/div/Main.scala @@ -4,7 +4,6 @@ import cats.std.int._ import com.twitter.finagle.Http import com.twitter.util.Await import io.finch._ -import shapeless._ /** * A tiny Finch application that serves a single endpoint `POST /:a/b:` that divides `a` by `b`. @@ -32,5 +31,7 @@ object Main extends App { case e: ArithmeticException => BadRequest(e) } - Await.ready(Http.server.serve(":8081", div.toServiceAs[Text.Plain])) + Await.ready(Http.server.serve(":8081", + ServiceBuilder().respond[Text.Plain](div).toService + )) } diff --git a/examples/src/main/scala/io/finch/eval/Main.scala b/examples/src/main/scala/io/finch/eval/Main.scala index 8cc184546..47088a814 100644 --- a/examples/src/main/scala/io/finch/eval/Main.scala +++ b/examples/src/main/scala/io/finch/eval/Main.scala @@ -44,5 +44,7 @@ object Main extends App { case e: Exception => BadRequest(e) } - Await.ready(Http.server.serve(":8081", eval.toService)) + Await.ready(Http.server.serve(":8081", + ServiceBuilder().respond[Application.Json](eval).toService + )) } diff --git a/examples/src/main/scala/io/finch/oauth2/Main.scala b/examples/src/main/scala/io/finch/oauth2/Main.scala index 7890797cd..177961aa0 100644 --- a/examples/src/main/scala/io/finch/oauth2/Main.scala +++ b/examples/src/main/scala/io/finch/oauth2/Main.scala @@ -49,5 +49,7 @@ object Main extends App { Ok(UnprotectedUser("unprotected")) } - Await.ready(Http.server.serve(":8081", (tokens :+: users :+: unprotected).toService)) + Await.ready(Http.server.serve(":8081", + ServiceBuilder().respond[Application.Json](tokens :+: users :+: unprotected).toService + )) } diff --git a/examples/src/main/scala/io/finch/streaming/Main.scala b/examples/src/main/scala/io/finch/streaming/Main.scala index fbd66399b..b68fcada8 100644 --- a/examples/src/main/scala/io/finch/streaming/Main.scala +++ b/examples/src/main/scala/io/finch/streaming/Main.scala @@ -5,9 +5,9 @@ import java.util.concurrent.atomic.AtomicLong import cats.std.long._ import com.twitter.concurrent.AsyncStream import com.twitter.finagle.{Http, Service} -import com.twitter.finagle.http.{Request, Response, Status} +import com.twitter.finagle.http.{Request, Response} import com.twitter.io.Buf -import com.twitter.util.{Await, Future, Try} +import com.twitter.util.{Await, Try} import io.circe.generic.auto._ import io.finch._ import io.finch.circe._ @@ -72,18 +72,10 @@ object Main extends App { Ok(as.map(b => sum.addAndGet(bufToLong(b)))) } - val textPlainService = (sumSoFar :+: sumTo :+: totalSum).toServiceAs[Text.Plain] - val jsonService = exampleGenerator.toService - - // TOOD: Fix this once we support multiple content-types - val service = new Service[Request, Response] { - override def apply(request: Request): Future[Response] = { - textPlainService(request).flatMap { - case response if response.status == Status.NotFound => jsonService(request) - case response => Future value response - } - } - } + val service: Service[Request, Response] = ServiceBuilder() + .respond[Text.Plain](sumSoFar :+: sumTo :+: totalSum) + .respond[Application.Json](exampleGenerator) + .toService Await.result(Http.server .withStreaming(enabled = true) diff --git a/examples/src/main/scala/io/finch/todo/Main.scala b/examples/src/main/scala/io/finch/todo/Main.scala index 93fe112e2..b67999077 100644 --- a/examples/src/main/scala/io/finch/todo/Main.scala +++ b/examples/src/main/scala/io/finch/todo/Main.scala @@ -3,8 +3,7 @@ package io.finch.todo import java.util.UUID import com.twitter.app.Flag -import com.twitter.finagle.{Http, Service} -import com.twitter.finagle.http.{Request, Response} +import com.twitter.finagle.Http import com.twitter.finagle.stats.Counter import com.twitter.server.TwitterServer import com.twitter.util.Await @@ -79,18 +78,20 @@ object Main extends TwitterServer { } } - val api: Service[Request, Response] = ( - getTodos :+: postTodo :+: deleteTodo :+: deleteTodos :+: patchTodo - ).handle({ + val api = (getTodos :+: postTodo :+: deleteTodo :+: deleteTodos :+: patchTodo).handle { case e: TodoNotFound => NotFound(e) - }).toService + } def main(): Unit = { log.info("Serving the Todo application") val server = Http.server .withStatsReceiver(statsReceiver) - .serve(s":${port()}", api) + .serve(s":${port()}", ServiceBuilder() + .respond[Application.Json](api) + .respond[Text.Plain](get("ping") { Ok("pong") } ) + .toService + ) onExit { server.close() } diff --git a/examples/src/main/scala/io/finch/todo/package.scala b/examples/src/main/scala/io/finch/todo/package.scala index d7131aaba..5eff10529 100644 --- a/examples/src/main/scala/io/finch/todo/package.scala +++ b/examples/src/main/scala/io/finch/todo/package.scala @@ -5,8 +5,8 @@ import io.circe.{Encoder, Json} package object todo { implicit val encodeException: Encoder[Exception] = Encoder.instance(e => Json.obj( - "type" -> Json.string(e.getClass.getSimpleName), - "message" -> Json.string(e.getMessage) + "type" -> Json.fromString(e.getClass.getSimpleName), + "message" -> Json.fromString(e.getMessage) ) ) } diff --git a/examples/src/main/scala/io/finch/wrk/Finch.scala b/examples/src/main/scala/io/finch/wrk/Finch.scala index 6e7a4cf17..8889a637a 100644 --- a/examples/src/main/scala/io/finch/wrk/Finch.scala +++ b/examples/src/main/scala/io/finch/wrk/Finch.scala @@ -3,7 +3,6 @@ package io.finch.wrk import io.circe.generic.auto._ import io.finch._ import io.finch.circe._ -import shapeless.Witness /** * How to benchmark this: @@ -21,5 +20,5 @@ object Finch extends App { val roundTrip: Endpoint[Payload] = post(body.as[Payload]) - serve(roundTrip.toServiceAs[Witness.`"application/json"`.T]) + serve(ServiceBuilder().respond[Application.Json](roundTrip).toService) }