Last active
June 26, 2021 10:50
-
-
Save BranislavLazic/d9597a567bfc33613746c05e3b3534e6 to your computer and use it in GitHub Desktop.
Scala Validated example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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