简体   繁体   中英

Validation of multiple ADTs with Scala Cats Validated

I am trying to validate a config in scala. First I convert the config json to the respective case class and then I validate it. As I want to fail slow I collect all the validations that fail rather than returning after the first validation that fails. I plan to use applicative functors provided by cats library Cats validation .

The problem I face is the form validation shown in the link works for simple case class

final case class RegistrationData(username: String, password: String, firstName: String, lastName: String, age: Int)

// Below is the code snippet for applying validation from the link   itself
 {
(validateUserName(username),
validatePassword(password),
validateFirstName(firstName),
validateLastName(lastName),
validateAge(age)).mapN(RegistrationData)}

// A more complex case for validations
final case class User(name:String,adds:List[Addresses])
final case class Address(street:String,lds:List[LandMark])
final case class LandMark(wellKnown:Boolean,street:String)

In this case validation on field 'username' is independent of validation on say 'firstName'. But what if the

  1. I had to impose some validation of kind that took both 'firstName' and 'userName' ( say hypothetically Levenstein distance of two should be <= some number ).
  2. case class was not made of simple primitives (Int,String) but had other case classes as its members. eg User case class as mentioned above.

In general is the approach of applicative functors suitable for this case ? Should I even collect all failed validations ?

PS: forgive me if mentioned something incorrectly, I am new to scala.

Based on cats validate example

import cats.data._
import cats.data.Validated._
import cats.implicits._

final case class RegistrationData(name: Name, age: Int, workAge: Int)

final case class Name(firstName: String, lastName: String)

sealed trait DomainValidation {
  def errorMessage: String
}

case object FirstNameHasSpecialCharacters extends DomainValidation {
  def errorMessage: String =
    "First name cannot contain spaces, numbers or special characters."
}

case object LastNameHasSpecialCharacters extends DomainValidation {
  def errorMessage: String =
    "Last name cannot contain spaces, numbers or special characters."
}

case object AgeIsInvalid extends DomainValidation {
  def errorMessage: String =
    "You must be aged 18 and not older than 75 to use our services."
}

case object AgeIsLessThanWorkInvalid extends DomainValidation {
  def errorMessage: String =
    "You must be aged 18 and not older than 75 to use our services."
}

sealed trait FormValidatorNec {

  type ValidationResult[A] = ValidatedNec[DomainValidation, A]

  private def validateFirstName(firstName: String): ValidationResult[String] =
    if (firstName.matches("^[a-zA-Z]+$")) firstName.validNec
    else FirstNameHasSpecialCharacters.invalidNec

  private def validateLastName(lastName: String): ValidationResult[String] =
    if (lastName.matches("^[a-zA-Z]+$")) lastName.validNec
    else LastNameHasSpecialCharacters.invalidNec

  private def validateAge(age: Int,
                          workAge: Int): ValidationResult[(Int, Int)] = {
    if (age >= 18 && age <= 75 && workAge >= 0)
      if (age > workAge)
        (age, workAge).validNec
      else
        AgeIsLessThanWorkInvalid.invalidNec
    else
      AgeIsInvalid.invalidNec
  }

  def validateForm(firstName: String,
                   lastName: String,
                   age: Int,
                   workAge: Int): ValidationResult[RegistrationData] = {
    (
      (validateFirstName(firstName), validateLastName(lastName)).mapN(Name),
      validateAge(age, workAge)
    ).mapN {
      case (n, (a, w)) => RegistrationData(name = n, age = a, workAge = w)
    }
  }

}

object FormValidatorNec extends FormValidatorNec

println(FormValidatorNec.validateForm("firstname", "lastname", 40, 30))
println(FormValidatorNec.validateForm("firs2tname", "lastname", 20, 30))

Check this fiddle

Function of mapN is called only when data in the tuple (ValidationResult[_], ValidationResult[_], ...) is Valid . If one or more elements in tuple are Invalid , they are collected into a NotEmtpyChain .

In summary, all of validate methods are called, and when all them return Valid[_] the mapN function is applied.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM