Skip to content

Commit

Permalink
feat: Add "all" refinement methods
Browse files Browse the repository at this point in the history
  • Loading branch information
Iltotore committed Feb 12, 2024
1 parent c11a669 commit fdbc756
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 21 deletions.
27 changes: 27 additions & 0 deletions main/src/io/github/iltotore/iron/MapLogic.scala
Original file line number Diff line number Diff line change
@@ -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)
64 changes: 52 additions & 12 deletions main/src/io/github/iltotore/iron/RefinedTypeOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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]].
*
Expand All @@ -48,31 +60,59 @@ 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]].
*/
def option(value: A): Option[T] =
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])

Expand Down
49 changes: 47 additions & 2 deletions main/src/io/github/iltotore/iron/conversion.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)

/**
Expand Down
50 changes: 48 additions & 2 deletions main/src/io/github/iltotore/iron/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
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)
))

Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand All @@ -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)
Expand Down

0 comments on commit fdbc756

Please sign in to comment.