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

feat: Add first order aliases for Cats and ZIO #235

Merged
merged 2 commits into from
May 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
155 changes: 154 additions & 1 deletion cats/src/io/github/iltotore/iron/cats.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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)

/**
Expand Down Expand Up @@ -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])

/**
Expand Down Expand Up @@ -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.
*/
Expand Down
119 changes: 108 additions & 11 deletions cats/test/src/io/github/iltotore/iron/CatsSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -90,31 +90,31 @@ 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'")

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'")

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'")

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"
Expand All @@ -125,15 +125,112 @@ 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"
)
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")
)))


}
4 changes: 2 additions & 2 deletions cats/test/src/io/github/iltotore/iron/RefinedOpsTypes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]
3 changes: 3 additions & 0 deletions main/src/io/github/iltotore/iron/InvalidValue.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.github.iltotore.iron

case class InvalidValue[A](value: A, message: String)
Loading