finch docs
ここ数日、finchを触っていたのだけど、せっかくなのでドキュメントを眺めたメモを残す。
眺めたドキュメントは、このタイミングのもの。
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]
- 2つのRouterを連続するものとして合成
|
- 同一の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
を用いる。以下の例を見て分かる通り、RequestReader
はReader 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
RequestReader
はReaderMonad
なので、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)
リクエストエラーハンドリング
エラーハンドリング
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を受け取る
Argonaut、Jackson、JSON4S用の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.Ok
はdef 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) } }