Last active
August 9, 2018 14:39
-
-
Save ambantis/99804354ef65123209239572ce40f8ab to your computer and use it in GitHub Desktop.
Data validation using just Scala
This file contains 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
/* sealed */ trait Color { | |
def isWarm: Boolean | |
def isCool: Boolean = !isWarm | |
} | |
object Color { | |
case object Red extends Color { val isWarm = true } | |
case object Yellow extends Color { val isWarm = true } | |
case object Orange extends Color { val isWarm = false } | |
case object Green extends Color { val isWarm = false } | |
case object Blue extends Color { val isWarm = false } | |
case object Purple extends Color { val isWarm = false } | |
} | |
object PersonTests { | |
def validName(name: String): Boolean = name.trim.nonEmpty | |
def validAge(age: Int): Boolean = age > 0 && age < 150 | |
def isCool(color: Color): Boolean = color.isCool | |
def errors(name: String, age: Int, favorite: Color): List[Throwable] = { | |
val builder = List.newBuilder[Throwable] | |
if (!validName(name)) builder += new IllegalArgumentException("invalid name!") | |
if (!validAge(age)) builder += new IllegalArgumentException("invalid age!") | |
if (!isCool(favorite)) builder += new IllegalArgumentException("not cool!") | |
builder.result() | |
} | |
} | |
/** Basic validation with require statements | |
* | |
* This is really good for asserting conditions, but can be tricky to use, | |
* and if there are multiple errors, only the first will be thrown. | |
*/ | |
final case class Person1(name: String, age: Int, favorite: Color) { | |
import PersonTests.{isCool, validAge, validName} | |
require(validName(name), "invalid name") | |
require(validAge(age), "invalid age") | |
require(isCool(favorite), "not cool") | |
} | |
/** Better validation with scala.util.Try | |
* | |
* The constructor is private; you can only create a person via `tryPerson`, | |
* but if there are multiple errors, only the first will be captured. | |
* | |
* Note: making the case class abstract makes the constructor private outside of | |
* this file. | |
*/ | |
abstract case class Person2(name: String, age: Int, favorite: Color) { | |
import PersonTests.{isCool, validAge, validName} | |
require(validName(name), "invalid name") | |
require(validAge(age), "invalid age") | |
require(isCool(favorite), "not cool") | |
} | |
object Person2 { | |
import scala.util.Try | |
def tryPerson(name: String, age: Int, favorite: Color): Try[Person2] = | |
Try(new Person2(name, age, favorite) {}) | |
} | |
/** Even better validation with scala Either. | |
* | |
* This will capture all the errors, but working with Either in 2.11 can be a bit | |
* clunky and capturing all the errors needs low level code. | |
*/ | |
abstract case class Person3(name: String, age: Int, favorite: Color) | |
object Person3 { | |
def eitherPerson(name: String, age: Int, favorite: Color): Either[List[Throwable], Person3] = | |
PersonTests.errors(name, age, favorite) match { | |
case Nil => Right(new Person3(name, age, favorite) {}) | |
case errors => Left(errors) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment