decadence

個人のメモ帳

finch docs

ここ数日、finchを触っていたのだけど、せっかくなのでドキュメントを眺めたメモを残す。

github.com

眺めたドキュメントは、このタイミングのもの

Router

https://github.com/finagle/finch/blob/627b0229ab5e21c303c56a05e7906b4ef50fe419/docs/route.md

val title: RequestReader[String] = paramOption("title").withDefault("")

val router: Router[String] =
  get(("hello" | "hi") / string ? title) { (name: String, title: String) =>
    s"Hello, $title $name!"
  }

Router[A]Request => Option[A]のように、リクエストからAというパラメータやbodyを取得するものを表す。複数の値を取得したい場合には、Router[L <: HList]となる。HListは、shapelessで定義された Heterogeneou Listだ。シンプルな例に対するエイリアスとして、type Router2[A, B] = Router[A :: B :: HNil]のようなものも用意されてる。

Routerの結合

  • /
    • 2つのRouterを連続するものとして合成 Router[L <: HList]
  • |
    • 同一のRouterとしてOR結合
  • :+:
    • 異なる種類のRouterとしてOR結合
val router: Router[Int :: Int :: HNil] =>
   get("users" / int("userId") / "tickets" / int("ticketId"))

Serviceの提供

Applicationを構成するServiceを提供する際は、以下のようにして結合されたRouterを用いる。

val foo: Router[Response] = get("foo") { Ok("foo") }
val bar: Router[String] = get("bar") { "bar" }

val service: Service[Request, Response] =
  (foo :+: bar).toService

既に知っている場合には説明が不要ではあるが、Serviceはfinagleが提供するものであり、リクエストを受け取ってレスポンスを返すデータ構造である(Service[-Req, +Rep])。

実際にサーバを起動する場合

val server = Httpx.serve(":8081", service)
Await.ready(server)
// Await.ready(server.close())

Requestの扱い

https://github.com/finagle/finch/blob/627b0229ab5e21c303c56a05e7906b4ef50fe419/docs/request.md

RequestReader

サンプルで見たとおり、requestから値を取得する際にはRequestReaderを用いる。以下の例を見て分かる通り、RequestReaderReader Monadの実装の一種であり、単なるRequest => Future[A]という関数のラッパーにすぎない。

val doSomethingWithRequest: RequestReader[Result] =
  for {
    foo <- param("foo")
    baz <- header("baz")
    content <- body
  } yield Result(...)

case classへのマッピング

Applicative Syntax
case class User(name: String, age: Int, city: String)

val user: RequestReader[User] = (
  param("name") ::
  param("age").as[Int].shouldNot(beLessThan(18)) ::
  paramOption("city").withDefault("Novosibirsk")
).as[User]

::複数のリーダーを結合して、RequestReader[L <: HList]にするもの。 Validationなどもここではしてる。

RequestReader[L <: HList]を利用すると、パラメータの不整合などに対して、Seq[Throwable]を受け取る事が出来る。

Monadic Syntax

RequestReaderReaderMonadなので、for式を用いて以下のようにも書ける。

case class User(name: String, age: Int)

val user: RequestReader[User] = for {
  name <- param("name")
  age <- param("age").as[Int]
} yield User(name, age)

Reader一覧

Request Item Reader Type Result Type
Parameter param(name)/paramOption(name) String/Option[String]
Multi-Value Parameters paramsNonEmpty(name)/params(name) Seq[String]/Seq[String]
Header header(name)/headerOption(name) String/Option[String]
Cookie cookie(name)/cookieOption(name) Cookie/Option[Cookie]
Text Body body/bodyOption String/Option[String]
Binary Body binaryBody/binaryBodyOption Array[Byte]/Option[Array[Byte]]
File Upload fileUpload/fileUploadOption FileUpload/Option[FileUpload]

複数のパラメータを受け取る

a=1,2,3&b=4&b=5に対して...

// asTuple method is available on HList-based readers
val reader: RequestReader[(Seq[Int], Seq[Int])] = (
  paramsNonEmpty("a").as[Int] ::
  paramsNonEmpty("b").as[Int]
).asTuple

val (a, b): (Seq[Int], Seq[Int]) = reader(request)
// a = Seq(1, 2, 3)
// b = Seq(4, 5)

リクエストエラーハンドリング

https://github.com/finagle/finch/blob/627b0229ab5e21c303c56a05e7906b4ef50fe419/docs/request.md#user-content-error-handling

エラーハンドリング

val user: Future[Json] = service(...) handle {
  case NotFound(ParamItem(param)) =>
    Json.obj("error" -> "param_not_found", "param" -> param)
  case NotValid(ParamItem(param), rule) =>
    Json.obj("error" -> "validation_failed", "param" -> param, "rule" -> rule)
}

生じるリクエスト: RequestError

// when multiple request items were invalid or missing
case class RequestErrors(errors: Seq[Throwable])
// when a required request item (header, param, cookie, body) was missing
case class NotFound(item: RequestItem)
// when type conversion failed
case class NotParsed(item: RequestItem, targetType: ClassTag[_], cause: Throwable)
// when a validation rule did not pass for a request item
case class NotValid(item: RequestItem, rule: String)

実際は、demoのpetstoreにあるようなこういう書き方をするのが良さそう?

任意の型で受け取る

implicit val dateTimeDecoder: DecodeRequest[DateTime] =
  DecodeRequest(s => Try(new DateTime(s.toLong)))

Jsonを受け取る

ArgonautJacksonJSON4S用のbindingが用意されている。import io.finch.argonaut._をすると、以下のようにして、Jsonをcase classへマッピングしたリクエストを受け取れる。

case class Person(name: String, age: Int)

implicit def PersonDecodeJson: DecodeJson[Person] =
  jdecode2L(Person.apply)("name", "age")

val person: RequestReader[Person] = body.as[Person]

Inline Validation

もし制約にかかった場合には、NotValid(item, rule)が生じる。

val adult2: RequestReader[User] = (
  param("name") ::
  param("age").as[Int].shouldNot("be less than 18") { _ < 18 }
).as[User]

ruleの再利用

val bePositive = ValidationRule[Int]("be positive") { _ > 0 }
def beLessThan(value: Int) = ValidationRule[Int](s"be less than $value") { _ < value }

val child: RequestReader[User] = (
  param("name") ::
  param("age").as[Int].should(bePositive and beLessThan(18))
).as[User]

Response

https://github.com/finagle/finch/blob/627b0229ab5e21c303c56a05e7906b4ef50fe419/docs/response.md

import io.finch.argonaut._
import io.finch.response._

val a = Ok() // an empty response with status 200
val b = NotFound("body") // 'text/plain' response with status 404
val c = Created(Json.obj("id" -> 42)) // 'application/json' response with status 201

EncodeResponse

io.finch.response.Okdef apply[A](body: A)(implicit encode: EncodeResponse[A]): Responseのように、どのようにして与えれれたbodyを処理するかを扱うEncodeResponseを必要とする。 これはPlayでもそうであったように、型に応じてContent-Typeを変えたり出来る。 例えば、import io.finch.argonaut._では、以下のようなimplicitが提供され、任意の型に対するjsonレスポンスを生成出来る。

implicit def encodeArgonaut[A](implicit encode: EncodeJson[A]): EncodeResponse[A] =
  EncodeResponse("application/json")(Utf8(encode.encode(_).nospaces))

文字列を扱う際には、fromStringのような便利なメソッドが用意されてる。

implicit def encodeArgonaut[A](implicit encode: EncodeJson[A]): EncodeResponse[A] =
  EncodeResponse.fromString("application/json")(encode.encode(_).nospaces)

Authentication

https://github.com/finagle/finch/blob/627b0229ab5e21c303c56a05e7906b4ef50fe419/docs/auth.md

OAuth2

finagle-oauth2が利用出来るが、もっとfinchにおいて利用しやすくするためのものは現在開発中

BasicAuth

finch-coreにはbasicAuthというメソッドが用意されてる

import io.finch.route._

val router: Router[String] = Router.value("42")
val authRouter: Router[String] = basicAuth("username", "password")(router)

実装は以下の通り

  def basicAuth[A](user: String, password: String)(r: Router[A]): Router[A] = {
    val userInfo = s"$user:$password"
    val expected = "Basic " + Base64StringEncoder.encode(userInfo.getBytes)

    new Router[A] {
      import Router._
      def apply(input: Input): Option[(Input, () => Future[A])] =
        input.request.authorization.flatMap {
          case `expected` => r(input)
        }

      override def toString: String = s"BasicAuth($r)"
    }
  }

このようにして、Routerを提供する任意のAuthenticationを定義出来る。実際、これはServletでいうFilterをfinchでどのように実装するかの例にもなっている。(まぁ場合に応じてfinagleのFilterとして提供した方が良いものもあると思うが...)

リクエストの振り分け

このようにして、RouterからServiceを作っているらしく、来たリクエストに対する処理を探すのはパターンマッチ一発

  protected def routerToService[R: ToRequest](router: Router[Service[R, Response]]): Service[R, Response] =
    new Service[R, Response] {
       import Router._
       def apply(req: R): Future[Response] = router(Input(implicitly[ToRequest[R]].apply(req))) match {
         case Some((input, result)) if input.isEmpty => result().flatMap(_(req))
         case _ => NotFound().toFuture
       }
    }

adjoinに対する理解が足りない

etc

finagleの基盤の上にのっているため、例えばfinatraで実現出来る事は凡そ実現出来る。一例として、twitter-serverを使う事も出来る。このあたりが大きな資産となって活躍出来るのは非常に便利だと思う。

trait HelloApp {
  val api: Service = ...
}

object Hello extends TwitterServer with HelloApp {
  def main() {
    val server = Httpx.serve(":8081", api.toService)
    onExit {
      server.close()
    }
    Await.ready(server)
  }
}