Unfilteredでモナド構文が使えるUnfiltered-Directives 2014年02月23日

Play2以降、Playばかり気にしていましたが、Unfilteredでモナド構文が使えるUnfiltered-Directivesというのが一年弱くらい前に入っていたようです。

unfiltered/directives at master · unfiltered/unfiltered

Unfiltered — Directives and Validation

もう公式のドキュメントにもしっかり書いてあって、ここであらためて書くこともないんですが、あまり知られてないようなので紹介しておきます。

UnfilteredはパターンマッチでHTTPのメソッドやパスを指定できるわけですが、書きやすい代わりにパターンマッチに失敗すると何でも404 Not Foundを返してしまうという問題があります。 公式にあるサンプルですが、

def intent = {
  case req @ POST(Path("/example")) & Accepts.Json(RequestContentType("application/json")) =>
    Ok ~> JsonContent ~> ResponseBytes(Body.bytes(req))
}

こういうふうに全部パターンマッチで書いた場合、HTTPのメソッドが間違っていた場合でも、Content-Typeが間違っていた場合でも全部404になってしまいます。 そこでUnfiltered-Directivesの出番になります。Unfiltered-Directivesを使うとモナド構文を使って以下のように書くのですが、

import unfiltered.directives._, Directives._

def contentType(tpe:String) =
  when{ case RequestContentType(`tpe`) => } orElse UnsupportedMediaType

def intent = Directive.Intent {
  case Path("/example") =>
    for {
      _ <- POST
      _ <- Accepts.Json
      _ <- contentType("application/json")
      req <- request[Any]
    } yield Ok ~> JsonContent ~> ResponseBytes(Body.bytes(req))
}

チェックに失敗した場合、それぞれで405 Method Not Allowed、406 Not Acceptable、415 Unsupported Media Typeを返すようになります。 デフォルトである程度エラーコードを返すように定義されていますが、contentTypeメソッドを見ればわかるように自分で定義するのも簡単にできます。

また、このUnfiltered-Directivesを使って、パラメータのチェックもできます。たとえば、

def intent = Directive.Intent {
  case Path("/") =>
    for {
      in <- data.as.Int.fail { (k, v) =>
        BadRequest ~> ResponseString(s"'$v' is not a valid int for $k")
      } named "in"
      req <- data.Requiring[String].fail{ name =>
        BadRequest ~> ResponseString(name + " is missing")
      } named "req"
    } yield ResponseString(s"in: $in, req: $req")
}

こう記述することにより、inというパラメータがIntかどうかチェックされ、reqというパラメータが必須パラメータになります。 他にも色々入力の加工やチェックする方法があるみたいです。詳しくは公式のドキュメントを見てみてください。

モナド構文は簡単にComposableな仕組みが作れるので、活用するライブラリが増えてほしいですね。