diff --git a/cats/src/io/github/iltotore/iron/cats.scala b/cats/src/io/github/iltotore/iron/cats.scala index ec4eb409..68873460 100644 --- a/cats/src/io/github/iltotore/iron/cats.scala +++ b/cats/src/io/github/iltotore/iron/cats.scala @@ -3,12 +3,15 @@ package io.github.iltotore.iron import _root_.cats.data.* import _root_.cats.kernel.{CommutativeMonoid, Hash, LowerBounded, PartialOrder, UpperBounded} import _root_.cats.syntax.either.* -import _root_.cats.{Eq, Monoid, Order, Show} +import _root_.cats.{Eq, Monoid, Order, Show, Traverse} import _root_.cats.data.Validated.{Invalid, Valid} import _root_.cats.Functor +import _root_.cats.implicits.* import io.github.iltotore.iron.constraint.numeric.{Greater, Less, Negative, Positive} import scala.util.NotGiven +import scala.util.boundary +import scala.util.boundary.break object cats extends IronCatsInstances: @@ -67,6 +70,50 @@ object cats extends IronCatsInstances: inline def refineValidatedNel[C](using inline constraint: Constraint[A, C]): ValidatedNel[String, A :| C] = Validated.condNel(constraint.test(value), value.asInstanceOf[A :| C], constraint.message) + extension [F[_], A](wrapper: F[A]) + + /** + * Refine the wrapped value(s) at runtime, accumulating errors. + * + * @param constraint the constraint to test with the value to refine. + * @return a [[Right]] containing this value as [[IronType]] or an [[Left]] containing a [[NonEmptyChain]] of errors. + * @see [[refineNec]]. + */ + inline def refineAllNec[C](using traverse: Traverse[F], inline constraint: Constraint[A, C]): EitherNec[InvalidValue[A], F[A :| C]] = + wrapper.refineAllValidatedNec[C].toEither + + /** + * Refine the wrapped value(s) at runtime, accumulating errors. + * + * @param constraint the constraint to test with the value to refine. + * @return a [[Right]] containing this value as [[IronType]] or an [[Left]] containing a [[NonEmptyList]] of errors. + * @see [[refineNec]]. + */ + inline def refineAllNel[C](using traverse: Traverse[F], inline constraint: Constraint[A, C]): EitherNel[InvalidValue[A], F[A :| C]] = + wrapper.refineAllValidatedNel[C].toEither + + /** + * Refine the wrapped value(s) at runtime, accumulating errors. + * + * @param constraint the constraint to test with the value to refine. + * @return a [[Valid]] containing this value as [[IronType]] or an [[Invalid]] containing a [[NonEmptyChain]] of errors. + * @see [[refineValidatedNec]]. + */ + inline def refineAllValidatedNec[C](using traverse: Traverse[F], inline constraint: Constraint[A, C]): ValidatedNec[InvalidValue[A], F[A :| C]] = + traverse.traverse(wrapper): value => + Validated.condNec[InvalidValue[A], A :| C](constraint.test(value), value.assume[C], InvalidValue(value, constraint.message)) + + /** + * Refine the wrapped value(s) at runtime, accumulating errors. + * + * @param constraint the constraint to test with the value to refine. + * @return a [[Valid]] containing this value as [[IronType]] or an [[Invalid]] containing a [[NonEmptyList]] of errors. + * @see [[refineValidatedNel]]. + */ + inline def refineAllValidatedNel[C](using traverse: Traverse[F], inline constraint: Constraint[A, C]): ValidatedNel[InvalidValue[A], F[A :| C]] = + traverse.traverse(wrapper): value => + Validated.condNel[InvalidValue[A], A :| C](constraint.test(value), value.assume[C], InvalidValue(value, constraint.message)) + extension [A, C1](value: A :| C1) /** @@ -119,6 +166,70 @@ object cats extends IronCatsInstances: inline def refineFurtherValidatedNel[C2](using inline constraint: Constraint[A, C2]): ValidatedNel[String, A :| (C1 & C2)] = (value: A).refineValidatedNel[C2].map(_.assumeFurther[C1]) + extension [F[_], A, C1](wrapper: F[A :| C1]) + + /** + * Refine further the wrapped value(s) at runtime, accumulating errors. + * + * @param constraint the constraint to test with the value to refine. + * @return a [[Right]] containing this value as [[IronType]] or an [[Left]] containing a [[NonEmptyChain]] of errors. + * @see [[refineFurtherNec]]. + */ + inline def refineAllFurtherNec[C2](using + traverse: Traverse[F], + inline constraint: Constraint[A, C2] + ): EitherNec[InvalidValue[A], F[A :| (C1 & C2)]] = + wrapper.refineAllFurtherValidatedNec[C2].toEither + + /** + * Refine further the wrapped value(s) at runtime, accumulating errors. + * + * @param constraint the constraint to test with the value to refine. + * @return a [[Right]] containing this value as [[IronType]] or an [[Left]] containing a [[NonEmptyList]] of errors. + * @see [[refineFurtherNel]]. + */ + inline def refineAllFurtherNel[C2](using + traverse: Traverse[F], + inline constraint: Constraint[A, C2] + ): EitherNel[InvalidValue[A], F[A :| (C1 & C2)]] = + wrapper.refineAllFurtherValidatedNel[C2].toEither + + /** + * Refine further the wrapped value(s) at runtime, accumulating errors. + * + * @param constraint the constraint to test with the value to refine. + * @return a [[Valid]] containing this value as [[IronType]] or an [[Invalid]] containing a [[NonEmptyChain]] of errors. + * @see [[refineFurtherValidatedNec]]. + */ + inline def refineAllFurtherValidatedNec[C2](using + traverse: Traverse[F], + inline constraint: Constraint[A, C2] + ): ValidatedNec[InvalidValue[A], F[A :| (C1 & C2)]] = + traverse.traverse(wrapper): value => + Validated.condNec[InvalidValue[A], A :| (C1 & C2)]( + constraint.test(value), + (value: A).assume[C1 & C2], + InvalidValue(value, constraint.message) + ) + + /** + * Refine further the wrapped value(s) at runtime, accumulating errors. + * + * @param constraint the constraint to test with the value to refine. + * @return a [[Valid]] containing this value as [[IronType]] or an [[Invalid]] containing a [[NonEmptyList]] of errors. + * @see [[refineFurtherValidatedNel]]. + */ + inline def refineAllFurtherValidatedNel[C2](using + traverse: Traverse[F], + inline constraint: Constraint[A, C2] + ): ValidatedNel[InvalidValue[A], F[A :| (C1 & C2)]] = + traverse.traverse(wrapper): value => + Validated.condNel[InvalidValue[A], A :| (C1 & C2)]( + constraint.test(value), + (value: A).assume[C1 & C2], + InvalidValue(value, constraint.message) + ) + extension [A, C, T](ops: RefinedTypeOps[A, C, T]) /** @@ -169,6 +280,48 @@ object cats extends IronCatsInstances: def validatedNel(value: A): ValidatedNel[String, T] = if ops.rtc.test(value) then Validated.validNel(value.asInstanceOf[T]) else Validated.invalidNel(ops.rtc.message) + /** + * Refine the given values applicatively at runtime, resulting in a [[EitherNec]]. + * + * @param constraint the constraint to test with the value to refine. + * @return a [[Right]] containing this value as [[IronType]] or an [[Left]] containing a [[NonEmptyChain]] of error messages. + * @see [[eitherNec]], [[eitherAllNel]]. + */ + def eitherAllNec[F[_]](value: F[A])(using Traverse[F]): EitherNec[InvalidValue[A], F[T]] = + ops.validatedAllNec(value).toEither + + /** + * Refine the given values applicatively at runtime, resulting in a [[EitherNel]]. + * + * @param constraint the constraint to test with the value to refine. + * @return a [[Right]] containing this value as [[IronType]] or an [[Left]] containing a [[NonEmptyList]] of error messages. + * @see [[eitherNel]], [[eitherAllNec]]. + */ + def eitherAllNel[F[_]](value: F[A])(using Traverse[F]): EitherNel[InvalidValue[A], F[T]] = + ops.validatedAllNel(value).toEither + + /** + * Refine the given values applicatively at runtime, resulting in a [[ValidatedNec]]. + * + * @param constraint the constraint to test with the value to refine. + * @return a [[Valid]] containing this value as [[IronType]] or an [[Invalid]] containing a [[NonEmptyChain]] of error messages. + * @see [[validatedNec]], [[validatedAllNel]]. + */ + def validatedAllNec[F[_]](wrapper: F[A])(using traverse: Traverse[F]): ValidatedNec[InvalidValue[A], F[T]] = + traverse.traverse(wrapper): value => + Validated.condNec[InvalidValue[A], T](ops.rtc.test(value), ops.assume(value), InvalidValue(value, ops.rtc.message)) + + /** + * Refine the given values applicatively at runtime, resulting in a [[ValidatedNel]]. + * + * @param constraint the constraint to test with the value to refine. + * @return a [[Valid]] containing this value as [[IronType]] or an [[Invalid]] containing a [[NonEmptyList]] of error messages. + * @see [[validatedNel]], [[validatedAllNec]]. + */ + def validatedAllNel[F[_]](wrapper: F[A])(using traverse: Traverse[F]): ValidatedNel[InvalidValue[A], F[T]] = + traverse.traverse(wrapper): value => + Validated.condNel[InvalidValue[A], T](ops.rtc.test(value), ops.assume(value), InvalidValue(value, ops.rtc.message)) + /** * Represent all Cats' typeclass instances for Iron. */ diff --git a/cats/test/src/io/github/iltotore/iron/CatsSuite.scala b/cats/test/src/io/github/iltotore/iron/CatsSuite.scala index 09ee93ff..fe5c1035 100644 --- a/cats/test/src/io/github/iltotore/iron/CatsSuite.scala +++ b/cats/test/src/io/github/iltotore/iron/CatsSuite.scala @@ -4,13 +4,13 @@ import _root_.cats.Show import _root_.cats.kernel.* import _root_.cats.derived.* import _root_.cats.instances.all.* -import io.github.iltotore.iron.cats.given +import io.github.iltotore.iron.cats.{*, given} import io.github.iltotore.iron.constraint.all.* import utest.{Show as _, *} +import _root_.cats.data.Chain import _root_.cats.data.NonEmptyChain import _root_.cats.data.NonEmptyList -import _root_.cats.data.Validated.{Valid, Invalid} -import _root_.cats.data.ValidatedNec +import _root_.cats.data.Validated.{Invalid, Valid} import scala.runtime.stdLibPatches.Predef.assert @@ -90,7 +90,7 @@ object CatsSuite extends TestSuite: test("eitherNec"): import io.github.iltotore.iron.cats.* - val eitherNecWithFailingPredicate = Temperature.eitherNec(-5.0) + val eitherNecWithFailingPredicate = Temperature.eitherNec(-5) assert(eitherNecWithFailingPredicate == Left(NonEmptyChain.one("Should be strictly positive")), "'eitherNec' returns left if predicate fails") val eitherNecWithSucceedingPredicate = Temperature.eitherNec(100) assert(eitherNecWithSucceedingPredicate == Right(Temperature(100)), "right should contain result of 'apply'") @@ -98,7 +98,7 @@ object CatsSuite extends TestSuite: test("eitherNel"): import io.github.iltotore.iron.cats.* - val eitherNelWithFailingPredicate = Temperature.eitherNel(-5.0) + val eitherNelWithFailingPredicate = Temperature.eitherNel(-5) assert(eitherNelWithFailingPredicate == Left(NonEmptyList.one("Should be strictly positive")), "'eitherNel' returns left if predicate fails") val eitherNelWithSucceedingPredicate = Temperature.eitherNel(100) assert(eitherNelWithSucceedingPredicate == Right(Temperature(100)), "right should contain result of 'apply'") @@ -106,7 +106,7 @@ object CatsSuite extends TestSuite: test("validated"): import io.github.iltotore.iron.cats.* - val validatedWithFailingPredicate = Temperature.validated(-5.0) + val validatedWithFailingPredicate = Temperature.validated(-5) assert(validatedWithFailingPredicate == Invalid("Should be strictly positive"), "'eitherNec' returns left if predicate fails") val validatedWithSucceedingPredicate = Temperature.validated(100) assert(validatedWithSucceedingPredicate == Valid(Temperature(100)), "right should contain result of 'apply'") @@ -114,7 +114,7 @@ object CatsSuite extends TestSuite: test("validatedNec"): import io.github.iltotore.iron.cats.* - val validatedNecWithFailingPredicate = Temperature.validatedNec(-5.0) + val validatedNecWithFailingPredicate = Temperature.validatedNec(-5) assert( validatedNecWithFailingPredicate == Invalid(NonEmptyChain.one("Should be strictly positive")), "'validatedNec' returns left if predicate fails" @@ -125,7 +125,7 @@ object CatsSuite extends TestSuite: test("validatedNel"): import io.github.iltotore.iron.cats.* - val validatedNelWithFailingPredicate = Temperature.validatedNel(-5.0) + val validatedNelWithFailingPredicate = Temperature.validatedNel(-5) assert( validatedNelWithFailingPredicate == Invalid(NonEmptyList.one("Should be strictly positive")), "'validatedNel' returns left if predicate fails" @@ -133,7 +133,104 @@ object CatsSuite extends TestSuite: val validatedNelWithSucceedingPredicate = Temperature.validatedNel(100) assert(validatedNelWithSucceedingPredicate == Valid(Temperature(100)), "valid should contain result of 'apply'") - test("refineAll"): - test - assert(Temperature.optionAll(NonEmptyList.of(1, 2, -3)).isEmpty) - test - assert(Temperature.optionAll(NonEmptyList.of(1, 2, 3)).contains(NonEmptyList.of(Temperature(1), Temperature(2), Temperature(3)))) + test("all"): + test("functoToMapLogic"): + test - assert(Temperature.optionAll(NonEmptyList.of(1, 2, -3)).isEmpty) + test - assert(Temperature.optionAll(NonEmptyList.of(1, 2, 3)).contains(NonEmptyList.of(Temperature(1), Temperature(2), Temperature(3)))) + + val valid = List(1, 2, 3) + val invalid = List(1, -2, -3) + + test("simple"): + test("eitherNec"): + test - assert(valid.refineAllNec[Positive] == Right(valid)) + test - assert(invalid.refineAllNec[Positive] == Left(NonEmptyChain.of( + InvalidValue(-2, "Should be strictly positive"), + InvalidValue(-3, "Should be strictly positive") + ))) + + test("eitherNel"): + test - assert(valid.refineAllNel[Positive] == Right(valid)) + test - assert(invalid.refineAllNel[Positive] == Left(NonEmptyList.of( + InvalidValue(-2, "Should be strictly positive"), + InvalidValue(-3, "Should be strictly positive") + ))) + + test("validatedNec"): + test - assert(valid.refineAllValidatedNec[Positive] == Valid(valid)) + test - assert(invalid.refineAllValidatedNec[Positive] == Invalid(NonEmptyChain.of( + InvalidValue(-2, "Should be strictly positive"), + InvalidValue(-3, "Should be strictly positive") + ))) + + test("validatedNel"): + test - assert(valid.refineAllValidatedNel[Positive] == Valid(valid)) + test - assert(invalid.refineAllValidatedNel[Positive] == Invalid(NonEmptyList.of( + InvalidValue(-2, "Should be strictly positive"), + InvalidValue(-3, "Should be strictly positive") + ))) + + test("further"): + + val furtherValid = List(2, 4, 6).refineAllUnsafe[Positive] + val furtherInvalid = List(1, 2, 3).refineAllUnsafe[Positive] + + test("eitherNec"): + test - assert(furtherValid.refineAllFurtherNec[Even] == Right(furtherValid)) + test - assert(furtherInvalid.refineAllFurtherNec[Even] == Left(NonEmptyChain.of( + InvalidValue(1, "Should be a multiple of 2"), + InvalidValue(3, "Should be a multiple of 2") + ))) + + test("eitherNel"): + test - assert(furtherValid.refineAllFurtherNel[Even] == Right(furtherValid)) + test - assert(furtherInvalid.refineAllFurtherNel[Even] == Left(NonEmptyList.of( + InvalidValue(1, "Should be a multiple of 2"), + InvalidValue(3, "Should be a multiple of 2") + ))) + + test("validatedNec"): + test - assert(furtherValid.refineAllFurtherValidatedNec[Even] == Valid(furtherValid)) + test - assert(furtherInvalid.refineAllFurtherValidatedNec[Even] == Invalid(NonEmptyChain.of( + InvalidValue(1, "Should be a multiple of 2"), + InvalidValue(3, "Should be a multiple of 2") + ))) + + test("validatedNel"): + test - assert(furtherValid.refineAllFurtherValidatedNel[Even] == Valid(furtherValid)) + test - assert(furtherInvalid.refineAllFurtherValidatedNel[Even] == Invalid(NonEmptyList.of( + InvalidValue(1, "Should be a multiple of 2"), + InvalidValue(3, "Should be a multiple of 2") + ))) + + test("ops"): + test("eitherNec"): + test - assert(Temperature.eitherAllNec(valid) == Right(Temperature.assumeAll(valid))) + test - assert(Temperature.eitherAllNec(invalid) == Left(NonEmptyChain.of( + InvalidValue(-2, "Should be strictly positive"), + InvalidValue(-3, "Should be strictly positive") + ))) + + test("eitherNel"): + test - assert(Temperature.eitherAllNel(valid) == Right(Temperature.assumeAll(valid))) + test - assert(Temperature.eitherAllNel(invalid) == Left(NonEmptyList.of( + InvalidValue(-2, "Should be strictly positive"), + InvalidValue(-3, "Should be strictly positive") + ))) + + test("validatedNec"): + test - assert(Temperature.validatedAllNec(valid) == Valid(Temperature.assumeAll(valid))) + test - assert(Temperature.validatedAllNec(invalid) == Invalid(NonEmptyChain.of( + InvalidValue(-2, "Should be strictly positive"), + InvalidValue(-3, "Should be strictly positive") + ))) + + test("validatedNel"): + test - assert(Temperature.validatedAllNel(valid) == Valid(Temperature.assumeAll(valid))) + test - assert(Temperature.validatedAllNel(invalid) == Invalid(NonEmptyList.of( + InvalidValue(-2, "Should be strictly positive"), + InvalidValue(-3, "Should be strictly positive") + ))) + + } diff --git a/cats/test/src/io/github/iltotore/iron/RefinedOpsTypes.scala b/cats/test/src/io/github/iltotore/iron/RefinedOpsTypes.scala index 1a6bacb8..6d6abce5 100644 --- a/cats/test/src/io/github/iltotore/iron/RefinedOpsTypes.scala +++ b/cats/test/src/io/github/iltotore/iron/RefinedOpsTypes.scala @@ -4,8 +4,8 @@ import io.github.iltotore.iron.* import io.github.iltotore.iron.constraint.numeric.Positive //Opaque types are truly opaque when used in another file than the one where they're defined. See Scala documentation. -opaque type Temperature = Double :| Positive -object Temperature extends RefinedTypeOps[Double, Positive, Temperature] +opaque type Temperature = Int :| Positive +object Temperature extends RefinedTypeOps[Int, Positive, Temperature] type Moisture = Double :| Positive object Moisture extends RefinedTypeOps.Transparent[Moisture] diff --git a/main/src/io/github/iltotore/iron/InvalidValue.scala b/main/src/io/github/iltotore/iron/InvalidValue.scala new file mode 100644 index 00000000..82c2e6d9 --- /dev/null +++ b/main/src/io/github/iltotore/iron/InvalidValue.scala @@ -0,0 +1,3 @@ +package io.github.iltotore.iron + +case class InvalidValue[A](value: A, message: String) diff --git a/zio/test/src/io/github/iltotore/iron/RefinedOpsTypes.scala b/zio/test/src/io/github/iltotore/iron/RefinedOpsTypes.scala index 1a6bacb8..42badc87 100644 --- a/zio/test/src/io/github/iltotore/iron/RefinedOpsTypes.scala +++ b/zio/test/src/io/github/iltotore/iron/RefinedOpsTypes.scala @@ -4,8 +4,5 @@ import io.github.iltotore.iron.* import io.github.iltotore.iron.constraint.numeric.Positive //Opaque types are truly opaque when used in another file than the one where they're defined. See Scala documentation. -opaque type Temperature = Double :| Positive -object Temperature extends RefinedTypeOps[Double, Positive, Temperature] - -type Moisture = Double :| Positive -object Moisture extends RefinedTypeOps.Transparent[Moisture] +opaque type Temperature = Int :| Positive +object Temperature extends RefinedTypeOps[Int, Positive, Temperature]