Skip to content

Commit

Permalink
feat: Add first order aliases for ZIO's Validation
Browse files Browse the repository at this point in the history
  • Loading branch information
Iltotore committed May 4, 2024
1 parent 7477c1d commit 2b44798
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 7 deletions.
26 changes: 24 additions & 2 deletions zio/src/io/github/iltotore/iron/zio.scala
@@ -1,7 +1,7 @@
package io.github.iltotore.iron

import _root_.zio.NonEmptyChunk
import _root_.zio.prelude.{Covariant, Debug, Equal, Hash, Ord, Validation}
import _root_.zio.prelude.{Covariant, Debug, Equal, ForEach, Hash, Ord, Validation}

object zio extends RefinedTypeOpsZio:

Expand All @@ -15,6 +15,12 @@ object zio extends RefinedTypeOpsZio:
inline def refineValidation[C](using inline constraint: Constraint[A, C]): Validation[String, A :| C] =
Validation.fromPredicateWith(constraint.message)(value.asInstanceOf[A :| C])(constraint.test(_))

extension [F[+_], A](wrapper: F[A])

inline def refineAllValidation[C](using forEach: ForEach[F], inline constraint: Constraint[A, C]): Validation[InvalidValue[A], F[A :| C]] =
forEach.forEach(wrapper): value =>
Validation.fromPredicateWith[InvalidValue[A], A :| C](InvalidValue(value, constraint.message))(value.assume[C])(constraint.test(_))

extension [A, C1](value: A :| C1)
/**
* Refine the given value again applicatively at runtime, resulting in a [[Validation]].
Expand All @@ -25,16 +31,32 @@ object zio extends RefinedTypeOpsZio:
inline def refineFurtherValidation[C2](using inline constraint: Constraint[A, C2]): Validation[String, A :| (C1 & C2)] =
(value: A).refineValidation[C2].map(_.assumeFurther[C1])

extension [F[+_], A, C1](wrapper: F[A :| C1])

inline def refineAllFurtherValidation[C2](using forEach: ForEach[F], inline constraint: Constraint[A, C2]): Validation[InvalidValue[A], F[A :| (C1 & C2)]] =
forEach.forEach(wrapper): value =>
Validation.fromPredicateWith[InvalidValue[A], A :| (C1 & C2)](InvalidValue(value, constraint.message))(value.assume[C1 & C2])(constraint.test(_))

extension [A, C, T](ops: RefinedTypeOps[A, C, T])
/**
* Refine the given value applicatively at runtime, resulting in a [[Validation]].
*
* @param constraint the constraint to test with the value to refine.
* @return a [[Valid]] containing this value as [[T]] or an [[Validation.Failure]] containing a [[NonEmptyChunk]] of error messages.
*/
def validation(value: A): Validation[String, T] =
Validation.fromPredicateWith(ops.rtc.message)(value)(ops.rtc.test(_)).asInstanceOf[Validation[String, T]]

extension [A, C, T](ops: RefinedTypeOps[A, C, T])

/**
* Refine the given values applicatively at runtime, resulting in a [[Validation]].
*
* @return a [[Valid]] containing the values as `F[T]` or an [[Validation.Failure]] containing a [[NonEmptyChunk]] of error messages.
*/
def validationAll[F[+_]](wrapper: F[A])(using forEach: ForEach[F]): Validation[InvalidValue[A], F[T]] =
forEach.forEach(wrapper): value =>
ops.assumeAll(Validation.fromPredicateWith[InvalidValue[A], A](InvalidValue(value, ops.rtc.message))(value)(ops.rtc.test(_)))

given [F[+_]](using covariant: Covariant[F]): MapLogic[F] with

override def map[A, B](wrapper: F[A], f: A => B): F[B] = covariant.map(f)(wrapper)
Expand Down
38 changes: 33 additions & 5 deletions zio/test/src/io/github/iltotore/iron/ZIOSuite.scala
Expand Up @@ -13,13 +13,41 @@ object ZIOSuite extends TestSuite:
val tests: Tests = Tests:

test("ZIO validation"):
assert(Temperature.validation(2.0) == ZValidation.Success[String, Temperature](Chunk.empty, Temperature(2.0)))
assert(Temperature.validation(2) == ZValidation.Success[String, Temperature](Chunk.empty, Temperature(2)))

assert(
Temperature.validation(0.0) ==
Temperature.validation(0) ==
ZValidation.Failure[String, String](Chunk.empty, NonEmptyChunk.single("Should be strictly positive"))
)

test("refineAll"):
test - assert(Temperature.optionAll(NonEmptyChunk(1, 2, 3)).contains(NonEmptyChunk(Temperature(1), Temperature(2), Temperature(3))))
test - assert(Temperature.optionAll(NonEmptyChunk(1, 2, -3)).isEmpty)
test("all"):
test("mapLogicToCovariant"):
test - assert(Temperature.optionAll(NonEmptyChunk(1, 2, 3)).contains(NonEmptyChunk(Temperature(1), Temperature(2), Temperature(3))))
test - assert(Temperature.optionAll(NonEmptyChunk(1, 2, -3)).isEmpty)

val valid = List(1, 2, 3)
val invalid = List(1, -2, -3)

test("validation"):
test - assert(valid.refineAllValidation[Positive] == ZValidation.Success(Chunk.empty, Chunk.from(valid)))
test - assert(invalid.refineAllValidation[Positive] == ZValidation.Failure(Chunk.empty, NonEmptyChunk(
InvalidValue(-2, "Should be strictly positive"),
InvalidValue(-3, "Should be strictly positive")
)))

test("newtype"):
test - assert(Temperature.validationAll(valid) == ZValidation.Success(Chunk.empty, Chunk.from(valid)))
test - assert(Temperature.validationAll(invalid) == ZValidation.Failure(Chunk.empty, NonEmptyChunk(
InvalidValue(-2, "Should be strictly positive"),
InvalidValue(-3, "Should be strictly positive")
)))

test("furtherValidation"):
val furtherValid = List(2, 4, 6).refineAllUnsafe[Positive]
val furtherInvalid = List(1, 2, 3).refineAllUnsafe[Positive]

test - assert(furtherValid.refineAllFurtherValidation[Even] == ZValidation.Success(Chunk.empty, Chunk.from(furtherValid)))
test - assert(furtherInvalid.refineAllFurtherValidation[Even] == ZValidation.Failure(Chunk.empty, NonEmptyChunk(
InvalidValue(1, "Should be a multiple of 2"),
InvalidValue(3, "Should be a multiple of 2")
)))

0 comments on commit 2b44798

Please sign in to comment.