テストステ論

高テス協会会長が, テストステロンに関する情報をお届けします.

finch: 0.9.4からはヘッダやパラメタでパスマッチング出来るようになる

私はfinagle/finch上に作られたFS-S3互換レイヤを現在, 開発中です. 楽しみにしていてください.

GitHub - akiradeveloper/akashic-storage: Akashic records implementation in Scala

0.9.4からfinchに入る素晴らしい機能を説明します.

現在, finchの最新版は0.9.3なのですが, このバージョンでは, パスマッチングに制限があります.

S3では, クエリパラメタを使ったAPIの分岐があります. これがfinchの枠組みの中では出来ません.

finchの典型的な使い方を説明します. finchにはEndpointとRequestReaderという抽象がありますが, Endpointというのはリクエストを入力として何か処理をするという抽象であり, RequestReaderは, クエリから情報を剥ぐための抽象(Reader Monad)です.

典型的には, finchのコードは以下の形を得ます.

val a: Endpoint[A] = method(endpoint (/ endpoint)* (? reader)*) { (arguments) => fn(arguments) }
val b: Endpoint[B] = ...
val c: Endpoint[A :: B] = a :+: b
val service = c.toService
Http.serve(service)

endpointとreaderで得たargumentsを関数に適用する. :+:演算子によってEndpointを合成する.

この2つの抽象のEndpointとRequestReaderは両方ともリクエストを参照して何か値を剥ぐ点が違うのです.

  • Endpointはパスを消費する
  • RequestReaderは値を取得するだけ

という違いがあり, Endpointのみがパスマッチングに影響します. EndpointはsprayでいうRouteに似ているのですが(もともとそういう名前でした), RouteはEndpointとRequestReaderを統合した概念であるという点が違います. 今, Endpointにはヘッダやクエリパラメタを見る実装がないため, これらによってパスマッチングを行うことは不可能です.

したがって, S3の

  • PUT /bucketName/keyName
  • PUT /bucketName/keyName?uploadId=xxx&partNumber=yyy

のような2つを見分けることは不可能です.

今回, Gitterで困ったという話をしたら,

question: if the endpoint is a :+: b and the request first raises exception in matching against a (e.g. RequestReader param("name") failed), does the routing result in BadRequest? or steps forward to the b? My, or natural expectation is the latter. But my local trial and error says it's former now I suppose finch can't use params for patch matching

開発者の人が

@akiradeveloper RequestReaders don't affect path matching, only Endpoints do. Finch doesn’s support dispatching/routing based on param/header/cookie value by default, but it’s easy to implement it like this finagle/finch#397. Wath’s worh, that ticket will probably be fixed in 0.9.4 so you could do that out of the box.

と返事してくれました.

今のfinchでは, あるパラメタが存在することを示すためには, 以下のようなワークアラウンドを書く必要がありますが

  case class ParamExists(name: String) extends Endpoint[HNil] {
    private[this] val hnilFutureOutput: Eval[Future[Output[HNil]]] = Eval.now(Future.value(Output.payload(HNil)))
    def apply(input: Input): Endpoint.Result[HNil] =
      if (input.request.containsParam(name)) Some((input, hnilFutureOutput))
      else None
  }
  def paramExists(name: String): Endpoint[HNil] = ParamExists(name)

0.9.4では, RequestReaderがEndpointに統合されるような変更が行われるはずです.

@akiradeveloper I have a local brahch that unifies request readers and endpoints as well as solves this kind of problem. TL;DR it’s possible to use any part of the request: path, param, header to either dispatch/match (endpoint behaviour) a request or read/fail (request reader behaviour). Hopefully, I will get this merged in 0.9.4 so you could get rid of paramExists.

akashic-storageもfinchと一緒に磨いていければよいと思います.