「Validation (14) - オブジェクトの生成」でValidationを使って検証とオブジェクトの生成を行いました。
検証とオブジェクトの生成に対して検証対象に多重度を加えることにします。
多重度は「1」、「0または1」、「1以上」、「0以上」の4種類を扱うのが現実解であることを説明しました。まず、この4つの多重度の実現方法について考えていきます。
課題
以下のPersonオブジェクトを生成する、makePerson関数の実現方法について考えます。
case class Person(
name: String,
age: Int,
address: String)
def makePerson(data: Map[String, String]): ValidationNEL[Throwable, Person] = {
sys.error("これから実装")
}
以下のようなMapデータの内容を検証し、正しいデータである場合にPersonを生成します。
val data1 = Map("name" -> "Taro",
"age" -> "30",
"address" -> "Kanagawa Yokohama")
この課題ではPersonオブジェクトの3つの属性name, age, addressを多重度1として扱います。
前回までは、検証関数に必ず値が渡されてきましたが、今回はMap内に値が格納されていないケースがあります。多重度1の場合は必ず値が必要になるので、Map内に値が格納されていないケースは検証エラーとする必要があります。
前提
ここまでに作ってきた部品(本記事後ろにある「これまでの部品」参照)を使用します。この部品の上に多重度1を扱うための部品を構築していきます。
多重度のハンドリング
多重度のハンドリングを行うための部品として以下の関数を作成しました。
val NoValueFailure = new IllegalArgumentException("No value").failNel
val EmptyValueFailure = new IllegalArgumentException("Empty value").failNel
val SequenceValueFailure = new IllegalArgumentException("Sequence value").failNel
def optionSeqOne[A, B](v: Option[Seq[A]], f: A => ValidationNEL[Throwable, B]): ValidationNEL[Throwable, B] = {
v match {
case Some(Nil) => EmptyValueFailure
case Some(x :: Nil) => f(x)
case Some(_ :: _) => SequenceValueFailure
case None => NoValueFailure
}
}
def validateOne[T](f: T => Boolean, message: String, v: Option[Seq[T]]): ValidationNEL[Throwable, T] = {
optionSeqOne(v, validate(f, message, (_: T)))
}
def validatesOne[A, B](
f: A => ValidationNEL[Throwable, B],
g: Seq[B => ValidationNEL[Throwable, B]],
v: Option[Seq[A]]
): ValidationNEL[Throwable, B] = {
optionSeqOne(v, validates(f, g, (_: A)))
}
def validatesDefOne[A, B](
d: (A => ValidationNEL[Throwable, B],
Seq[B => ValidationNEL[Throwable, B]]),
v: Option[Seq[A]]
): ValidationNEL[Throwable, B] = {
validatesOne(d._1, d._2, v)
}
ポイントになるのはoptionSeqOne関数で、ここで多重度1に対する検証を行っています。多重度以外の検証項目については、引数として渡されてきた関数(型はA => ValidationNEL[Throwable, B])を使用します。
optionSeqOne関数では、入力データをStringではなく、型パラメタAで受けています。こうすることで、入力データがString以外の場合も使用できるようになり、より汎用的に活用できる可能性が高まります。
データの正規化
多重度を扱うための検討事項として、検証処理が扱うデータの正規化があります。データ処理を考える場合、各データを統一的なフォーマットにしておくことで、処理を効率的に実装することができます。
4種類の多重度「1」、「0または1」、「1以上」、「0以上」をすべて表現できるフォーマットということで、今回はOption[Seq[String]]を標準フォーマットにします。入力データを一旦この形に正規化することで、後続のデータ処理がやりやすくなります。
Mapに格納されているデータを、この形で取り出す関数fetchは以下のようになります。
def fetch(data: Map[String, String], key: String): Option[Seq[String]] = {
data.get(key).map {
case null => Nil
case "" => Nil
case x => x.split(";").toList
}
}
Mapにデータが格納されていない場合はNone、データが格納されていた場合は文字列を「;」を区切り記号にしてデータ列をSome[Seq[String]]として取り出します。
検証関数
ここまでで作ってきた部品を使って以下の多重度1用の検証関数を作成しました。
def validateName(name: Option[Seq[String]]): ValidationNEL[Throwable, String] = {
validatesDefOne(nameDef, name)
}
def validateAge(age: Option[Seq[String]]): ValidationNEL[Throwable, Int] = {
validatesDefOne(ageDef, age)
}
def validateAddress(address: Option[Seq[String]]): ValidationNEL[Throwable, String] = {
validatesDefOne(addressDef, address)
}
実際に動かしてみましょう。
使用するデータを再掲します。
val data1 = Map("name" -> "Taro",
"age" -> "30",
"address" -> "Kanagawa Yokohama")
validateName, validateAge, validateAddress関数は以下のように正しい値を入力にして、Successを返しました。
scala> validateName(fetch(data1, "name"))
res423: scalaz.Scalaz.ValidationNEL[Throwable,String] = Success(Taro)
scala> validateAge(fetch(data1, "age"))
res424: scalaz.Scalaz.ValidationNEL[Throwable,Int] = Success(30)
scala> validateAddress(fetch(data1, "address"))
res426: scalaz.Scalaz.ValidationNEL[Throwable,String] = Success(Kanagawa Yokohama)
Mapに値が設定されていない場合は、以下のようにFailureになりました。
scala> validateName(fetch(Map.empty, "name"))
res428: scalaz.Scalaz.ValidationNEL[Throwable,String] = Failure(NonEmptyList(java.lang.IllegalArgumentException: No value))
Mapに設定されている値が異常だった場合は、以下のようにFailureになりました。
scala> validateAddress(fetch(Map("address" -> "Yokohama"), "address"))
res429: scalaz.Scalaz.ValidationNEL[Throwable,String] = Failure(NonEmptyList(java.lang.IllegalArgumentException: 住所が短すぎます))
Mapに設定されている値がデータ列だった場合は、以下のようにFailureになりました。
scala> validateName(fetch(Map("name" -> "Taro;Hanako"), "name"))
res431: scalaz.Scalaz.ValidationNEL[Throwable,String] = Failure(NonEmptyList(java.lang.IllegalArgumentException: Sequence value))
次回は、これらの部品を組み合わせてmakePerson関数を実装します。
前回までの部品
前回まで記事の中で作ってきた検証のための基本部品は以下の通りです。
def validate[T](f: T => Boolean, message: String, v: T): ValidationNEL[Throwable, T] = {
v |> f.toValidation(new IllegalArgumentException(message))
}
def validates[A, B](
f: A => ValidationNEL[Throwable, B],
g: Seq[B => ValidationNEL[Throwable, B]],
value: A
): ValidationNEL[Throwable, B] = {
g.foldLeft(f(value)) { (a, x) =>
a.flatMap(x)
}
}
def validatesDef[A, B](
d: (A => ValidationNEL[Throwable, B],
Seq[B => ValidationNEL[Throwable, B]]),
v: A
): ValidationNEL[Throwable, B] = {
validates(d._1, d._2, v)
}
def intValue = (_: String).parseInt.liftFailNel
def stringValue = (_: String).success[NonEmptyList[Throwable]]
def greaterEqual[T: Order](value: T, message: String) = {
validate(value.lte, message, (_: T))
}
def lessEqual[T: Order](value: T, message: String) = {
validate(value.gte, message, (_: T))
}
def maxLength[T](conv: T => Int, length: Int, message: String) = {
validate(conv(_: T) <= length, message, (_: T))
}
def maxStringLength(length: Int, message: String) = {
maxLength((_: String).length, length, message)
}
def minLength[T](conv: T => Int, length: Int, message: String) = {
validate(conv(_: T) >= length, message, (_: T))
}
def minStringLength(length: Int, message: String) = {
minLength((_: String).length, length, message)
}
Personオブジェクトの検証用の個々のプロパティに対する検証の定義は以下の3つになります。
val ageDef = (
intValue,
List(greaterEqual(0, "年齢が0より小さいです"),
lessEqual(150, "年齢が150より大きいです")))
val nameDef = (
stringValue,
List(maxStringLength(10, "名前が長すぎます"),
minStringLength(2, "名前が短すぎます")))
val addressDef = (
stringValue,
List(maxStringLength(100, "住所が長すぎます"),
minStringLength(10, "住所が短すぎます")))