0
0

CatsのValidatedNecを利用してバリデーションを実装してみる

Last updated at Posted at 2024-09-14

はじめに

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.isEmpty) 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.validateJsValuePostTodo.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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0