Skip to content

Commit

Permalink
Finch in Action
Browse files Browse the repository at this point in the history
  • Loading branch information
vkostyukov committed Feb 27, 2015
1 parent 49edd59 commit cfd7922
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 77 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ lazy val demo = project
lazy val playground = project
.settings(moduleName := "finch-playground")
.settings(allSettings: _*)
.dependsOn(core)
.dependsOn(core, jackson)
.disablePlugins(CoverallsPlugin)

lazy val jawn = project
Expand Down
61 changes: 61 additions & 0 deletions core/src/main/scala/io/finch/micro/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.finch

import com.twitter.finagle.Service
import com.twitter.util.Future

import io.finch.route.{Endpoint => _, _}
import io.finch.response._
import io.finch.request._

/**
* An experimental package that enables `micro`-services support in Finch.
*/
package object micro {

/**
* `RequestReader` is a composable `Microservice`.
*/
type Micro[A] = RequestReader[A]

/**
* Codename for new-style endpoint.
*/
type Endpoint[A] = Router[Micro[A]]

/**
* An HTTP endpoints.
*/
type Endpoints = Seq[Endpoint[HttpResponse]]

implicit def endpointsToHttpService(es: Endpoints): Service[HttpRequest, HttpResponse] =
anyRouterToHttpService(es.reduce(_ | _))

implicit def anyToHttp[A](a: A)(implicit e: EncodeResponse[A]): HttpResponse = Ok(a)

implicit def anyMicroToHttpMicro[A](
m: Micro[A]
)(implicit ev: A => HttpResponse): Micro[HttpResponse] = m.map(ev)

implicit def anyMicroToHttpService[A](
m: Micro[A]
)(implicit ev: A => HttpResponse): Service[HttpRequest, HttpResponse] = new Service[HttpRequest, HttpResponse] {
def apply(req: HttpRequest): Future[HttpResponse] = m.map(ev)(req)
}

implicit def anyRouterToHttpRouter[A](
r: RouterN[A]
)(implicit ev: A => Micro[HttpResponse]): RouterN[Micro[HttpResponse]] = r.map(ev)

/**
* Implicitly converts new-style endpoint into `Service`.
*/
implicit def anyRouterToHttpService[A](
e: RouterN[A]
)(implicit ev: A => Micro[HttpResponse]): Service[HttpRequest, HttpResponse] =
new Service[HttpRequest, HttpResponse] {
def apply(req: HttpRequest): Future[HttpResponse] = e.map(ev)(requestToRoute(req)) match {
case Some((Nil, rr)) => rr(req)
case _ => RouteNotFound(s"${req.method.toString.toUpperCase} ${req.path}").toFutureException
}
}
}
6 changes: 3 additions & 3 deletions core/src/main/scala/io/finch/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@
package io

import com.twitter.finagle.httpx.path.Path

import scala.language.implicitConversions
import io.finch.response.{Ok, EncodeResponse}

import com.twitter.finagle.httpx
import com.twitter.util.Future
import com.twitter.finagle.{Filter, Service}
import com.twitter.finagle.Service
import com.twitter.finagle.Filter

/**
* This is a root package of the Finch library, which provides an immutable layer of functions and types atop of Finagle
Expand Down
64 changes: 64 additions & 0 deletions core/src/main/scala/io/finch/request/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ package request {
def apply(req: String): Try[A] = d(req)(tag)
}
}

/**
* Adds a `~>` compositor to `RequestReader` to compose it with function of one argument.
*/
implicit class RrArrow1[A](rr: RequestReader[A]) {
def ~>[B](fn: A => Future[B]): RequestReader[B] =
rr.embedFlatMap(fn)
}
}
}

Expand Down Expand Up @@ -247,6 +255,62 @@ package object request extends LowPriorityImplicits {
case None => true
}

/**
* Adds a `~>` compositor to `RequestReader` to compose it with function of two arguments.
*/
implicit class RrArrow2[A, B](val rr: RequestReader[A ~ B]) extends AnyVal {
def ~>[C](fn: (A, B) => Future[C]): RequestReader[C] =
rr.embedFlatMap { case (a ~ b) => fn(a, b) }
}

/**
* Adds a `~>` compositor to `RequestReader` to compose it with function of three arguments.
*/
implicit class RrArrow3[A, B, C](val rr: RequestReader[A ~ B ~ C]) extends AnyVal {
def ~>[D](fn: (A, B, C) => Future[D]): RequestReader[D] =
rr.embedFlatMap { case (a ~ b ~ c) => fn(a, b, c) }
}

/**
* Adds a `~>` compositor to `RequestReader` to compose it with function of four arguments.
*/
implicit class RrArrow4[A, B, C, D](val rr: RequestReader[A ~ B ~ C ~ D]) extends AnyVal {
def ~>[E](fn: (A, B, C, D) => Future[E]): RequestReader[E] =
rr.embedFlatMap { case (a ~ b ~ c ~ d) => fn(a, b, c, d) }
}

/**
* Adds a `~>` compositor to `RequestReader` to compose it with function of five arguments.
*/
implicit class RrArrow5[A, B, C, D, E](val rr: RequestReader[A ~ B ~ C ~ D ~ E]) extends AnyVal {
def ~>[F](fn: (A, B, C, D, E) => Future[F]): RequestReader[F] =
rr.embedFlatMap { case (a ~ b ~ c ~ d ~ e) => fn(a, b, c, d, e) }
}

/**
* Adds a `~>` compositor to `RequestReader` to compose it with function of six arguments.
*/
implicit class RrArrow6[A, B, C, D, E, F](val rr: RequestReader[A ~ B ~ C ~ D ~ E ~ F]) extends AnyVal {
def ~>[G](fn: (A, B, C, D, E, F) => Future[G]): RequestReader[G] =
rr.embedFlatMap { case (a ~ b ~ c ~ d ~ e ~ f) => fn(a, b, c, d, e, f) }
}

/**
* Adds a `~>` compositor to `RequestReader` to compose it with function of seven arguments.
*/
implicit class RrArrow7[A, B, C, D, E, F, G](val rr: RequestReader[A ~ B ~ C ~ D ~ E ~ F ~ G]) extends AnyVal {
def ~>[H](fn: (A, B, C, D, E, F, G) => Future[H]): RequestReader[H] =
rr.embedFlatMap { case (a ~ b ~ c ~ d ~ e ~ f ~ g) => fn(a, b, c, d, e, f, g) }
}

/**
* Adds a `~>` compositor to `RequestReader` to compose it with function of eight arguments.
*/
implicit class RrArrow8[A, B, C, D, E, F, G, H](val rr: RequestReader[A ~ B ~ C ~ D ~ E ~ F ~ G ~ H]) extends AnyVal {
def ~>[I](fn: (A, B, C, D, E, F, G, H) => Future[I]): RequestReader[I] =
rr.embedFlatMap { case (a ~ b ~ c ~ d ~ e ~ f ~ g ~ h) => fn(a, b, c, d, e, f, g, h) }
}

// Helper functions.
private[request] def requestParam(param: String)(req: HttpRequest): Option[String] =
req.params.get(param) orElse {
Expand Down
6 changes: 0 additions & 6 deletions core/src/main/scala/io/finch/route/Router.scala
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,6 @@ trait RouterN[+A] { self =>
def /(that: Router0): RouterN[A] =
this andThen that

/**
* Maps this router to the given function `A => B`.
*/
def />[B](fn: A => B): RouterN[B] =
this map fn

/**
* Sequentially composes this router with the given `that` router. The resulting router will succeed if either this or
* `that` routers are succeed.
Expand Down
56 changes: 46 additions & 10 deletions core/src/main/scala/io/finch/route/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ import com.twitter.finagle.Service
import com.twitter.finagle.httpx.Method
import com.twitter.util.Future

package route {

trait LowPriorityImplicits {

/**
* Add `/>` compositor to `RouterN` to compose it with function of one argument.
*/
implicit class RArrow1[A](r: RouterN[A]) {
def />[B](fn: A => B): RouterN[B] = r.map(fn)
}
}
}

/**
* This package contains various of functions and types that enable _router combinators_ in Finch. A Finch
* [[io.finch.route.Router Router]] is an abstraction that is responsible for routing the HTTP requests using their
Expand All @@ -49,7 +62,7 @@ import com.twitter.util.Future
* )
* }}}
*/
package object route {
package object route extends LowPriorityImplicits {

object tokens {
//
Expand Down Expand Up @@ -89,23 +102,46 @@ package object route {
*/
case class RouteNotFound(r: String) extends Exception(s"Route not found: $r")

/**
* Implicitly converts `Req` to `Route`.
*/
def requestToRoute[Req](req: Req)(implicit ev: Req => HttpRequest): Route =
(MethodToken(req.method): RouteToken) :: (req.path.split("/").toList.drop(1) map PathToken)

/**
* 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)) match {
case Some((Nil, service)) => service(req)
case _ => RouteNotFound(s"${req.method.toString.toUpperCase} ${req.path}").toFutureException
}
}

private def requestToRoute(req: Req)(implicit ev: Req => HttpRequest): Route =
(MethodToken(req.method): RouteToken) :: (req.path.split("/").toList.drop(1) map PathToken)
/**
* Add `/>` compositor to `RouterN` to compose it with function of two argument.
*/
implicit class RArrow2[A, B](val r: RouterN[A / B]) extends AnyVal {
def />[C](fn: (A, B) => C): RouterN[C] =
r.map { case a / b => fn(a, b) }
}

def apply(req: Req): Future[Rep] = {
val path = requestToRoute(req)
r(path) match {
case Some((Nil, service)) => service(req)
case _ => RouteNotFound(s"${req.method.toString.toUpperCase} ${req.path}").toFutureException
}
}
/**
* Add `/>` compositor to `RouterN` to compose it with function of three argument.
*/
implicit class RArrow3[A, B, C](val r: RouterN[A / B / C]) extends AnyVal {
def />[D](fn: (A, B, C) => D): RouterN[D] =
r.map { case a / b / c => fn(a, b, c) }
}

/**
* Add `/>` compositor to `RouterN` to compose it with function of four argument.
*/
implicit class RArrow4[A, B, C, D](val r: RouterN[A / B / C / D]) extends AnyVal {
def />[E](fn: (A, B, C, D) => E): RouterN[E] =
r.map { case a / b / c / d => fn(a, b, c, d) }
}

implicit def intToMatcher(i: Int): Router0 = new Matcher(i.toString)
Expand Down
2 changes: 1 addition & 1 deletion core/src/test/scala/io/finch/route/RouterSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ class RouterSpec extends FlatSpec with Matchers {

it should "be composable as an endpoint" in {
val r1 = Get / "a" / int /> { _ + 10 }
val r2 = Get / "b" / int / int /> { case a / b => a + b }
val r2 = Get / "b" / int / int /> { _ + _ }
val r3 = r1 | r2

r3(route) shouldBe Some((route.drop(3), 11))
Expand Down
86 changes: 30 additions & 56 deletions playground/src/main/scala/io/finch/playgorund/Main.scala
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package io.finch.playgorund

import com.twitter.finagle.{Httpx, Service, Filter}
import com.twitter.util.{Await, Future}
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import com.twitter.finagle.Httpx
import com.twitter.util.Await

import io.finch.{Endpoint => _, _}
import io.finch.micro._
import io.finch.request._
import io.finch.route._
import io.finch.response._
import io.finch.route.{Endpoint => _, _}
import io.finch.jackson._

/**
* GET /user/groups -> Seq[Group]
Expand All @@ -15,69 +18,40 @@ import io.finch.response._
*/
object Main extends App {

// enable finch-jackson magic
implicit val objectMapper: ObjectMapper = new ObjectMapper().registerModule(DefaultScalaModule)

// model
case class Group(name: String, ownerId: Int = 0)
case class User(id: Int, groups: Seq[Group])

// custom request
case class AuthReq(http: HttpRequest, userId: Int)
implicit val authReqEv: AuthReq => HttpRequest = _.http
case class UnknownUser(id: Int) extends Exception(s"Unknown user with id=$id")

val currentUser: Micro[Int] = OptionalHeader("X-User-Id") ~> { id =>
id.getOrElse("0").toInt match {
case i @ (1 | 2 | 3) => i.toFuture
case i => UnknownUser(i).toFutureException
}
}

// GET /user/groups -> Seq[Group]
def getUserGroups(userId: Int): Future[Seq[Group]] = Seq(Group("foo"), Group("bar")).toFuture
val getUserGroups: Micro[Seq[Group]] =
currentUser ~> { Seq(Group("foo"), Group("bar")).toFuture }

// POST /groups?name=foo -> Group
def postGroup(name: String, ownerId: Int): Future[Group] = Group(name, ownerId).toFuture
val postGroup: Micro[Group] =
RequiredParam("name") ~ currentUser ~> { Group(_, _).toFuture }

// PUT /user/groups/:group -> User
def putUserGroup(userId: Int, group: String): Future[User] = User(userId, Seq.empty[Group]).toFuture

implicit val encodeGroup: EncodeResponse[Group] =
EncodeResponse[Group]("application/json") { g =>
s"""
|{"name":"$${g.name}","owner":$${g.ownerId}}
""".stripMargin
}

implicit def encodeUser(implicit e: EncodeResponse[Group]): EncodeResponse[User] =
EncodeResponse[User]("application/json") { u =>
s"""
|{"id":$${u.id},"groups":[$${encodeSeq(e(u.groups))}]}
""".stripMargin
}

implicit def encodeSeq[A](implicit encode: EncodeResponse[A]): EncodeResponse[Seq[A]] =
EncodeResponse[Seq[A]]("application/json") { seq =>
seq.map(encode(_)).mkString("[", ",", "]")
}

def service[Req, Rep](f: Req => Future[Rep]): Service[Req, Rep] = Service.mk(f)

val endpoint: Endpoint[AuthReq, HttpResponse] = (
Get / "user" / "groups" /> service[AuthReq, HttpResponse] { req =>
getUserGroups(req.userId).map(Ok(_))
}
) | (
Post / "groups" /> service[AuthReq, HttpResponse] { req =>
RequiredParam("group").embedFlatMap(postGroup(_, req.userId)).map(Ok(_))(req)
}
) | (
Put / "user" / "groups" / string /> { group =>
service[AuthReq, HttpResponse] { req =>
putUserGroup(req.userId, group).map(Ok(_))
}
}
def putUserGroup(group: String): Micro[User] =
currentUser ~> { User(_, Seq.empty[Group]).toFuture }

// an API endpoints
val api: Endpoints = Seq(
Get / "user" / "groups" /> getUserGroups,
Post / "groups" /> postGroup,
Put / "user" / "groups" / string /> putUserGroup
)

val authorize = new Filter[HttpRequest, HttpResponse, AuthReq, HttpResponse] {
def apply(req: HttpRequest, service: Service[AuthReq, HttpResponse]): Future[HttpResponse] = for {
id <- OptionalHeader("X-User-Id")(req)
rep <- service(AuthReq(req, id.getOrElse("0").toInt))
} yield rep
}

val api: Service[HttpRequest, HttpResponse] =
authorize andThen (endpoint: Service[AuthReq, HttpResponse])

Await.ready(Httpx.serve(":8081", api))
}

0 comments on commit cfd7922

Please sign in to comment.