この記事は Play framework Advent Calendar 2014 の7日目です。
昨日は @dorako321 さんの Play framework Advent Calendar 2014 6日目 位置情報を使ってみよう でした。
明日は @nazoking さんの play2.3 の sbt-web を使わず node で代替システムを作るための資料 です。
さて、そんなこんなで公式ドキュメントではまだ語られていない ActionFunction とそのサブトレイトについて紹介したいと思います。 (公式ドキュメントにも記載ありました https://www.playframework.com/documentation/2.3.x/ScalaActionsComposition#Different-request-types )
Play2.2 から ActionBuilder が導入され、 Play2.3 から ActionBuilder をさらに抽象化した ActionFunction が導入されました。
さて、ActionFunction の話をする前に、ActionBuilder について解説したいと思います。
そもそも ActionBuilder とは何なのでしょうか。
実は ActionBuilder は Play Scala を使っている方であれば、割と皆さん普通につかっています。
以下のコードを見て見ましょう。
object MyCoolController extends Controller {
def index = Action { req =>
Ok("Hello Play!")
}
}
何の変哲もないControllerのコードですが、実はこのコード内にも ActionBuilder が使われています。
index
メソッド は Action
オブジェクトの apply
メソッドに Request[AnyContent] => Result
を渡して Action[AnyContent]
インスタンスを返しています。
この Action
オブジェクトが実は ActionBuilder[Request]
なのです。
注意する点としては、Action
クラスと Action
オブジェクトは別物という事です。 Action
オブジェクトが、Action
クラスのインスタンスを生成する役割を担っているのです。
この、「Action
インスタンスを生成する」という役割を抽象化したものが ActionBuilder になります。
「Action
インスタンスを生成する」という役割を抽象化した、といいましたが、具体的に何が抽象化されたのでしょうか。
Action
オブジェクトのシグネチャをよく見てみましょう。
object Action extends ActionBuilder[Request] {
ActionBuilder が型引数を持っていることがわかるでしょうか。
この型引数に Request
を指定しています。そして Action
オブジェクト は Request[A]
を受け取り Result
などを返すような処理を使って Action
インスタンスを生成します。
つまり、ActionBuilder は、リクエストの型を抽象化しているのでした。
したがって、たとえば認証を行ってユーザ情報を保持したリクエストを使いたい、といった場合、
case class UserRequest[A](user: User) extends Request[A]
のようなクラスを作るとすると
object UserAction extends ActionBuilder[UserRequest] {
といったオブジェクトを作れば、
def index = UserAction { req =>
Ok(req.user.name)
}
と言うような、アクションを作ることが可能になります。
これが ActionBuilder の役割となります。
ActionBuilder について紹介したところで、ActionFunction について話していきたいと思います。
ActionFunction とそのサブトレイトのクラス階層は下記の図の様になっています。
ActionFunction の具象インターフェイスとして ActionBuilder と ActionRefiner があり、 更に ActionRefiner の具象インターフェイスとして ActionTransformer と ActionFilter が存在しています。
それぞれのシグネチャを見てみましょう。
trait ActionFunction[-R[_], +P[_]] {
def invokeBlock[A](request: R[A], block: P[A] => Future[Result]): Future[Result]
def andThen[Q[_]](other: ActionFunction[P, Q]): ActionFunction[R, Q]
trait ActionBuilder[+R[_]] extends ActionFunction[Request, R] {
override def andThen[Q[_]](other: ActionFunction[R, Q]): ActionBuilder[Q]
trait ActionRefiner[-R[_], +P[_]] extends ActionFunction[R, P] {
protected def refine[A](request: R[A]): Future[Either[Result, P[A]]]
trait ActionTransformer[-R[_], +P[_]] extends ActionRefiner[R, P] {
protected def transform[A](request: R[A]): Future[P[A]]
trait ActionFilter[R[_]] extends ActionRefiner[R, R] {
protected def filter[A](request: R[A]): Future[Option[Result]]
実際に Action
インスタンスを生成できるのは ActionBuilder です。しかし、ActionBuilder を他の ActionBuilder と合成しようと思うと自由にはできません。
そこで、ActionBuilder の核となる処理を抽象化した、ActionFunction が定義されました。
この ActionFunction は通常の関数合成と同じように、他の ActionFunction と andThen
, compose
を使って合成する事ができます。
そして ActionBuilder はその合成メソッドを override しており、ActionBuilder とActionFunction を合成すると、新しい ActionBuilder が作られるようになっています。
基本的には ActionFunction を定義すればいいのですが、ActionFunction は invokeBlock
を定義する必要があるために、今関心がある処理以外に、アクションを生成するためのブロックのハンドリングなどまで意識しなければなりません。
そこで、関心のある処理だけ書けば済むようにサブトレイトが提供されています。
そのうちのひとつが ActionRefiner です。
ActionRefiner はリクエスト型の変換と絞込みの処理の責務を持ちます。
たとえば認証処理を考えてみましょう。標準の Request[A]
から認証情報を抽出し、UserRequest[A]
に変換して後続処理を行うか、認証に失敗した場合にログインページに飛ばす、といった形になると思います。こういった ActionFunction を作りたい際には、ActionRefiner にするとよい感じです。
object AuthRefiner extends ActionRefiner[Request, UserRequest] {
protected def refine[A](request: Request[A]): Future[Either[Result, UserRequest[A]]] = Future.successful {
val Option[User] user = ... // request から User を取得
user.map { user =>
Right(UserRequest[A](user))
}.getOrElse {
Left(Redirect(routes.Login.form))
}
}
}
ActionTransformer は ActionRefiner を更に特化させたトレイトで、リクエスト型の変換だけを行います。
認証などは失敗する可能性があるため、ActionTransformer では表現できませんでしたが、必ず変換できることがわかっているのであれば、ActionTransformer が便利です。
たとえば DB のトランザクションを開始して、トランザクション情報を持ったリクエストを作りたい場合などを考えて見ましょう。(もちろんDBの接続がうまくいかないなどのケースはありますが、そのハンドリングは別で行う方がよいでしょう)
case class TxRequest[A](session: DBSession) extends Request[A]
object TxTransformer extends ActionTransformer[Request, TxRequest] {
protected def transform[A](request: Request[A]): Future[TxRequest[A]] = {
import calikejdbc.TxBoundary.Future._
DB localTx { session =>
Future.successful(TxRequest[A](session))
}
}
}
ActionTransformer が ActionRefiner の変換に特化したトレイトだとすれば、ActionFilter はフィルタリングに特化したトレイトになっています。
特定の条件だけ後続処理を行い、条件に一致してない場合に Result
を返す場合は ActionFilter が便利です。
たとえば、権限チェックの処理を考えて見ましょう。UserRequest[A]
を受け取って、権限エラーであれば Unauthorized を返すケースです。
object AuthorizationFilter extends ActionFilter[UserRequest] {
protected def filter[A](request: UserRequest[A]): Future[Option[Result]] = {
if (request.user.role != Admin) Some(Unauthorized) else None
}
}
フィルターで取り除きたい時に Some
になるのが少し面白いですね。
こんな風にいろんな処理を合成して ActionBuilder を生成する事ができるようになったのですが、実は問題点もあります。
たとえば先ほど作成した AuthRefiner
と TxTransformer
を合成してみましょう。
val MyCoolAction = Action andThen AuthRefiner andThen TxTransformer
しかしこのコードはコンパイルができません。TxTransformer
は ActionBuilder[Request]
とは合成できますが、 ActionBuilder[UserRequest]
とは型が合わないため andThen
に渡せないのです。
このように、それぞれお互いの存在を知らずに作成された ActionFunction を合成しようとすると、一筋縄ではいかなくなります。
もし、アクションの共通処理をお互いに依存させずに定義し、自由に合成したい場合は、 ActionFunction ではなく拙作の Action-Zipper の利用も検討してみてください。
Play2 に認証認可の機構を提供するライブラリの一つに play2-auth があります。
play2-auth の標準実装は Stackable-Controller を使用していますが、0.13.0 から ActionFunctionも同時に提供するようになりました。
こちらではある程度他の ActionFunction と合成が可能なように工夫がされています。
興味がありましたらそちらもご参照ください。
というわけで、Play2.3 より追加された ActionFunction について紹介してみました。
もしアクションに共通処理をしたいが、BodyParser
を指定した場合とそうじゃない場合がある、sync と async と両方使い分けたい、などがあれば ActionFunction の利用が選択肢になるかもしれません。
使える道具のうちの一つになれば幸いです。