Skip to content

Commit

Permalink
Introduce ServiceBuilder
Browse files Browse the repository at this point in the history
  • Loading branch information
vkostyukov committed Apr 5, 2016
1 parent b047b1b commit 13e31c8
Show file tree
Hide file tree
Showing 12 changed files with 125 additions and 61 deletions.
28 changes: 4 additions & 24 deletions core/src/main/scala/io/finch/Endpoint.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions core/src/main/scala/io/finch/ServiceBuilder.scala
Original file line number Diff line number Diff line change
@@ -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)
}
2 changes: 2 additions & 0 deletions core/src/main/scala/io/finch/internal/ToResponse.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand Down
49 changes: 49 additions & 0 deletions core/src/main/scala/io/finch/internal/ToService.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
18 changes: 10 additions & 8 deletions core/src/test/scala/io/finch/EndToEndSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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"
Expand All @@ -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"
Expand Down
5 changes: 3 additions & 2 deletions examples/src/main/scala/io/finch/div/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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
))
}
4 changes: 3 additions & 1 deletion examples/src/main/scala/io/finch/eval/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
))
}
4 changes: 3 additions & 1 deletion examples/src/main/scala/io/finch/oauth2/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
))
}
20 changes: 6 additions & 14 deletions examples/src/main/scala/io/finch/streaming/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down Expand Up @@ -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)
Expand Down
15 changes: 8 additions & 7 deletions examples/src/main/scala/io/finch/todo/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() }

Expand Down
4 changes: 2 additions & 2 deletions examples/src/main/scala/io/finch/todo/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
)
}
3 changes: 1 addition & 2 deletions examples/src/main/scala/io/finch/wrk/Finch.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
}

0 comments on commit 13e31c8

Please sign in to comment.