Skip to content

Commit

Permalink
Merge 55399ab into 69b022d
Browse files Browse the repository at this point in the history
  • Loading branch information
vkostyukov committed Jun 2, 2015
2 parents 69b022d + 55399ab commit 5744afc
Show file tree
Hide file tree
Showing 9 changed files with 143 additions and 157 deletions.
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,9 @@ This "Hello World!" example is built with the `0.7.0-SNAPSHOT` version of `finch

```scala
import io.finch.route._
import io.finch.micro._
import com.twitter.finagle.Httpx

// this uses the "REST API as a Monad" mode
Httpx.serve(":8080", Get / "hello" /> Micro.value("Hello, World!"))
Httpx.serve(":8080", (Get / "hello" /> "Hello, World!").toService)
```

Documentation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,16 @@ class FinchUserService(implicit
body.as[User].apply(req).flatMap(db.update).map(_ => NoContent())
}

def deleteUsers: Service[HttpRequest, HttpResponse] =
val deleteUsers: Service[HttpRequest, HttpResponse] =
Service.const(db.delete().map(count => Ok(s"$count users deleted")))

val users: Endpoint[HttpRequest, HttpResponse] =
val users: Service[HttpRequest, HttpResponse] = (
Get / "users" / long /> getUser :+:
Get / "users" /> allUsers :+:
Post / "users" /> createUser :+:
Put / "users" /> updateUser :+:
Delete / "users" /> deleteUsers
).toService

val handleExceptions = new SimpleFilter[HttpRequest, HttpResponse] {
def apply(req: HttpRequest, service: Service[HttpRequest, HttpResponse]): Future[HttpResponse] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ import scala.reflect.ClassTag
trait LowPriorityRequestReaderImplicits {

/**
* Creates a [[io.finch.request.DecodeMagnet DecodeMagnet]] from
* [[io.finch.request.DecodeAnyRequest DecodeAnyRequest]].
* Creates a [[DecodeRequest]] from [[DecodeAnyRequest ]].
*/
implicit def decodeRequestFromAnyDecode[A](
implicit d: DecodeAnyRequest, tag: ClassTag[A]
Expand Down
107 changes: 0 additions & 107 deletions core/src/main/scala/io/finch/route/EndpointConversions.scala

This file was deleted.

127 changes: 117 additions & 10 deletions core/src/main/scala/io/finch/route/RouterN.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@

package io.finch.route

import shapeless.{:+:, CNil, Coproduct, Inl, Inr}
import com.twitter.finagle.Service
import com.twitter.util.Future
import io.finch._
import io.finch.request._
import io.finch.response._
import shapeless._
import shapeless.ops.coproduct.Folder

/**
* A router that extracts some value of the type `A` from the given route.
Expand Down Expand Up @@ -129,7 +135,100 @@ trait RouterN[+A] { self =>
* Provides extension methods for [[RouterN]] to support coproduct and path
* syntax.
*/
object RouterN extends EndpointConversions with LowPriorityRouterImplicits {
object RouterN extends LowPriorityRouterImplicits {

private val respondNotFound: Future[HttpResponse] = NotFound().toFuture
private def routerToService[R: ToRequest](r: RouterN[Service[R, HttpResponse]]): Service[R, HttpResponse] =
Service.mk[R, HttpResponse] { req =>
r(requestToRoute[R](implicitly[ToRequest[R]].apply(req))) match {
case Some((Nil, service)) => service(req)
case _ => respondNotFound
}
}

/**
* A polymorphic function value that accepts types that can be transformed into a Finagle service from a request-like
* type to a [[HttpResponse]].
*/
private object EncodeAll extends Poly1 {
/**
* Transforms an [[HttpResponse]] directly into a constant service.
*/
implicit def response[R: ToRequest]: Case.Aux[HttpResponse, Service[R, HttpResponse]] =
at(r => Service.const(r.toFuture))

/**
* Transforms an encodeable value into a constant service.
*/
implicit def encodeable[R: ToRequest, A: EncodeResponse]: Case.Aux[A, Service[R, HttpResponse]] =
at(a => Service.const(Ok(a).toFuture))

/**
* Transforms an [[HttpResponse]] in a future into a constant service.
*/
implicit def futureResponse[R: ToRequest]: Case.Aux[Future[HttpResponse], Service[R, HttpResponse]] =
at(Service.const)

/**
* Transforms an encodeable value in a future into a constant service.
*/
implicit def futureEncodeable[R: ToRequest, A: EncodeResponse]: Case.Aux[Future[A], Service[R, HttpResponse]] =
at(fa => Service.const(fa.map(Ok(_))))

/**
* Transforms a [[RequestReader]] into a service.
*/
implicit def requestReader[R: ToRequest, A: EncodeResponse]: Case.Aux[RequestReader[A], Service[R, HttpResponse]] =
at(reader => Service.mk(req => reader(implicitly[ToRequest[R]].apply(req)).map(Ok(_))))

/**
* An identity transformation for services that return an [[HttpResponse]].
*
* Note that the service may have a static type that is more specific than `Service[R, HttpResponse]`.
*/
implicit def serviceResponse[S, R](implicit
ev: S => Service[R, HttpResponse],
tr: ToRequest[R]
): Case.Aux[S, Service[R, HttpResponse]] =
at(s => Service.mk(req => ev(s)(req)))

/**
* A transformation for services that return an encodeable value. Note that the service may have a static type that
* is more specific than `Service[R, A]`.
*/
implicit def serviceEncodeable[S, R, A](implicit
ev: S => Service[R, A],
tr: ToRequest[R],
ae: EncodeResponse[A]
): Case.Aux[S, Service[R, HttpResponse]] =
at(s => Service.mk(req => ev(s)(req).map(Ok(_))))
}


/**
* An implicit conversion that turns any endpoint with an output type that can be converted into a response into a
* service that returns responses.
*/
implicit def endpointToHttpResponse[A, B](e: Endpoint[A, B])(implicit
encoder: EncodeResponse[B]
): Endpoint[A, HttpResponse] = e.map { service =>
new Service[A, HttpResponse] {
def apply(a: A): Future[HttpResponse] = service(a).map(b => Ok(encoder(b)))
}
}

/**
* Implicitly converts the given `Router[Service[_, _]]` into a service.
*/
implicit def endpointToService[Req, Rep](
r: RouterN[Service[Req, Rep]]
)(implicit ev: Req => HttpRequest): Service[Req, Rep] = new Service[Req, Rep] {
def apply(req: Req): Future[Rep] = r(requestToRoute[Req](req)) match {
case Some((Nil, service)) => service(req)
case _ => RouteNotFound(s"${req.method.toString.toUpperCase} ${req.path}").toFutureException[Rep]
}
}

/**
* Implicit class that provides `:+:` and other operations on any coproduct router.
*/
Expand All @@ -149,27 +248,35 @@ object RouterN extends EndpointConversions with LowPriorityRouterImplicits {

override def toString = s"(${that.toString}|${self.toString})"
}

def toService[R: ToRequest](implicit
folder: Folder.Aux[EncodeAll.type, C, Service[R, HttpResponse]]
): Service[R, HttpResponse] = routerToService(self.map(c => folder(c)))
}

/**
* Implicit class that provides `:+:` on any router.
*/
final implicit class ValueRouterNOps[C](self: RouterN[C]) {
def :+:[A](that: RouterN[A]): RouterN[A :+: C :+: CNil] =
new RouterN[A :+: C :+: CNil] {
def apply(route: Route): Option[(Route, A :+: C :+: CNil)] =
final implicit class ValueRouterNOps[B](self: RouterN[B]) {
def :+:[A](that: RouterN[A]): RouterN[A :+: B :+: CNil] =
new RouterN[A :+: B :+: CNil] {
def apply(route: Route): Option[(Route, A :+: B :+: CNil)] =
(that(route), self(route)) match {
case (aa @ Some((ar, av)), cc @ Some((cr, cv))) =>
if (ar.length <= cr.length) Some((ar, Inl(av))) else Some((cr, Inr(Inl(cv))))
case (a, c) => a.map {
case (aa @ Some((ar, av)), bb @ Some((br, bv))) =>
if (ar.length <= br.length) Some((ar, Inl(av))) else Some((br, Inr(Inl(bv))))
case (a, b) => a.map {
case (r, v) => (r, Inl(v))
} orElse c.map {
} orElse b.map {
case (r, v) => (r, Inr(Inl(v)))
}
}

override def toString = s"(${that.toString}|${self.toString})"
}

def toService[R: ToRequest](implicit
folder: Folder.Aux[EncodeAll.type, B :+: CNil, Service[R, HttpResponse]]
): Service[R, HttpResponse] = routerToService(self.map(b => folder(Inl(b))))
}

/**
Expand Down
23 changes: 11 additions & 12 deletions core/src/test/scala/io/finch/route/RouterSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ class RouterSpec extends FlatSpec with Matchers {
case class Item(s: String)

implicit val encodeItem: EncodeResponse[Item] =
EncodeResponse("plain/text")(_.s)
EncodeResponse("text/plain")(_.s)

implicit val decodeItem: DecodeRequest[Item] =
DecodeRequest(s => Return(Item(s)))
Expand All @@ -269,7 +269,7 @@ class RouterSpec extends FlatSpec with Matchers {
val itemService: Service[HttpRequest, Item] =
Service.const(Item("qux").toFuture)

val endpoint: Endpoint[HttpRequest, HttpResponse] =
val service: Service[HttpRequest, HttpResponse] = (
// Router returning an [[HttpResponse]].
Get / "foo" /> Ok("foo") :+:
// Router returning an encodeable value.
Expand All @@ -284,19 +284,18 @@ class RouterSpec extends FlatSpec with Matchers {
Get / "qux" / "s1" /> responseService :+:
// Router returning a Finagle service returning an encodeable value.
Get / "qux" / "s2" /> itemService
).toService

val route1 = List(MethodToken(Method.Get), PathToken("foo"))
val route2 = List(MethodToken(Method.Get), PathToken("foo"), PathToken("t"))

val res1 = endpoint(route1) match {
case Some((Nil, service)) => Await.result(service(httpx.Request()))
}

val res2 = endpoint(route2) match {
case Some((Nil, service)) => Await.result(service(httpx.Request()))
}
val res1 = Await.result(service(httpx.Request("/foo")))
val res2 = Await.result(service(httpx.Request("/foo/t")))

res1.contentString shouldBe Ok("foo").contentString
res2.contentString shouldBe Ok("t").contentString
}

it should "convert a value router into an endpoint" in {
val s: Service[HttpRequest, HttpResponse] = (Get / "foo" /> "bar").toService

Await.result(s(httpx.Request("/foo"))).contentString shouldBe Ok("bar").contentString
}
}
3 changes: 2 additions & 1 deletion demo/src/main/scala/io/finch/demo/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ object Demo {
}

// An API endpoint.
val api: Endpoint[AuthRequest, HttpResponse] = users | tickets
val api: Service[AuthRequest, HttpResponse] =
(getUser :+: getUsers :+: postUser :+: postTicket).toService

// An HTTP endpoint with exception handler and Auth filter.
val backend: Service[HttpRequest, HttpResponse] =
Expand Down
18 changes: 5 additions & 13 deletions demo/src/main/scala/io/finch/demo/endpoint.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,16 @@ package io.finch.demo
import argonaut.{EncodeJson, Json}
import com.twitter.finagle.Service
import com.twitter.util.Future
import io.finch.HttpResponse
import io.finch.argonaut._
import io.finch.demo.model.Ticket
import io.finch.response._
import io.finch.route._

object endpoint {

import model.User
import model._
import service._

// User endpoint.
val users: Endpoint[AuthRequest, HttpResponse] =
Get / "users" / long /> GetUser :+:
Post / "users" /> PostUser :+:
Get / "users" /> GetAllUsers

// Ticket endpoint.
val tickets: Endpoint[AuthRequest, Ticket] =
Post / "users" / long / "tickets" /> PostUserTicket
val getUser: Endpoint[AuthRequest, User] = Get / "users" / long /> GetUser
val postUser: Endpoint[AuthRequest, User] = Post / "users" /> PostUser
val getUsers: Endpoint[AuthRequest, List[User]] = Get / "users" /> GetAllUsers
val postTicket: Endpoint[AuthRequest, Ticket] = Post / "users" / long / "tickets" /> PostUserTicket
}

0 comments on commit 5744afc

Please sign in to comment.