Skip to content

Commit

Permalink
added new methods for Validation
Browse files Browse the repository at this point in the history
  • Loading branch information
pablf committed Dec 24, 2023
1 parent be55536 commit 34879f7
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ sealed trait Bool[A] { self =>
def &&(that: Bool[A]): Bool[A] = Bool.And(self, that)
def ||(that: Bool[A]): Bool[A] = Bool.Or(self, that)
def unary_! : Bool[A] = Bool.Not(self)

def map[B](f: A => B, notCounter: Int = 0): Bool[B] = self match {
case Bool.And(left, right) => Bool.And(left.map(f, notCounter), right.map(f, notCounter))
case Bool.Or(left, right) => Bool.Or(left.map(f, notCounter), right.map(f, notCounter))
case Bool.Leaf(value) => Bool.Leaf(f(value))
case Bool.Not(value) => Bool.Not(value.map(f, notCounter + 1))
}
}

object Bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package zio.schema.validation

import zio.Chunk

sealed trait Predicate[A] {
sealed trait Predicate[A] { self =>
type Errors = Chunk[ValidationError]
type Result = Either[Errors, Errors]
def validate(value: A): Result
def premap[B](f: B => A): Predicate[B] = Predicate.Premap(Bool.Leaf(self), f)
}

object Predicate {
Expand Down Expand Up @@ -69,4 +70,27 @@ object Predicate {
final case class True[A]() extends Predicate[A] { // A => True
def validate(value: A): Result = Right(Chunk.empty)
}

final case class Optional[A](pred: Bool[Predicate[A]], validNone: Boolean) extends Predicate[Option[A]] {

def validate(value: Option[A]): Result = value match {
case None =>
if (validNone) Right(Chunk(ValidationError.EqualToNone())) else Left(Chunk(ValidationError.EqualToNone()))
case Some(v) => Validation(pred).validate(v).map(_ => Chunk.empty)
}
}

final case class Premap[B, A](pred: Bool[Predicate[A]], f: (B => A)) extends Predicate[B] {
def validate(value: B): Result = Validation(pred).validate(f(value)).map(_ => Chunk.empty)
}

final case class Either[L, R](left: Bool[Predicate[L]], right: Bool[Predicate[R]])
extends Predicate[scala.util.Either[L, R]] {

def validate(value: scala.util.Either[L, R]): Result = value match {
case scala.util.Left(l) => Validation(left).validate(l).map(_ => Chunk.empty)
case scala.util.Right(r) => Validation(right).validate(r).map(_ => Chunk.empty)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,27 @@ final case class Validation[A](bool: Bool[Predicate[A]]) { self =>
def ||(that: Validation[A]): Validation[A] = Validation(self.bool || that.bool)
def unary_! : Validation[A] = Validation(!self.bool)

/*
Returns a `Validation` for `Option[A]` applying current `Validation` if found value is `Some(_)` and accepts `None` depending on `validNone`.
*/
def optional(validNone: Boolean = true): Validation[Option[A]] =
Validation(Bool.Leaf(Predicate.Optional(bool, validNone)))

/*
Returns a new `Validation` transforming a `B` value using `f` and then validating.
*/
def premap[B](f: B => A): Validation[B] = Validation(bool.map(_.premap(f)))

/*
Returns a new `Validation` for `Either[B, A]`. With default `onLeft` fails on `Left` and applies current validation on `Right`.
*/
def right[B](onLeft: Validation[B] = Validation.fail[B]): Validation[Either[B, A]] = Validation.either(onLeft, self)

/*
Returns a new `Validation` for `Either[A, B]`. With default `onRight` fails on `Right` and applies current validation on `Left`.
*/
def left[B](onRight: Validation[B] = Validation.fail[B]): Validation[Either[A, B]] = Validation.either(self, onRight)

def validate(value: A): Either[Chunk[ValidationError], Unit] = {
type Errors = Chunk[ValidationError]
type Result = Either[Errors, Errors]
Expand Down Expand Up @@ -76,4 +97,7 @@ object Validation extends Regexs with Time {

def anyOf[A](vs: Validation[A]*): Validation[A] = vs.foldLeft(fail[A])(_ || _)
def anyOf[A](vl: Iterable[Validation[A]]): Validation[A] = anyOf(vl.toSeq: _*)

def either[L, R](left: Validation[L], right: Validation[R]): Validation[scala.util.Either[L, R]] =
Validation(Bool.Leaf(Predicate.Either(left.bool, right.bool)))
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,18 @@ object ValidationError {
override def message: String =
s"$value should be equal to $expected"
}

final case class EqualToNone() extends ValidationError {
override def message: String =
s"Value should not be None"
}
final case class EqualToLeft[A](value: A) extends ValidationError {
override def message: String =
s"$value should not be Left"
}
final case class EqualToRight[A](value: A) extends ValidationError {
override def message: String =
s"$value should not be Right"
}
final case class NotEqualTo[A](value: A, expected: A) extends ValidationError {
override def message: String =
s"$value should not be equal to $expected"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,47 @@ object ValidationSpec extends ZIOSpecDefault {
import zio.schema.validation.ValidationSpec.Second._

def spec: Spec[Environment with TestEnvironment with Scope, Any] = suite("ValidationSpec")(
test("Optional") {
val validationDefault = Validation.greaterThan(4).optional(true)
val validationTrue = Validation.greaterThan(4).optional(true)
val validationFalse = Validation.greaterThan(4).optional(false)

assertTrue(validationDefault.validate(Some(4)).isLeft) &&
assertTrue(validationDefault.validate(Some(5)).isRight) &&
assertTrue(validationDefault.validate(None).isRight) &&
assertTrue(validationTrue.validate(Some(4)).isLeft) &&
assertTrue(validationTrue.validate(Some(5)).isRight) &&
assertTrue(validationTrue.validate(None).isRight) &&
assertTrue(validationFalse.validate(Some(4)).isLeft) &&
assertTrue(validationFalse.validate(Some(5)).isRight) &&
assertTrue(validationFalse.validate(None).isLeft)

},
test("Premap") {
val validation = Validation.greaterThan(4).premap[Int](x => x - 10)

assertTrue(validation.validate(14).isLeft) &&
assertTrue(validation.validate(15).isRight)

},
test("Either") {
val validation = Validation.greaterThan(4)
val validationLeft = validation.left[String]()
val validationRight = validation.right[String]()
val validationBoth = Validation.either(Validation.greaterThan(4), Validation.greaterThan(5))

assertTrue(validationLeft.validate(Left(4)).isLeft) &&
assertTrue(validationLeft.validate(Left(5)).isRight) &&
assertTrue(validationLeft.validate(Right("a")).isLeft) &&
assertTrue(validationRight.validate(Right(4)).isLeft) &&
assertTrue(validationRight.validate(Left("a")).isLeft) &&
assertTrue(validationRight.validate(Right(5)).isRight) &&
assertTrue(validationBoth.validate(Left(4)).isLeft) &&
assertTrue(validationBoth.validate(Left(5)).isRight) &&
assertTrue(validationBoth.validate(Right(5)).isLeft) &&
assertTrue(validationBoth.validate(Right(6)).isRight)

},
test("Greater than") {
val validation = Validation.greaterThan(4)

Expand Down

0 comments on commit 34879f7

Please sign in to comment.