Skip to content

Commit

Permalink
feat: Add companion for refined new types (#118)
Browse files Browse the repository at this point in the history
  • Loading branch information
antonkw authored May 21, 2023
1 parent 9465c5b commit 760c30c
Show file tree
Hide file tree
Showing 10 changed files with 393 additions and 8 deletions.
6 changes: 6 additions & 0 deletions build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,12 @@ object zio extends SubModule {

object js extends JSCrossModule

object test extends Tests {
def ivyDeps = Agg(
ivy"com.lihaoyi::utest:0.8.1"
)
}

}

object zioJson extends SubModule {
Expand Down
59 changes: 54 additions & 5 deletions cats/src/io/github/iltotore/iron/cats.scala
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,58 @@ 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 [A, C, T](ops: RefinedTypeOpsImpl[A, C, T])

/**
* Refine the given value at runtime, resulting in an [[EitherNec]].
*
* @param constraint the constraint to test with the value to refine.
* @return a [[Right]] containing this value as [[IronType]] or a [[Left]] containing the constraint message.
* @see [[either]], [[eitherNel]].
*/
inline def eitherNec(value: A)(using inline c: Constraint[A, C]): EitherNec[String, T] = value.refineNec[C].map(_.asInstanceOf[T])

/**
* Represent all Cats' typeclass instances for Iron.
*/
/**
* Refine the given value at runtime, resulting in an [[EitherNel]].
*
* @param constraint the constraint to test with the value to refine.
* @return a [[Right]] containing this value as [[IronType]] or a [[Left]] containing the constraint message.
* @see [[either]], [[eitherNec]].
*/
inline def eitherNel(value: A)(using inline c: Constraint[A, C]): EitherNel[String, T] = value.refineNel[C].map(_.asInstanceOf[T])

/**
* Refine the given value at runtime, resulting in a [[Validated]].
*
* @param constraint the constraint to test with the value to refine.
* @return a [[Valid]] containing this value as [[IronType]] or an [[Invalid]] containing the constraint message.
* @see [[validatedNec]], [[validatedNel]].
*/
inline def validated(value: A)(using inline c: Constraint[A, C]): Validated[String, T] = value.refineValidated[C].map(_.asInstanceOf[T])

/**
* Refine the given value 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 [[validated]], [[validatedNel]].
*/
inline def validatedNec(value: A)(using inline c: Constraint[A, C]): ValidatedNec[String, T] =
value.refineValidatedNec[C].map(_.asInstanceOf[T])

/**
* Refine the given value 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 [[validated]], [[validatedNec]].
*/
inline def validatedNel(value: A)(using inline c: Constraint[A, C]): ValidatedNel[String, T] =
value.refineValidatedNel[C].map(_.asInstanceOf[T])

/**
* Represent all Cats' typeclass instances for Iron.
*/
private trait IronCatsInstances extends IronCatsLowPriority:
inline given [A, C](using inline ev: Eq[A]): Eq[A :| C] = ev.asInstanceOf[Eq[A :| C]]

Expand All @@ -130,9 +177,11 @@ private trait IronCatsInstances extends IronCatsLowPriority:

inline given [A, C](using inline ev: Show[A]): Show[A :| C] = ev.asInstanceOf[Show[A :| C]]

inline given [A, C, V](using inline ev: LowerBounded[A], implication: C ==> Greater[V]): LowerBounded[A :| C] = ev.asInstanceOf[LowerBounded[A :| C]]
inline given [A, C, V](using inline ev: LowerBounded[A], implication: C ==> Greater[V]): LowerBounded[A :| C] =
ev.asInstanceOf[LowerBounded[A :| C]]

inline given [A, C, V](using inline ev: UpperBounded[A], implication: C ==> Greater[V]): UpperBounded[A :| C] = ev.asInstanceOf[UpperBounded[A :| C]]
inline given [A, C, V](using inline ev: UpperBounded[A], implication: C ==> Greater[V]): UpperBounded[A :| C] =
ev.asInstanceOf[UpperBounded[A :| C]]

private def posMonoid[A, C](using ev: CommutativeMonoid[A], shift: PosShift[A], implication: C ==> Positive): CommutativeMonoid[A :| C] =
new CommutativeMonoid[A :| C]:
Expand Down
62 changes: 60 additions & 2 deletions cats/test/src/io/github/iltotore/iron/CatsSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@ 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.constraint.all.*

import utest.{Show as _, *}
import _root_.cats.data.NonEmptyChain
import _root_.cats.data.NonEmptyList
import _root_.cats.data.Validated.{Valid, Invalid}
import _root_.cats.data.ValidatedNec

import scala.runtime.stdLibPatches.Predef.assert

opaque type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps[Temperature]

object CatsSuite extends TestSuite:

Expand Down Expand Up @@ -80,4 +87,55 @@ object CatsSuite extends TestSuite:
}
}
}

test("eitherNec") {
import io.github.iltotore.iron.cats.*

val eitherNecWithFailingPredicate = Temperature.eitherNec(-5.0)
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)
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)
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)
assert(
validatedNecWithFailingPredicate == Invalid(NonEmptyChain.one("Should be strictly positive")),
"'validatedNec' returns left if predicate fails"
)
val validatedNecWithSucceedingPredicate = Temperature.validatedNec(100)
assert(validatedNecWithSucceedingPredicate == Valid(Temperature(100)), "valid should contain result of 'apply'")
}

test("validatedNel") {
import io.github.iltotore.iron.cats.*

val validatedNelWithFailingPredicate = Temperature.validatedNel(-5.0)
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'")
}
}
36 changes: 36 additions & 0 deletions docs/_docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,42 @@ import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.all.*
```

### Companion object
`RefinedTypeOps` create convenient companion object.

```scala
opaque type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps[Temperature]
```

#### apply function for compile-time validation
```scala
val temperature = Temperature(100) //temperature is of Temperature type
```
`Temperature(-100)` won't compile.

#### fromIronType to wrap already refined values

`fromIronType` helps to manage values which already were checked against constrains.

Implication works here; hence, both `Double :| Greater[10]` and `Double :| Positive` could be `Temperature`
```scala
val x: Double :| Positive = 5.0
val y: Double :| Greater[10] = 15.0
val t1 = Temperature.fromIronType(x)
val t2 = Temperature.fromIronType(y)
```

#### Option/Either
`Temperature.either(-5.0)` and `Temperature.option(-5.0)` return `Either` and `Option`.

#### applyUnsafe
`applyUnsafe` throws `IllegalArgumentException` exception if predicate fails.
`Temperature.applyUnsafe(-1)` is example of usage.

#### assume
`Temperature.assume(x)` doesn't fail (and returns `Temperature`). No validations being performed. Consider it as unsafe cast.

## Next steps

You can find the list of all standard constraints in the [[constraint package summary|io.github.iltotore.iron.constraint]].
Expand Down
21 changes: 21 additions & 0 deletions docs/_docs/modules/cats.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,24 @@ name1.show // Martin
name1 |+| name2 // MartinGeorge
age1 === 49 // false
```

## Companion object (RefinedTypeOps extensions)

Companion object created with `RefinedTypeOps` is being extended by set of functions.

### Companion object
```scala
opaque type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps[Temperature]
```

### Imports
`import io.github.iltotore.iron.cats.*`

### functions
All the example return cats structures with either result or error report.
- `Temperature.eitherNec(-5.0)`
- `Temperature.eitherNel(-5.0)`
- `Temperature.validated(-5.0)`
- `Temperature.validatedNec(-5.0)`
- `Temperature.validatedNel(-5.0)`
20 changes: 20 additions & 0 deletions docs/_docs/modules/zio.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,24 @@ def createUser(name: String, age: Int): Validation[String, User] =
createUser("Iltotore", 18) //Success(Chunk(),User(Iltotore,18))
createUser("Il_totore", 18) //Failure(Chunk(),NonEmptyChunk(Username should be alphanumeric))
createUser("Il_totore", -18) //Failure(Chunk(),NonEmptyChunk(Username should be alphanumeric, Age should be positive))
```

## Companion object (RefinedTypeOps validation extension)

Companion object created with `RefinedTypeOps` is being extended by set of functions.

### Companion object
```scala
opaque type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps[Temperature]
```

### Imports
`import io.github.iltotore.iron.zio.validation`

### validation
The example below returns `ZValidation.Success` or `ZValidation.Failure`.

```scala
Temperature.validation(x)
```
69 changes: 69 additions & 0 deletions main/src/io/github/iltotore/iron/RefinedTypeOps.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package io.github.iltotore.iron

type RefinedTypeOps[T] = T match
case IronType[a, c] => RefinedTypeOpsImpl[a, c, T]

class RefinedTypeOpsImpl[A, C, T]:
/**
* Implicitly refine at compile-time the given value.
*
* @param value the value to refine.
* @param constraint the implementation of `C` to check.
* @tparam A the refined type.
* @tparam C the constraint applied to the type.
* @return the given value typed as [[IronType]]
* @note This method ensures that the value satisfies the constraint. If it doesn't or isn't evaluable at compile-time, the compilation is aborted.
*/
inline def apply(value: A)(using Constraint[A, C]): T =
autoRefine[A, C](value).asInstanceOf[T]

/**
* Refine the given value at runtime, assuming the constraint holds.
*
* @return a constrained value, without performing constraint checks.
* @see [[apply]], [[applyUnsafe]].
*/
inline def assume(value: A): T = value.assume[C].asInstanceOf[T]

extension [A, C, T](ops: RefinedTypeOpsImpl[A, C, T])

/**
* Refine the given value at runtime, resulting in an [[Either]].
*
* @param constraint the constraint to test with the value to refine.
* @return a [[Right]] containing this value as [[T]] or a [[Left]] containing the constraint message.
* @see [[fromIronType]], [[option]], [[applyUnsafe]].
*/
inline def either(value: A)(using constraint: Constraint[A, C]): Either[String, T] =
Either.cond(constraint.test(value), value.asInstanceOf[T], constraint.message)

/**
* 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]].
*/
inline def option(value: A)(using constraint: Constraint[A, C]): Option[T] =
Option.when(constraint.test(value))(value.asInstanceOf[T])

/**
* Refine the given value at runtime, resulting in an [[Option]].
*
* @param implication the constraint (with possible implication) to test with the value to refine.
* @return an Option containing this value as [[T]] or [[None]].
* @see [[fromIronType]], [[either]], [[applyUnsafe]].
*/
inline def fromIronType[C1](value: IronType[A, C1])(using Implication[C1, C]): T =
value.asInstanceOf[T]

/**
* Refine the given value at runtime.
*
* @param constraint the constraint to test with the value to refine.
* @return this value as [[T]].
* @throws an [[IllegalArgumentException]] if the constraint is not satisfied.
* @see [[fromIronType]], [[either]], [[option]].
*/
inline def applyUnsafe(value: A)(using Constraint[A, C]): T =
value.refine[C].asInstanceOf[T]
Loading

0 comments on commit 760c30c

Please sign in to comment.