日頃の行い

個人的な日頃の行いをつらつら書いてます\\\\ ٩( 'ω' )و ////

Scala製ValidationライブラリのAccordを触るぞい

最近Scalaのコードを書いててフレームワークに付随しないタイプのValidationのライブラリなんか無いかなー
と探していたらAccordというものを見つけたので触ってみた備忘録です。

Accord: A sane validation library for Scala

準備

検証用リポジトリはこちらで、使ったライブラリのversionは0.7.2でした。

github.com

installはbuild.sbtのlibDependenciesに追加する感じですね
書いたbuild.sbtはこんな感じでした

val commonSettings = Seq(
  version := "0.1-SNAPSHOT",
  scalaVersion := "2.12.5",
)

lazy val root = (project in file("."))
  .settings(commonSettings)
  .settings(
    name := "scala validation sample",
    libraryDependencies ++= serverDependencies
  )

val serverDependencies = Seq(
  "com.wix" %% "accord-core" % "0.7.2"

)

Accord概要

Accordのvalidationは特定の型に対するValidatorを定義して、
com.wix.accord.validate関数がそのValidatorを利用してvalidationを行い、
最終的にはResult traitを継承した(?)SuccessかFailureを返すみたいな形みたいですね。
validate関数はimplicit parameterでValidatorを渡せるようになっているので、
スコープ内にValidatorを定義しておけば引数から渡さなくても渡せますね。
明示的に渡したい場合は第二引数(?)に渡してあげれば渡せますね。

Ref: http://wix.github.io/accord/api.html#execution

使ってみた

実際に書いてみたコードはこんな感じでした。
APIサーバとかで使うならJSON形式で゚来たRequest Bodyをなんらかのcase classのオブジェクトにマッピングして、
そのオブジェクトをvalidationするみたいな形にするかなーとか思いながら書いてました。

package com.ru.waka

import com.wix.accord.{NullSafeValidator, RuleViolation, Validator, validate}
import com.wix.accord.ViolationBuilder.singleViolationToFailure
import com.wix.accord.dsl._

object Accord {
  implicit val hogeValidator: Validator[Hoge] = validator[Hoge] { h =>
    h.a is notEmpty
    h.b is in(1, 2)
  }

  implicit val FugaValidator: Validator[Fuga] = validator { f =>
    f.as has size > 0
  }

  implicit val PiyoValidator: Validator[Piyo] = validator { p =>
    p.as is notEmpty
    p.bs is myNotEmpty as "その値"
  }

  /**
    * @see http://wix.github.io/accord/dsl.html#combinators
    */
  def main(args: Array[String]): Unit = {
    println(validate(Hoge(a = "", b = 10)))
    // Failure(Set(a must not be empty, b with value "10" got 10, expected one of: [1, 2]))

    println(validate(Fuga(Nil)))
    // Failure(Set(as has size 0, expected more than 0))
    println(validate(Fuga(Seq(1, 2, 3))))
    // Success

    println(validate(Piyo(Nil, Nil)))
    // Failure(Set(as must not be empty, その値 List() は空じゃだめなんじゃ〜))
    println(validate(Piyo(Seq("a"), Seq(1))))
    // Success
  }

  case class Hoge(a: String, b: Int)

  case class Fuga(as: Seq[Int])

  case class Piyo(as: Seq[String], bs: Seq[Int])

  def myNotEmpty[T <: Seq[_]]: Validator[ T ] =
    new NullSafeValidator[ T ](
      test    = x => x.nonEmpty,
      failure = x => RuleViolation(x, s"$x は空じゃだめなんじゃ〜")
    )
}

感想

  • 😁 Validationの成功失敗、失敗はどの項目がどのルールで失敗したかが返るので基本的なValidationの機能は満たしてる
  • 😁 既存のルールが割と多いので時前で定義せずともわりと使える
  • 😁 困ったら自分でも作れるのでまあ便利
  • 😁 レポジトリのStar数もそれなりにあるし、開発も続いてるし息短くはなさそう
  • 😥 エラーメッセージを日本語にしたいと思ったんだけど、既存のものを変える方法が見つけられてなくて困ってる
    • h.a as "変な値" is notEmpty みたいに定義しても 変な値 must not be empty となるのでメッセージ全体は変えられなかった
    • このあたりはもう少し深掘りしながらissueとかも見ていこうかなと思った