diff --git a/zio-schema/shared/src/main/scala/zio/schema/validation/Bool.scala b/zio-schema/shared/src/main/scala/zio/schema/validation/Bool.scala index 05c9cab78..77b6e3548 100644 --- a/zio-schema/shared/src/main/scala/zio/schema/validation/Bool.scala +++ b/zio-schema/shared/src/main/scala/zio/schema/validation/Bool.scala @@ -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 { diff --git a/zio-schema/shared/src/main/scala/zio/schema/validation/Predicate.scala b/zio-schema/shared/src/main/scala/zio/schema/validation/Predicate.scala index 3f47a297e..37f4c0220 100644 --- a/zio-schema/shared/src/main/scala/zio/schema/validation/Predicate.scala +++ b/zio-schema/shared/src/main/scala/zio/schema/validation/Predicate.scala @@ -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 { @@ -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) + } + } + } diff --git a/zio-schema/shared/src/main/scala/zio/schema/validation/Validation.scala b/zio-schema/shared/src/main/scala/zio/schema/validation/Validation.scala index 078795ef1..9a8a5ede2 100644 --- a/zio-schema/shared/src/main/scala/zio/schema/validation/Validation.scala +++ b/zio-schema/shared/src/main/scala/zio/schema/validation/Validation.scala @@ -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] @@ -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))) } diff --git a/zio-schema/shared/src/main/scala/zio/schema/validation/ValidationErrors.scala b/zio-schema/shared/src/main/scala/zio/schema/validation/ValidationErrors.scala index d1d0049bb..1b8e19c2d 100644 --- a/zio-schema/shared/src/main/scala/zio/schema/validation/ValidationErrors.scala +++ b/zio-schema/shared/src/main/scala/zio/schema/validation/ValidationErrors.scala @@ -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" diff --git a/zio-schema/shared/src/test/scala/zio/schema/validation/ValidationSpec.scala b/zio-schema/shared/src/test/scala/zio/schema/validation/ValidationSpec.scala index 9b6ddcac8..2fe499e7e 100644 --- a/zio-schema/shared/src/test/scala/zio/schema/validation/ValidationSpec.scala +++ b/zio-schema/shared/src/test/scala/zio/schema/validation/ValidationSpec.scala @@ -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)