Skip to content

Instantly share code, notes, and snippets.

@gakuzzzz
Last active April 6, 2023 01:47
Show Gist options
  • Save gakuzzzz/860ab5a921c852f90ebe to your computer and use it in GitHub Desktop.
Save gakuzzzz/860ab5a921c852f90ebe to your computer and use it in GitHub Desktop.
ActionFunction の紹介 - Play framework Advent Calendar 2014 7日目

ActionFunction の紹介

この記事は 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 が導入されました。

ActionBuilder とは

さて、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 になります。

独自 Request 型

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 の役割となります。

ActionFunction

ActionBuilder について紹介したところで、ActionFunction について話していきたいと思います。

ActionFunction とそのサブトレイトのクラス階層は下記の図の様になっています。

ActionFunction class hierarchy

ActionFunction の具象インターフェイスとして ActionBuilderActionRefiner があり、 更に ActionRefiner の具象インターフェイスとして ActionTransformerActionFilter が存在しています。

それぞれのシグネチャを見てみましょう。

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 は通常の関数合成と同じように、他の ActionFunctionandThen, compose を使って合成する事ができます。

そして ActionBuilder はその合成メソッドを override しており、ActionBuilderActionFunction を合成すると、新しい ActionBuilder が作られるようになっています。

ActionRefiner

基本的には ActionFunction を定義すればいいのですが、ActionFunctioninvokeBlock を定義する必要があるために、今関心がある処理以外に、アクションを生成するためのブロックのハンドリングなどまで意識しなければなりません。

そこで、関心のある処理だけ書けば済むようにサブトレイトが提供されています。

そのうちのひとつが 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

ActionTransformerActionRefiner を更に特化させたトレイトで、リクエスト型の変換だけを行います。

認証などは失敗する可能性があるため、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))
    }
  }
}

ActionFilter

ActionTransformerActionRefiner の変換に特化したトレイトだとすれば、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 になるのが少し面白いですね。

ActionFunction の問題点

こんな風にいろんな処理を合成して ActionBuilder を生成する事ができるようになったのですが、実は問題点もあります。

たとえば先ほど作成した AuthRefinerTxTransformer を合成してみましょう。

val MyCoolAction = Action andThen AuthRefiner andThen TxTransformer

しかしこのコードはコンパイルができません。TxTransformerActionBuilder[Request] とは合成できますが、 ActionBuilder[UserRequest] とは型が合わないため andThen に渡せないのです。

このように、それぞれお互いの存在を知らずに作成された ActionFunction を合成しようとすると、一筋縄ではいかなくなります。

もし、アクションの共通処理をお互いに依存させずに定義し、自由に合成したい場合は、 ActionFunction ではなく拙作の Action-Zipper の利用も検討してみてください。

play2-auth での利用

Play2 に認証認可の機構を提供するライブラリの一つに play2-auth があります。

play2-auth の標準実装は Stackable-Controller を使用していますが、0.13.0 から ActionFunctionも同時に提供するようになりました。

こちらではある程度他の ActionFunction と合成が可能なように工夫がされています。

興味がありましたらそちらもご参照ください。

まとめ

というわけで、Play2.3 より追加された ActionFunction について紹介してみました。

もしアクションに共通処理をしたいが、BodyParser を指定した場合とそうじゃない場合がある、sync と async と両方使い分けたい、などがあれば ActionFunction の利用が選択肢になるかもしれません。

使える道具のうちの一つになれば幸いです。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment