From fdbc7562fe70d4a4e6e7c227f1f1a92dd7bb2a49 Mon Sep 17 00:00:00 2001 From: Iltotore Date: Mon, 12 Feb 2024 07:28:29 +0100 Subject: [PATCH] feat: Add "all" refinement methods --- .../io/github/iltotore/iron/MapLogic.scala | 27 ++++++++ .../github/iltotore/iron/RefinedTypeOps.scala | 64 +++++++++++++++---- .../io/github/iltotore/iron/conversion.scala | 49 +++++++++++++- .../src/io/github/iltotore/iron/package.scala | 50 ++++++++++++++- .../iron/testing/RefinedTypeOpsSuite.scala | 25 ++++++-- 5 files changed, 194 insertions(+), 21 deletions(-) create mode 100644 main/src/io/github/iltotore/iron/MapLogic.scala diff --git a/main/src/io/github/iltotore/iron/MapLogic.scala b/main/src/io/github/iltotore/iron/MapLogic.scala new file mode 100644 index 00000000..db82a503 --- /dev/null +++ b/main/src/io/github/iltotore/iron/MapLogic.scala @@ -0,0 +1,27 @@ +package io.github.iltotore.iron + +import scala.collection.IterableOnceOps +import scala.concurrent.{ExecutionContext, Future} + +/** + * A typeclass providing a `map` method. Mainly used to abstract over Cats and ZIO Prelude. + * + * @tparam F the wrapper type + */ +trait MapLogic[F[_]]: + + def map[A, B](wrapper: F[A], f: A => B): F[B] + +object MapLogic: + + given [C, CC[x] <: IterableOnceOps[x, CC, C]]: MapLogic[CC] with + + def map[A, B](wrapper: CC[A], f: A => B): CC[B] = wrapper.map(f) + + given [L]: MapLogic[[x] =>> Either[L, x]] with + + override def map[A, B](wrapper: Either[L, A], f: A => B): Either[L, B] = wrapper.map(f) + + given (using ExecutionContext): MapLogic[Future] with + + override def map[A, B](wrapper: Future[A], f: A => B): Future[B] = wrapper.map(f) \ No newline at end of file diff --git a/main/src/io/github/iltotore/iron/RefinedTypeOps.scala b/main/src/io/github/iltotore/iron/RefinedTypeOps.scala index 4c991e85..c6dd0e31 100644 --- a/main/src/io/github/iltotore/iron/RefinedTypeOps.scala +++ b/main/src/io/github/iltotore/iron/RefinedTypeOps.scala @@ -2,6 +2,8 @@ package io.github.iltotore.iron import scala.compiletime.summonInline import scala.reflect.TypeTest +import scala.util.boundary +import scala.util.boundary.break /** * Utility trait for new types' companion object. @@ -37,6 +39,16 @@ trait RefinedTypeOps[A, C, T](using private val _rtc: RuntimeConstraint[A, C]): */ inline def assume(value: A): T = value.asInstanceOf[T] + /** + * Refine the given value at runtime. + * + * @return this value as [[T]]. + * @throws an [[IllegalArgumentException]] if the constraint is not satisfied. + * @see [[fromIronType]], [[either]], [[option]]. + */ + inline def applyUnsafe(value: A): T = + if rtc.test(value) then value.asInstanceOf[T] else throw new IllegalArgumentException(rtc.message) + /** * Refine the given value at runtime, resulting in an [[Either]]. * @@ -48,8 +60,6 @@ trait RefinedTypeOps[A, C, T](using private val _rtc: RuntimeConstraint[A, C]): /** * Refine the given value at runtime, resulting in an [[Option]]. - * - * @param constraint the constraint to test with the value to refine. * @return an Option containing this value as [[T]] or [[None]]. * @see [[fromIronType]], [[either]], [[applyUnsafe]]. */ @@ -57,22 +67,52 @@ trait RefinedTypeOps[A, C, T](using private val _rtc: RuntimeConstraint[A, C]): Option.when(rtc.test(value))(value.asInstanceOf[T]) /** - * Refine the given value at runtime. + * Refine the given value(s), assuming the constraint holds. * - * @return this value as [[T]]. - * @throws an [[IllegalArgumentException]] if the constraint is not satisfied. - * @see [[fromIronType]], [[either]], [[option]]. + * @return a wrapper of constrained values, without performing constraint checks. + * @see [[assume]]. */ - inline def applyUnsafe(value: A): T = - if rtc.test(value) then value.asInstanceOf[T] else throw new IllegalArgumentException(rtc.message) + inline def assumeAll[F[_]](wrapper: F[A]): F[T] = wrapper.asInstanceOf[F[T]] /** - * Refine the given value, assuming the constraint holds. + * Refine the given value(s) at runtime. * - * @return a constrained value, without performing constraint checks. - * @see [[assume]], [[apply]], [[applyUnsafe]]. + * @return the given values as [[T]]. + * @throws IllegalArgumentException if the constraint is not satisfied. + * @see [[applyUnsafe]]. */ - inline def assumeAll[F[_]](wrapper: F[A]): F[T] = wrapper.asInstanceOf[F[T]] + inline def applyAllUnsafe[F[_]](wrapper: F[A])(using mapLogic: MapLogic[F]): F[T] = + mapLogic.map(wrapper, applyUnsafe(_)) + + /** + * Refine the given value(s) at runtime, resulting in an [[Either]]. + * + * @return a [[Right]] containing the given values as [[T]] or a [[Left]] containing the constraint message. + * @see [[either]]. + */ + inline def eitherAll[F[_]](wrapper: F[A])(using mapLogic: MapLogic[F]): Either[String, F[T]] = + boundary: + Right(mapLogic.map( + wrapper, + either(_) match + case Right(value) => value + case Left(error) => break(Left(error)) + )) + + /** + * Refine the given value at runtime, resulting in an [[Option]]. + * + * @return an Option containing the refined values as `F[T]` or [[None]]. + * @see [[option]]. + */ + inline def optionAll[F[_]](wrapper: F[A])(using mapLogic: MapLogic[F]): Option[F[T]] = + boundary: + Some(mapLogic.map( + wrapper, + option(_) match + case Some(value) => value + case None => break(None) + )) def unapply(value: T): Option[A :| C] = Some(value.asInstanceOf[A :| C]) diff --git a/main/src/io/github/iltotore/iron/conversion.scala b/main/src/io/github/iltotore/iron/conversion.scala index 7afc639b..b773a2a6 100644 --- a/main/src/io/github/iltotore/iron/conversion.scala +++ b/main/src/io/github/iltotore/iron/conversion.scala @@ -3,6 +3,8 @@ package io.github.iltotore.iron import io.github.iltotore.iron.constraint.collection.ForAll import scala.language.implicitConversions +import scala.util.boundary +import scala.util.boundary.break /** * Implicitly refine at compile-time the given value. @@ -115,13 +117,56 @@ extension [A, C1](value: A :| C1) extension[F[_], A, C1] (wrapper: F[A :| C1]) /** - * Refine the given value again, assuming the constraint holds. + * Refine the given value(s) again, assuming the constraint holds. * - * @return a constrained value, without performing constraint checks. + * @return the constrained values, without performing constraint checks. * @see [[assume]], [[assumeFurther]]. */ inline def assumeAllFurther[C2]: F[A :| (C1 & C2)] = wrapper.asInstanceOf[F[A :| (C1 & C2)]] + /** + * Refine the given value(s) again at runtime. + * + * @param constraint the new constraint to test. + * @return the given values refined with `C1 & C2`. + * @throws IllegalArgumentException if the constraint is not satisfied. + * @see [[refineUnsafe]], [[refineFurtherUnsafe]]. + */ + inline def refineAllFurtherUnsafe[C2](using mapLogic: MapLogic[F], inline constraint: Constraint[A, C2]): F[A :| (C1 & C2)] = + mapLogic.map(wrapper, _.refineFurtherUnsafe[C2]) + + /** + * Refine the given value(s) again at runtime, resulting in an [[Either]]. + * + * @param constraint the new constraint to test. + * @return a [[Right]] containing the given values refined with `C1 & C2` or a [[Left]] containing the constraint message. + * @see [[refineEither]], [[refineAllFurtherEither]]. + */ + inline def refineAllFurtherEither[C2](using mapLogic: MapLogic[F], inline constraint: Constraint[A, C2]): Either[String, F[A :| (C1 & C2)]] = + boundary: + Right(mapLogic.map( + wrapper, + _.refineFurtherEither[C2] match + case Right(value) => value + case Left(error) => break(Left(error)) + )) + + /** + * Refine the given value(s) again at runtime, resulting in an [[Option]]. + * + * @param constraint the new constraint to test. + * @return a [[Option]] containing the given values refined with `C1 & C2` or [[None]]. + * @see [[refineOption]], [[refineFurtherOption]]. + */ + inline def refineAllFurtherOption[C2](using mapLogic: MapLogic[F], inline constraint: Constraint[A, C2]): Option[F[A :| (C1 & C2)]] = + boundary: + Some(mapLogic.map( + wrapper, + _.refineFurtherOption[C2] match + case Some(value) => value + case None => break(None) + )) + extension [A, C1, C2](value: A :| C1 :| C2) /** diff --git a/main/src/io/github/iltotore/iron/package.scala b/main/src/io/github/iltotore/iron/package.scala index bfd6d39c..148f45e7 100644 --- a/main/src/io/github/iltotore/iron/package.scala +++ b/main/src/io/github/iltotore/iron/package.scala @@ -5,7 +5,8 @@ import io.github.iltotore.iron.macros import scala.Console.{CYAN, RESET} import scala.compiletime.{codeOf, error, summonInline} import scala.reflect.TypeTest -import scala.util.NotGiven +import scala.util.{boundary, NotGiven} +import scala.util.boundary.break /** * The main package of Iron. Contains: @@ -107,4 +108,49 @@ extension [F[_], A](wrapper: F[A]) * @return constrained values, without performing constraint checks. * @see [[assume]], [[autoRefine]], [[refineUnsafe]]. */ - inline def assumeAll[B]: F[A :| B] = wrapper \ No newline at end of file + inline def assumeAll[B]: F[A :| B] = wrapper + + /** + * Refine the given value(s) at runtime. + * + * @param constraint the constraint to test with the value to refine. + * @return the given values as [[IronType]]. + * @throws an [[IllegalArgumentException]] if the constraint is not satisfied. + * @see [[refineUnsafe]]. + */ + inline def refineAllUnsafe[B](using mapLogic: MapLogic[F], inline constraint: Constraint[A, B]): F[A :| B] = + mapLogic.map(wrapper, _.refineUnsafe[B]) + + + /** + * Refine the given value(s) at runtime, resulting in an [[Either]]. + * + * @param constraint the constraint to test with the value to refine. + * @return a [[Right]] containing the given values as [[IronType]] or a [[Left]] containing the constraint message. + * @see [[refineEither]]. + */ + inline def refineAllEither[B](using mapLogic: MapLogic[F], inline constraint: Constraint[A, B]): Either[String, F[A :| B]] = + boundary: + Right(mapLogic.map( + wrapper, + _.refineEither[B] match + case Right(value) => value + case Left(error) => break(Left(error)) + )) + + /** + * Refine the given value(s) at runtime, resulting in an [[Option]]. + * + * @param constraint the constraint to test with the value to refine. + * @return a [[Some]] containing the given values as [[IronType]] or [[None]]. + * @see [[refineOption]]. + */ + inline def refineAllOption[B](using mapLogic: MapLogic[F], inline constraint: Constraint[A, B]): Option[F[A :| B]] = + boundary: + Some(mapLogic.map( + wrapper, + _.refineOption[B] match + case Some(value) => value + case None => break(None) + )) + diff --git a/main/test/src/io/github/iltotore/iron/testing/RefinedTypeOpsSuite.scala b/main/test/src/io/github/iltotore/iron/testing/RefinedTypeOpsSuite.scala index fd63f654..3452ccc2 100644 --- a/main/test/src/io/github/iltotore/iron/testing/RefinedTypeOpsSuite.scala +++ b/main/test/src/io/github/iltotore/iron/testing/RefinedTypeOpsSuite.scala @@ -32,6 +32,13 @@ object RefinedTypeOpsSuite extends TestSuite: assert(t2 == Temperature(15.0)) } + test("assume") - assert(Temperature.assume(-15) == -15.0.asInstanceOf[Temperature]) + + test("applyUnsafe") { + test - assertMatch(Try(Temperature.applyUnsafe(-100))) { case Failure(e) if e.getMessage == "Should be strictly positive" => } + test - assert(Temperature.applyUnsafe(100) == Temperature(100)) + } + test("either") { val eitherWithFailingPredicate = Temperature.either(-5.0) assert(eitherWithFailingPredicate == Left("Should be strictly positive")) @@ -46,14 +53,22 @@ object RefinedTypeOpsSuite extends TestSuite: assert(fromWithSucceedingPredicate.contains(Temperature(100))) } - test("applyUnsafe") { - test - assertMatch(Try(Temperature.applyUnsafe(-100))) { case Failure(e) if e.getMessage == "Should be strictly positive" => } - test - assert(Temperature.applyUnsafe(100) == Temperature(100)) + test("assumeAll") - assert(Temperature.assumeAll(List(1, -15)) == List(1, -15).asInstanceOf[List[Temperature]]) + + test("applyAllUnsafe") { + test - assertMatch(Try(Temperature.applyAllUnsafe(List(1, 2, -3)))) { case Failure(e) if e.getMessage == "Should be strictly positive" => } + test - assert(Temperature.applyAllUnsafe(List(1, 2, 3)) == List(Temperature(1), Temperature(2), Temperature(3))) } - test("assume") - assert(Temperature.assume(-15) == -15.0.asInstanceOf[Temperature]) + test("either") { + test - assert(Temperature.eitherAll(List(1, 2, -3)) == Left("Should be strictly positive")) + test - assert(Temperature.eitherAll(List(1, 2, 3)) == Right(List(Temperature(1), Temperature(2), Temperature(3)))) + } - test("assumeAll") - assert(Temperature.assumeAll(List(1, -15)) == List(1, -15).asInstanceOf[List[Temperature]]) + test("option") { + test - assert(Temperature.optionAll(List(1, 2, -3)).isEmpty) + test - assert(Temperature.optionAll(List(1, 2, 3)).contains(List(Temperature(1), Temperature(2), Temperature(3)))) + } test("nonOpaque") { val moisture = Moisture(11)