finagel-http path の紹介

これは Finagle Advent Calendar 2012 (だいたい俺) の16日目です。

Finagle には HTTP通信用の http サブプロジェクトがありますが、この中に path というパッケージと Path によって Routing を行う RoutingService というものが提供されています。今日はこれの紹介をしようと思います。

Routing

RoutingService は リクエストパスによって実行する Service を切り替える Service です。

具体的に使い方を見てみましょう。

  def main(args: Array[String]) {
    val service = RoutingService.byPathObject {
      case Root                    => IndexService
      case Root / "users" / userId => UserDetailService(userId)
    }

    ServerBuilder()
      .codec(RichHttp[Request](Http()))
      .bindTo(new InetSocketAddress(8080))
      .name("Finagle Advent Calendar 2012")
      .build(service)
  }

こんな感じで `RoutingService.byPathObject` に `PartialFunction[Path, Service[REQ, RES]]` 渡すことで、リクエストで渡された path に応じて呼び出す Service を切り替えてくれます。

`PartialFunction` に match する case が存在しなかった場合は、`NotFoundService` が使用されます。

/ Extractor

path の階層区切りには / を使うことができます。

    val service = RoutingService.byPathObject {
      case Root / "users"                        => UsersListService
      case Root / "users" / userId               => UserDetailService(userId)
      case Root / "users" / userId / "followers" => FollowersService(userId)
    }

基本的に文字列と / で階層を表現します。間の文字列もパターンなので、例の `userId` のように変数パターンを使うことで渡されてきた値を変数として使用することができます。

:? :& extractor

クエリパラメータに対してマッチさせたい場合には、 :? や :& を使うことができます。

事前に `ParamMatcher` を利用して、クエリパラメータ名に一致する Extractor を用意します。たとえば `/details?userId=xxx&targetid=yyy` のような URL にマッチさせたい場合、以下の様になります。

    object UserId extends ParamMatcher("userId")
    object TargetId extends ParamMatcher("targetId")

    val service = RoutingService.byPathObject {
      case Root / "details" :? (UserId(u) :& TargetId(t))  => ...
      case ...
    }

ちなみに Extract する値の型を Int や Long や Dobule などを使いたい場合、`IntParamMatcher``LongParamMatcher``DoubleParamMatcher` などを使うことで特定の型で取得することができます。

また、ポイントとしてクエリパラメータの順序はきちんと無視して解釈してくれます。`Root / "details" :? (UserId(u) :& TargetId(t))` という定義であれば `/details?targetid=yyy&userId=xxx` という値が送られてきても適切にマッチします。

Integer, Long Extractor

またパラメータだけでなく、パスの値を Int などで取得したい場合は、 Integer Extractor などを使うことができます。

    val service = RoutingService.byPathObject {
      case Root / "users"                        => UsersListService
      case Root / "users" / Integer(userId)      => UserDetailService(userId)
    }

~ Extractor

ファイルの拡張子によって分岐をしたい場合は ~ を使うことができます。

    val service = RoutingService.byPathObject {
      case Root / "users" ~ "json"               => UsersListService.map(_.asJson)
      case Root / "users" ~ "xml"                => UsersListService.map(_.asXml)
    }

-> Extractor

HTTP メソッドによって分岐をしたい場合 -> を使うことができます。

    import Method._

    val service = RoutingService.byPathObject {
      case Get  -> Root / "users"  => UsersListService
      case Post -> Root / "users"  => CreateuserService
    }

まとめ

というわけで finagle-http の path パッケージの紹介でした。公式でもドキュメントがなく、現状ではテスト を見るしかないというマイナー機能ですが、 Finagle で HTTP サーバを立てる場合には使えるかもしれません。