Skip to content

Instantly share code, notes, and snippets.

@BranislavLazic
Last active June 26, 2021 10:50
Show Gist options
  • Save BranislavLazic/d9597a567bfc33613746c05e3b3534e6 to your computer and use it in GitHub Desktop.
Save BranislavLazic/d9597a567bfc33613746c05e3b3534e6 to your computer and use it in GitHub Desktop.
Scala Validated example
package ba.codeeng
import java.time.LocalDate
import cats.data.Validated
import cats.data.Validated._
import cats.data.NonEmptyList
import cats.implicits._
import java.time.format.DateTimeFormatter
import scala.util.Try
// For Scala 2.12, add "-Ypartial-unification" compiler option
// Why Cats Validated and not Either?
// Validated makes it easier to accumulate errors and validate fields in parallel.
// Use Either if you want to "fail fast".
// With Either, the first field that returns an error (Left type) will
// prevent further validation, and contain an information about the error for
// that field only.
// Make a dedicated type which represents error
case class ValidationError(field: String, message: String)
// Create common validation methods
object Validator {
// Create a type alias for Validated which accumulates errors in NEL
type ValidatedNel[T] = Validated[NonEmptyList[ValidationError], T]
// Create a simple validator
def notNull[T](value: T, field: String): ValidatedNel[T] =
if (value != null) value.valid else ValidationError(field, "cannot be null").invalidNel
// Checking whether a value is blank is unsafe since it can throw an NPE.
// In this case, you can compose validators with method for sequential validation - "andThen".
// If the validator fails, it will "short circuit" the validation.
def notBlank(value: String, field: String): ValidatedNel[String] =
notNull(value, field).andThen { v =>
if (!value.isBlank) v.valid else ValidationError(field, "cannot be blank").invalidNel
}
// More composition...
def minLength(value: String, field: String, min: Int): ValidatedNel[String] =
notBlank(value, field).andThen { v =>
if (value.length >= min) v.valid else ValidationError(field, s"must have at least $min characters").invalidNel
}
def dateFormat(value: String, field: String, format: String): ValidatedNel[String] =
Try(DateTimeFormatter.ofPattern(format).parse(value))
.fold(_ => ValidationError(field, s"invalid date format").invalidNel, _ => value.valid)
}
// Create a type that you want to validate
case class UserRequest(id: Option[Long], firstName: String, lastName: String, address: String, dob: String) {
import Validator._
// Create a tuple where each field returns a Validated.
// Apply validator methods from the above.
// If you want to ignore validation, just call "valid" method on a field.
// In the end, call "mapN" on the tuple, and it will apply values on your type.
// All encountering errors will be accumulated in the list.
def validate(): ValidatedNel[UserRequest] =
(
id.valid,
minLength(firstName, "firstName", 2),
minLength(lastName, "lastName", 2),
notBlank(address, "address"),
dateFormat(dob, "dob", "yyyy-MM-dd")
).mapN(UserRequest)
}
object Main {
def main(args: Array[String]): Unit = {
val userRequest = UserRequest(Some(1), "John", "Doe", "21st. Avenue", "1990-03-02")
println("This looks good: " + userRequest.validate)
println("Uh oh! This looks bad: " + UserRequest(Some(1), null, "D", " ", "05-03-1990").validate)
// fold over the result
userRequest.validate
.fold(
errors => println(s"Errors: ${errors.toList.mkString(",")}"),
req => println(s"Hooray! UserRequest is valid! $req")
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment