Skip to content

Commit

Permalink
Introduce Bootstrap
Browse files Browse the repository at this point in the history
  • Loading branch information
vkostyukov committed Jun 13, 2017
1 parent c64c966 commit 5fc4c74
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 11 deletions.
33 changes: 33 additions & 0 deletions core/src/main/scala/io/finch/Bootstrap.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.finch

import com.twitter.finagle.Service
import com.twitter.finagle.http.{Request, Response}
import io.finch.internal.ToService
import shapeless._

/**
* Bootstraps a Finagle HTTP service out of the collection of Finch endpoints.
*
* {{{
*
* val api: Service[Request, Response] = Bootstrap
* .serve[Application.Json](getUser :+: postUser)
* .serve[Text.Plain](healthcheck)
* .toService
* }}}
*
* @note This API is experimental/unstable. Use it with caution.
*/
case class Bootstrap[ES <: HList, CTS <: HList](es: ES) { self =>

class Serve[CT <: String] {
def apply[E](e: Endpoint[E]): Bootstrap[Endpoint[E] :: ES, CT :: CTS] =
self.copy(e :: self.es)
}

def serve[CT <: String]: Serve[CT] = new Serve[CT]

def toService(implicit ts: ToService[ES, CTS]): Service[Request, Response] = ts(es)
}

object Bootstrap extends Bootstrap[HNil, HNil](HNil)
6 changes: 5 additions & 1 deletion core/src/main/scala/io/finch/Endpoint.scala
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ trait Endpoint[A] { self =>

/**
* Converts this endpoint to a Finagle service `Request => Future[Response]` that serves JSON.
*
* Consider using [[Bootstrap]] instead.
*/
final def toService(implicit
tr: ToResponse.Aux[A, Application.Json],
Expand All @@ -255,11 +257,13 @@ trait Endpoint[A] { self =>
/**
* Converts this endpoint to a Finagle service `Request => Future[Response]` that serves custom
* content-type `CT`.
*
* Consider using [[Bootstrap]] instead.
*/
final def toServiceAs[CT <: String](implicit
tr: ToResponse.Aux[A, CT],
tre: ToResponse.Aux[Exception, CT]
): Service[Request, Response] = new ToService(self, tr, tre)
): Service[Request, Response] = Bootstrap.serve[CT](this).toService

/**
* Recovers from any exception occurred in this endpoint by creating a new endpoint that will
Expand Down
61 changes: 51 additions & 10 deletions core/src/main/scala/io/finch/internal/ToService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,33 @@ import com.twitter.finagle.Service
import com.twitter.finagle.http.{Request, Response, Status, Version}
import com.twitter.util.Future
import io.finch.{Endpoint, EndpointResult, Input, Output}
import scala.annotation.implicitNotFound
import shapeless._

/**
* Wraps a given list of [[Endpoint]]s and their content-types with a Finagle [[Service]].
*
* Guarantees to:
*
* - handle Finch's own errors (i.e., [[Error]] and [[Error]]) as 400s
* - supply the date header to each response
* - copy requests's HTTP version onto a response
* - respond with 404 when en endpoint is not matched
*/
@implicitNotFound(
"""An Endpoint you're trying to convert into a Finagle service is missing one or more encoders.
Make sure ${A} is one of the following:
* A com.twitter.finagle.http.Response
* A value of a type with an io.finch.Encode instance (with the corresponding content-type)
* A coproduct made up of some combination of the above
See https://github.com/finagle/finch/blob/master/docs/cookbook.md#fixing-the-toservice-compile-error
""")
trait ToService[ES <: HList, CTS <: HList] {
def apply(es: ES): Service[Request, Response]
}

/**
* Wraps a given [[Endpoint]] with a Finagle [[Service]].
Expand All @@ -14,25 +41,39 @@ import io.finch.{Endpoint, EndpointResult, Input, Output}
* - copy requests's HTTP version onto a response
* - respond with 404 when en endpoint is not matched
*/
private[finch] final class ToService[A, CT <: String](
e: Endpoint[A],
tr: ToResponse.Aux[A, CT],
tre: ToResponse.Aux[Exception, CT]) extends Service[Request, Response] {
object ToService {

private[this] val underlying = e.handle {
private val respond400OnErrors: PartialFunction[Throwable, Output[Nothing]] = {
case e: io.finch.Error => Output.failure(e, Status.BadRequest)
case es: io.finch.Errors => Output.failure(es, Status.BadRequest)
}

private[this] def conformHttp(rep: Response, version: Version): Response = {
private def conformHttp(rep: Response, version: Version): Response = {
rep.version = version
rep.date = currentTime()
rep
}

def apply(req: Request): Future[Response] = underlying(Input.fromRequest(req)) match {
case EndpointResult.Matched(rem, out) if rem.route.isEmpty =>
out.map(oa => conformHttp(oa.toResponse(tr, tre), req.version)).run
case _ => Future.value(conformHttp(Response(Status.NotFound), req.version))
implicit val hnilTS: ToService[HNil, HNil] = new ToService[HNil, HNil] {
def apply(es: HNil): Service[Request, Response] = new Service[Request, Response] {
def apply(req: Request): Future[Response] =
Future.value(conformHttp(Response(Status.NotFound), req.version))
}
}

implicit def hlistTS[A, EH <: Endpoint[A], ET <: HList, CTH <: String, CTT <: HList](implicit
trA: ToResponse.Aux[A, CTH],
trE: ToResponse.Aux[Exception, CTH],
tsT: ToService[ET, CTT]
): ToService[Endpoint[A] :: ET, CTH :: CTT] = new ToService[Endpoint[A] :: ET, CTH :: CTT] {
def apply(es: Endpoint[A] :: ET): Service[Request, Response] = new Service[Request, Response] {
private[this] val underlying = es.head.handle(respond400OnErrors)

def apply(req: Request): Future[Response] = underlying(Input.fromRequest(req)) match {
case EndpointResult.Matched(rem, out) if rem.route.isEmpty =>
out.map(oa => conformHttp(oa.toResponse(trA, trE), req.version)).run
case _ => tsT(es.tail)(req)
}
}
}
}

0 comments on commit 5fc4c74

Please sign in to comment.