Skip to content

Instantly share code, notes, and snippets.

@vkostyukov
Last active August 29, 2015 14:16
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vkostyukov/e0e952c28b87563b2383 to your computer and use it in GitHub Desktop.
Save vkostyukov/e0e952c28b87563b2383 to your computer and use it in GitHub Desktop.
Finch in Action (Micro-Finch)

History

There is a bunch of tickets/PRs related to the same underlying problem. It has started with two tickets:

  • Issue 190 - composing routers with filters and services
  • Issue 172 - mixing futures and services in the same endpoint/router

Then Jens has created a PR 184 that indicated a problem of domain types in Finch. So, I created an issue 204 to track the progress on this direction. More importantly, I've posted a first version of possible solution for the "Finch in Action" problem in PR 206. This document mostly describes an approach called "Micro Finch", that in my opinion solves the original problem.

The "Finch in Action" Problem

For now, a typical Finch use-case looks like a bunch of Finagle services Service[HttpRequest, HttpResponse] glued together with either Router or Endpoint. This is a totally good and reasonable approach but it doesn't really use the whole power of Finch basic blocks and more importantly, hides the actual domain types user is working with. That said, it ruins the type-safety. Good news here is that Finch already has a good potential to expose and use types that matter rather than hide them besides boilerplate HTTP types: HttpRequest, HttpResponse.

The idea is to allow users to write their (micro)-services working with custom domain types instead. If you want to greet a user by given name you're thinking about service String => String rather than HttpRequest => HttpResponse. The "Finch in Action" problem belongs to:

  1. Working with types that matter, i.e., getting user's groups is User => Seq[Group] but not HttpRequest => HttpResponse
  2. Not dealing with raw HTTP types directly

Solution

Good news! Finch already has everything is needed to solve the problem. On one hand, a RequestReader hides the raw HttpRequest type and substitutes it with domain type A (i.e., RequestReader[A]). On the other hand, a ResponseBuilder hides the raw HttpResponse type and substitutes it with domain type B if there is an implicit view available B => HttpResponse.

In order to approach this, we have to define an abstraction that represents a user-defined (micro)-service, working with domain types. It takes a request and returns type A: Micro[A]. In fact, there is already abstraction in Finch that does the same thing: RequestReader[A]. For example, RequiredParam("a") is a (micro)-service HttpRequest => String.

Thus, the solution for the "Finch in Action" problem is a "Micro Finch" that, at hight level, is attempt to replace Fiangle Service with Micro[A] (RequestReader[A]), which is an insanely composable version of Service[HttpRequest, A].

Micro-Finch (Your REST API as a Monad)

There is nothing new here rather than a type alias type Micro[A] = RequestReader and a couple of implicit conversions that allows us to threat Router[Micro[HttpResponse]] as a Service[HttpRequest, HttpResponse].

A full-featured example of usage micros is available here. Although, compositors ~> (or map) and ~~> (or embedFlatMap) should be explained in details. When dealing with composed request readers, i.e., RequestReader[A ~ B] it's not that easy to map it to function since it requires pattern matching the type firstly:

def sum(i: Int, j: Int) = i + j
val r: RequestReader[Int] = 
  RequiredParam("i").as[Int] ~ RequiredParam("j").as[Int] map {
    case i ~ j => sum(i, j)
  }

Both ~> and ~~> compositors allow to threat underlying type A ~ B .. ~ Z as (A, B, ..., Z). So, the example might be rewrite in a sane way:

def sum(i: Int, j: Int) = i + j
val r: RequestReader[Int] = 
  RequiredParam("i").as[Int] ~ RequiredParam("j").as[Int] ~> sum

Using the ~~> we can make long calls to the underlying async API (considering that Micro is RequiestReader):

def fetchUserFromDb(id: Int): Future[User] = ???
def getUser: Micro[User] = RequiredParam("id").as[Int] ~~> fetchUserFromDb

That said, we can rethink the meaning of Endpoint. Instead of two meaningless types it should carry just one type:

type Endpoint[A] = Router[Micro[A]]

So, keeping in mind that there is an implicit conversion Endpoint[A] => Service[HttpRequest, HttpResponse] available (if there also an implicit view A => HttpResponse around). We can write a "Hello, World!" service that returns plain/text response to every request like this:

Httpx.serve(":8081", ** /> Micro.value("Hello, World!"))

An echo service might look as follows:

def echo(what: String): Micro[String] = Micro.value(what)
Httpx.serve(":8081", Get / "echo" /> echo)

Let's think about more complex example to see the whole picture: given interval from ... to, return all the users from database:

def fetchUsers(from: Int, to: Int): Future[Seq[User]] = ???

case class Interval(from: Int, to: Int)
val interval: Micro[Interval] = RequiredParam("from").as[Int] ~ RequiredParam("to").as[Int] ~> Interval
val getUsers: Micro[Seq[User]] = interval ~~> {
  case Interval(from, to) => fetchUsers(from, to)
}

Httpx.serve(":8081", Get / "users" /> getUsers)

Conclusion

This is a big step for the Finch project. I'm totally excited about where we're now. We did a fantastic job on making the RequestReader (our cornerstone abstraction) composable as hell. The next reasonable step is to treat it not as just a reader but a complete (micro)-service.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment