Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added new methods for Validation #624

Merged
merged 5 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This notCounter parameter is not used for anything

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this called contramap?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you are right! Fixed this and the counter.

}

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
Loading