はじめに
scalaのplayframeworkでValidatedNecを利用してバリデーションを実装しました。
今回はJsonで受け取ったTodoのタイトル、内容をバリデーションして、保存する流れで利用します。
使い所を考えるのが難しいので(Todoアプリでは)、とりあえずAPIで受け取った値をバリデーションします。
(※playframeworkにはデフォルトでコンビネータが用意されており、コンビネータでバリデーションできます。
最後にデフォルトで用意されているコンビネータと比較していきたいと思います。)
JsValue
ValidatedNecは複数のバリデーションエラーメッセージをまとめてチェックしてくれます!
一つのエラーが発生しても他のバリデーションを止めずに実行し、すべてのエラーメッセージを溜め込むことができます!
今の実装だと、条件増えてくると大変そうではありますね。
case class JsValuePostTodo(
title: String,
content: String
){}
object JsValuePostTodo {
implicit val todoReads:Reads[JsValuePostTodo] = Json.reads[JsValuePostTodo]
//バリデーションエラーメッセージ用の型
type ValidationError = String
type ValidationResult[A] = ValidatedNec[ValidationError, A]
// タイトルのバリデーション
def validateTitle(title: String): ValidationResult[String] = {
if (title.nonEmpty) title.validNec
else "Title cannot be empty".invalidNec
}
// コンテンツのバリデーション
def validateContent(content: String): ValidationResult[String] = {
(
if (content.nonEmpty) content.validNec else "Content cannot be empty".invalidNec,
if (content.length >= 2) content.validNec else "content must be at least 2 characters".invalidNec
).mapN((_, _) => content)
}
// 全体のバリデーション
def validate(todo: JsValuePostTodo): ValidationResult[JsValuePostTodo] = {
(
validateTitle(todo.title),
validateContent(todo.content)
).mapN(JsValuePostTodo.apply)
}
}
バリデーションを順次実行(1つ目のバリデーションが成功した場合のみ次のバリデーション...)↓
// 全体のバリデーション
def validate(todo: JsValuePostTodo): ValidationResult[JsValuePostTodo] = {
validateTitle(todo.title) *> validateContent(todo.content).map(_ => todo)
}
Controller
愚直にエラーハンドリングを書くと、コードが冗長になるため、EitherTを使って行っています。(失敗した場合はBadRequestを返し、成功すれば次の処理に進みます。)
def post() = Action(parse.json) async { request =>
(for {
// リクエスト内のJsonデータを取ってきてJsValueに変換
jsValuePostTodo <- EitherT.fromEither[Future](request.body.validate[JsValuePostTodo].asEither.leftMap(_ => BadRequest))
// バリデーション
_ <- EitherT.fromEither[Future](JsValuePostTodo.validate(jsValuePostTodo).toEither.leftMap( errors => BadRequest(errors.toNonEmptyList.toList.mkString(", "))))
// Todo保存
_ <- EitherT.liftF[Future, Result, Option[Todo.Id]](todoRepository.add(jsValuePostTodo.build))
} yield NoContent).merge
}
request.body.validate
とJsValuePostTodo.validate
が並んでいて少し読みにくいですね。
2つ目のJsValuePostTodo.validate
で、バリデーションが実行されます。
用意されているコンビネータとバリデーション
デフォルトで用意されているコンビネータも非常に優秀です。
implicit val placeReads: Reads[JsValuePostTodo] = (
(JsPath \ "title").read[String](minLength[String](2)) and
(JsPath \ "content").read[String](minLength[String](2) keepAnd maxLength[String](10) keepAnd pattern("""^[a-zA-Z]+$""".r, "error.alphanumeric"))
)(JsValuePostTodo.apply _)
ほとんどの場合こっちでバリデーションすると思います。
ただし、複数のフィールド間の相関関係をバリデートする場合、コンビネータでは対応が難しくなるため、そのような場合にはValidatedNecが適していると思います。
まとめ
エラーメッセージを溜め込む処理がこんなに簡単に!
ドキュメントを見ていると難しそうに見えるかもしれませんが(目が慣れない)、実際に使ってみるととてもシンプルでした😄
参考リンク
https://typelevel.org/cats/datatypes/validated.html
https://typelevel.org/cats/datatypes/eithert.html