Skip to content

Instantly share code, notes, and snippets.

@loicknuchel
Created August 16, 2017 16:21
Show Gist options
  • Save loicknuchel/e38ab17ec3ce1f777b766ddc7fad8605 to your computer and use it in GitHub Desktop.
Save loicknuchel/e38ab17ec3ce1f777b766ddc7fad8605 to your computer and use it in GitHub Desktop.
import scala.util.{Failure, Success, Try}
object ValidationMonad {
sealed abstract class Validation[+A] {
def isValid: Boolean
def get: A
def getOrElse[B >: A](default: => B): B
def map[B](f: A => B): Validation[B]
def flatMap[B](f: A => Validation[B]): Validation[B]
}
case class Valid[A](value: A) extends Validation[A] {
def isValid: Boolean = true
def get: A = value
def getOrElse[B >: A](default: => B): B = value
def map[B](f: A => B): Validation[B] = Valid(f(value))
def flatMap[B](f: A => Validation[B]): Validation[B] = f(value)
}
case class Invalid[A](errors: Seq[String]) extends Validation[A] {
def isValid: Boolean = false
def get: A = throw new NoSuchElementException("Invalid.get")
def getOrElse[B >: A](default: => B): B = default
def map[B](f: A => B): Validation[B] = Invalid(errors)
def flatMap[B](f: A => Validation[B]): Validation[B] = f(null.asInstanceOf[A]) match {
case Valid(_) => Invalid(errors)
case Invalid(errs) => Invalid(errors ++ errs)
}
}
object Validation {
def apply[A](value: => A, error: Throwable => String = _.getMessage): Validation[A] =
Try(value) match {
case Success(v) => Valid(v)
case Failure(e) => e match {
case _: NullPointerException => Invalid()
case _ => Invalid(error(e))
}
}
def apply[A](value: => A, message: String): Validation[A] =
apply(value, _ => message)
}
object Invalid {
def apply[A](errors: String*): Validation[A] =
Invalid(errors)
}
}
import org.scalatest.{FunSpec, Matchers}
class ValidationMonadSpec extends FunSpec with Matchers {
describe("ValidationMonad") {
import ValidationMonad._
it("should tell if valid") {
Valid(1).isValid shouldBe true
Invalid().isValid shouldBe false
}
it("should extract the value") {
Valid(1).get shouldBe 1
assertThrows[NoSuchElementException] {
Invalid().get
}
}
it("should extract the value with default") {
Valid(1).getOrElse(2) shouldBe 1
Invalid().getOrElse(2) shouldBe 2
}
it("should transform the value") {
Valid(1).map(_.toString) shouldBe Valid("1")
Invalid[Int]().map(_.toString) shouldBe Invalid()
}
it("should flatMap the value") {
Valid(1).flatMap(n => Valid(n + 1)) shouldBe Valid(2)
Valid(1).flatMap(_ => Invalid()) shouldBe Invalid()
Invalid[Int]().flatMap(n => Valid(n + 1)) shouldBe Invalid()
Invalid[Int]().flatMap(_ => Invalid()) shouldBe Invalid()
}
it("should succeed when all validations are valid") {
val map = Map(
"a" -> 1,
"b" -> 2,
"c" -> 3,
"d" -> 4)
val result: Validation[Int] = for {
a <- Validation(map("a"), "key 'a' is missing")
b <- Validation(map("b"), "key 'b' is missing")
c <- Validation(map("c"), "key 'c' is missing")
d <- Validation(map("d"), "key 'd' is missing")
} yield a + b + c + d
result shouldBe Valid(10)
}
it("should return the error if exists") {
val map = Map(
"a" -> 1,
"b" -> 2,
"d" -> 4)
val result: Validation[Int] = for {
a <- Validation(map("a"), "key 'a' is missing")
b <- Validation(map("b"), "key 'b' is missing")
c <- Validation(map("c"), "key 'c' is missing")
d <- Validation(map("d"), "key 'd' is missing")
} yield a + b + c + d
result shouldBe Invalid("key 'c' is missing")
}
it("should return all errors") {
val map = Map(
"a" -> 1,
"d" -> 4)
val result: Validation[Int] = for {
a <- Validation(map("a"), "key 'a' is missing")
b <- Validation(map("b"), "key 'b' is missing")
c <- Validation(map("c"), "key 'c' is missing")
d <- Validation(map("d"), "key 'd' is missing")
} yield a + b + c + d
result shouldBe Invalid("key 'b' is missing", "key 'c' is missing")
}
it("should not return null errors") {
val map = Map(
"a" -> "a",
"c" -> "c",
"d" -> "d")
val result: Validation[String] = for {
a <- Validation(map("a"), "key 'a' is missing")
b <- Validation(map("b"), "key 'b' is missing")
c <- Validation(map(b.toString), "key 'c' is missing")
d <- Validation(map("d"), "key 'd' is missing")
} yield a + b + c + d
result shouldBe Invalid("key 'b' is missing")
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment