Skip to content

Commit

Permalink
feat: Add first order aliases for Cats' EitherNel/Nec and ValidatedNe…
Browse files Browse the repository at this point in the history
…l/Nec
  • Loading branch information
Iltotore committed May 4, 2024
1 parent 074122f commit 7477c1d
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 19 deletions.
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)
7 changes: 2 additions & 5 deletions zio/test/src/io/github/iltotore/iron/RefinedOpsTypes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]

0 comments on commit 7477c1d

Please sign in to comment.