ScalatraのCommandsでバリデーション

ScalatraにはCommandsという機能があって、リクエストパラメータをケースクラスにマッピングしたり、そのケースクラスに対してバリデーションを行ったりすることができます。

…できるのですが、Commandsはバリデーションのための機能というわけではなく、モデルに対するメッセージ(コマンド)とその処理を分離・実装するためのフレームワークで、機能の一つとしてバリデーションがある、という感じです。なのでマッピング・バリデーション用の機能と考えるとちょっと面倒な部分が多いです。
説明だけではわかりにくいので、実際にどんな感じになるのか見てみましょう。
まずはリクエストパラメータをマッピングするケースクラスを普通に作ります。

case class User(name: String)

このケースクラスに対するコマンドの基底クラスを定義します。

abstract class UserCommand[S](implicit mf: Manifest[S]) 
  extends ModelCommand[S] with ParamsOnlyCommand

例としてユーザを追加するコマンドを作成してみます。バリデーション用の定義はこのクラスで行います。ここではnotBlankで必須チェックを行っていますが、もちろんバリデータを自分で追加したり、エラーメッセージをカスタマイズしたりすることもできます。

class AddUserCommand extends UserCommand[User] { 
  val name: Field[String] = asType[String]("name").notBlank
}

このコマンドを受け取って実際の処理を行うハンドラを実装します。コマンドに対してハンドラでパターンマッチして処理を記述するにはアクターへのメッセージに似ていますね。

object UserCommandHandler extends CommandHandler {
    
  protected def handle: Handler  = {
    case c: AddUserCommand => add(User(c.name.value.get))
  }    
    
  private def validate(user: User): ModelValidation[RepositoryCreationForm] = {
    allCatch.withApply(errorFail) {
      // ここにユーザを追加する処理を記述
      ...
      user.successNel
    }
  }

  def errorFail(ex: Throwable) = ValidationError(ex.getMessage, UnknownError).failNel
}

コントローラにはCommandSupportをミックスインした上で以下のように使います。foldして成功か失敗かで分岐するのはPlay2のFormに似ていますね。errorsにはバリデーションエラーの情報が入っているので、これを使って画面にエラーメッセージを表示したりできます。

class UserController extends ScalatraServlet with CommandSupport {
  post("/add"){
    val cmd = command[AddUserCommand]
    UserCommandHandler.execute(cmd).fold(
      // エラーの場合
      errors => halt(400, errors),
      // 成功した場合
      user => redirect("/")
    )
  }
}

冒頭でバリデーションするにしては面倒、と書いた理由がおわかりでしょうか。作らなければいけないものも多いですし、ケースクラスへのマッピングも結局は手動でやっているわけで、正直あまり楽になった気がしないのですが、モデルに対する処理をきちんとコマンドとハンドラで実装することでコントローラが非常にシンプルになるのは良い所…なのかなぁ…?
個人的には、別の、もう少し素直な方法でもいいんじゃないかという気がしますね…。モデルにメソッドとして実装しとけばいいんじゃない?みたいな…。