Skip to content

Commit

Permalink
feat: Add compile-time support for non-primitive types (#266)
Browse files Browse the repository at this point in the history
Closes #147
  • Loading branch information
Iltotore authored Sep 14, 2024
1 parent 555940c commit a55d33c
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 105 deletions.
8 changes: 4 additions & 4 deletions docs/_docs/reference/constraint.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ import io.github.iltotore.iron.*
//}
given Constraint[Int, Positive] with

override inline def test(value: Int): Boolean = value > 0
override inline def test(inline value: Int): Boolean = value > 0

override inline def message: String = "Should be strictly positive"
```
Expand All @@ -87,10 +87,10 @@ trait PositiveConstraint[A] extends Constraint[A, Positive]:
override inline def message: String = "Should be strictly positive"

given PositiveConstraint[Int] with
override inline def test(value: Int): Boolean = value > 0
override inline def test(inline value: Int): Boolean = value > 0

given PositiveConstraint[Double] with
override inline def test(value: Double): Boolean = value > 0.0
override inline def test(inline value: Double): Boolean = value > 0.0
```

This constraint can now be used like any other:
Expand Down Expand Up @@ -121,7 +121,7 @@ import scala.compiletime.constValue

given [V]: Constraint[Int, Greater[V]] with

override inline def test(value: Int): Boolean = value > constValue[V]
override inline def test(inline value: Int): Boolean = value > constValue[V]

override inline def message: String = "Should be greater than " + stringValue[V]
```
Expand Down
6 changes: 3 additions & 3 deletions main/src/io/github/iltotore/iron/Constraint.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,22 @@ import io.github.iltotore.iron.macros.intersection.*
*/
trait Constraint[A, C]:

inline def test(value: A): Boolean
inline def test(inline value: A): Boolean

inline def message: String

object Constraint:
class UnionConstraint[A, C] extends Constraint[A, C]:

override inline def test(value: A): Boolean = unionCond[A, C](value)
override inline def test(inline value: A): Boolean = unionCond[A, C](value)

override inline def message: String = unionMessage[A, C]

inline given [A, C](using inline u: IsUnion[C]): UnionConstraint[A, C] = new UnionConstraint

class IntersectionConstraint[A, C] extends Constraint[A, C]:

override inline def test(value: A): Boolean = intersectionCond[A, C](value)
override inline def test(inline value: A): Boolean = intersectionCond[A, C](value)

override inline def message: String = intersectionMessage[A, C]

Expand Down
2 changes: 1 addition & 1 deletion main/src/io/github/iltotore/iron/RuntimeConstraint.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import scala.util.NotGiven
* In cases that one does not exist in scope, one will be automatically derived from a [[Constraint]].
*/
final class RuntimeConstraint[A, C](_test: A => Boolean, val message: String):
inline def test(value: A): Boolean = _test(value)
inline def test(inline value: A): Boolean = _test(value)

object RuntimeConstraint:
inline given derived[A, C](using inline c: Constraint[A, C]): RuntimeConstraint[A, C] =
Expand Down
18 changes: 9 additions & 9 deletions main/src/io/github/iltotore/iron/constraint/any.scala
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ object any:

inline given [A]: Constraint[A, True] with

override inline def test(value: A): Boolean = true
override inline def test(inline value: A): Boolean = true

override inline def message: String = "Always valid"

Expand All @@ -101,7 +101,7 @@ object any:

inline given [A]: Constraint[A, False] with

override inline def test(value: A): Boolean = false
override inline def test(inline value: A): Boolean = false

override inline def message: String = "Always invalid"

Expand All @@ -113,7 +113,7 @@ object any:
object DescribedAs:
class DescribedAsConstraint[A, C, Impl <: Constraint[A, C], V <: String](using Impl) extends Constraint[A, DescribedAs[C, V]]:

override inline def test(value: A): Boolean = summonInline[Impl].test(value)
override inline def test(inline value: A): Boolean = summonInline[Impl].test(value)

override inline def message: String = constValue[V]

Expand All @@ -133,7 +133,7 @@ object any:
object Not:
class NotConstraint[A, C, Impl <: Constraint[A, C]](using Impl) extends Constraint[A, Not[C]]:

override inline def test(value: A): Boolean =
override inline def test(inline value: A): Boolean =
!summonInline[Impl].test(value)

override inline def message: String =
Expand All @@ -157,7 +157,7 @@ object any:

class XorConstraint[A, C1, C2, Impl1 <: Constraint[A, C1], Impl2 <: Constraint[A, C2]] extends Constraint[A, Xor[C1, C2]]:

override inline def test(value: A): Boolean = summonInline[Impl1].test(value) != summonInline[Impl2].test(value)
override inline def test(inline value: A): Boolean = summonInline[Impl1].test(value) != summonInline[Impl2].test(value)

override inline def message: String = "(" + summonInline[Impl1].message + " xor " + summonInline[Impl2].message + ")"

Expand Down Expand Up @@ -188,13 +188,13 @@ object any:
override inline def message: String = "Should strictly equal to " + stringValue[V]

inline given [A, V]: StrictEqualConstraint[A, V] with
override inline def test(value: A): Boolean = value == constValue[V]
override inline def test(inline value: A): Boolean = value == constValue[V]

inline given bigDecimalDouble[V <: Float | Double]: StrictEqualConstraint[BigDecimal, V] with
override inline def test(value: BigDecimal): Boolean = value == BigDecimal(doubleValue[V])
override inline def test(inline value: BigDecimal): Boolean = value == BigDecimal(doubleValue[V])

inline given bigDecimalLong[V <: Int | Long]: StrictEqualConstraint[BigDecimal, V] with
override inline def test(value: BigDecimal): Boolean = value == BigDecimal(longValue[V])
override inline def test(inline value: BigDecimal): Boolean = value == BigDecimal(longValue[V])

inline given [V <: Int | Long]: StrictEqualConstraint[BigInt, V] with
override inline def test(value: BigInt): Boolean = value == BigInt(longValue[V])
override inline def test(inline value: BigInt): Boolean = value == BigInt(longValue[V])
10 changes: 5 additions & 5 deletions main/src/io/github/iltotore/iron/constraint/char.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ object char:

inline given Constraint[Char, Whitespace] with

override inline def test(value: Char): Boolean = ${ check('value) }
override inline def test(inline value: Char): Boolean = ${ check('value) }

override inline def message: String = "Should be a whitespace"

Expand All @@ -59,7 +59,7 @@ object char:

inline given Constraint[Char, LowerCase] with

override inline def test(value: Char): Boolean = ${ check('value) }
override inline def test(inline value: Char): Boolean = ${ check('value) }

override inline def message: String = "Should be a lower cased"

Expand All @@ -75,7 +75,7 @@ object char:

inline given Constraint[Char, UpperCase] with

override inline def test(value: Char): Boolean = ${ check('value) }
override inline def test(inline value: Char): Boolean = ${ check('value) }

override inline def message: String = "Should be a upper cased"

Expand All @@ -91,7 +91,7 @@ object char:

inline given Constraint[Char, Digit] with

override inline def test(value: Char): Boolean = ${ check('value) }
override inline def test(inline value: Char): Boolean = ${ check('value) }

override inline def message: String = "Should be a digit"

Expand All @@ -107,7 +107,7 @@ object char:

inline given Constraint[Char, Letter] with

override inline def test(value: Char): Boolean = ${ check('value) }
override inline def test(inline value: Char): Boolean = ${ check('value) }

override inline def message: String = "Should be a letter"

Expand Down
52 changes: 35 additions & 17 deletions main/src/io/github/iltotore/iron/constraint/collection.scala
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ object collection:

class LengthIterable[I <: Iterable[?], C, Impl <: Constraint[Int, C]](using Impl) extends Constraint[I, Length[C]]:

override inline def test(value: I): Boolean = summonInline[Impl].test(value.size)
override inline def test(inline value: I): Boolean = ${ checkIterable('value, '{ summonInline[Impl] }) }

override inline def message: String = "Length: (" + summonInline[Impl].message + ")"

Expand All @@ -105,12 +105,22 @@ object collection:

class LengthString[C, Impl <: Constraint[Int, C]](using Impl) extends Constraint[String, Length[C]]:

override inline def test(value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) }
override inline def test(inline value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) }

override inline def message: String = "Length: (" + summonInline[Impl].message + ")"

inline given lengthString[C, Impl <: Constraint[Int, C]](using inline impl: Impl): LengthString[C, Impl] = new LengthString

private def checkIterable[I <: Iterable[?]: Type, C, Impl <: Constraint[Int, C]](expr: Expr[I], constraintExpr: Expr[Impl])(using
Quotes
): Expr[Boolean] =
val rflUtil = reflectUtil
import rflUtil.*

expr.decode match
case Right(value) => applyConstraint(Expr(value.size), constraintExpr)
case _ => applyConstraint('{ $expr.size }, constraintExpr)

private def checkString[C, Impl <: Constraint[Int, C]](expr: Expr[String], constraintExpr: Expr[Impl])(using Quotes): Expr[Boolean] =
val rflUtil = reflectUtil
import rflUtil.*
Expand All @@ -124,16 +134,24 @@ object collection:
object Contain:
inline given [A, V <: A, I <: Iterable[A]]: Constraint[I, Contain[V]] with

override inline def test(value: I): Boolean = value.iterator.contains(constValue[V])
override inline def test(inline value: I): Boolean = ${ checkIterable('value, '{ constValue[V] }) }

override inline def message: String = "Should contain at most " + stringValue[V] + " elements"
override inline def message: String = "Should contain the value " + stringValue[V]

inline given [V <: String]: Constraint[String, Contain[V]] with

override inline def test(value: String): Boolean = ${ checkString('value, '{ constValue[V] }) }
override inline def test(inline value: String): Boolean = ${ checkString('value, '{ constValue[V] }) }

override inline def message: String = "Should contain the string " + constValue[V]

private def checkIterable[I <: Iterable[?]: Type, V: Type](expr: Expr[I], partExpr: Expr[V])(using Quotes): Expr[Boolean] =
val rflUtil = reflectUtil
import rflUtil.*

(expr.decode, partExpr.decode) match
case (Right(value), Right(part)) => Expr(value.iterator.contains(part))
case _ => '{ ${ expr }.iterator.contains($partExpr) }

private def checkString(expr: Expr[String], partExpr: Expr[String])(using Quotes): Expr[Boolean] =
val rflUtil = reflectUtil
import rflUtil.*
Expand All @@ -146,7 +164,7 @@ object collection:

class ForAllIterable[A, I <: Iterable[A], C, Impl <: Constraint[A, C]](using Impl) extends Constraint[I, ForAll[C]]:

override inline def test(value: I): Boolean = value.forall(summonInline[Impl].test(_))
override inline def test(inline value: I): Boolean = value.forall(summonInline[Impl].test(_))

override inline def message: String = "For each element: (" + summonInline[Impl].message + ")"

Expand All @@ -155,7 +173,7 @@ object collection:

class ForAllString[C, Impl <: Constraint[Char, C]](using Impl) extends Constraint[String, ForAll[C]]:

override inline def test(value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) }
override inline def test(inline value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) }

override inline def message: String = "For each element: (" + summonInline[Impl].message + ")"

Expand Down Expand Up @@ -183,7 +201,7 @@ object collection:

class InitIterable[A, I <: Iterable[A], C, Impl <: Constraint[A, C]](using Impl) extends Constraint[I, Init[C]]:

override inline def test(value: I): Boolean = value.isEmpty || value.init.forall(summonInline[Impl].test(_))
override inline def test(inline value: I): Boolean = value.isEmpty || value.init.forall(summonInline[Impl].test(_))

override inline def message: String = "For each element except head: (" + summonInline[Impl].message + ")"

Expand All @@ -192,7 +210,7 @@ object collection:

class InitString[C, Impl <: Constraint[Char, C]](using Impl) extends Constraint[String, Init[C]]:

override inline def test(value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) }
override inline def test(inline value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) }

override inline def message: String = "For each element except last: (" + summonInline[Impl].message + ")"

Expand Down Expand Up @@ -220,7 +238,7 @@ object collection:

class TailIterable[A, I <: Iterable[A], C, Impl <: Constraint[A, C]](using Impl) extends Constraint[I, Tail[C]]:

override inline def test(value: I): Boolean = value.isEmpty || value.tail.forall(summonInline[Impl].test(_))
override inline def test(inline value: I): Boolean = value.isEmpty || value.tail.forall(summonInline[Impl].test(_))

override inline def message: String = "For each element: (" + summonInline[Impl].message + ")"

Expand All @@ -229,7 +247,7 @@ object collection:

class TailString[C, Impl <: Constraint[Char, C]](using Impl) extends Constraint[String, Tail[C]]:

override inline def test(value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) }
override inline def test(inline value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) }

override inline def message: String = "For each element: (" + summonInline[Impl].message + ")"

Expand All @@ -256,7 +274,7 @@ object collection:

class ExistsIterable[A, I <: Iterable[A], C, Impl <: Constraint[A, C]](using Impl) extends Constraint[I, Exists[C]]:

override inline def test(value: I): Boolean = value.exists(summonInline[Impl].test(_))
override inline def test(inline value: I): Boolean = value.exists(summonInline[Impl].test(_))

override inline def message: String = "At least one: (" + summonInline[Impl].message + ")"

Expand All @@ -265,7 +283,7 @@ object collection:

class ExistsString[C, Impl <: Constraint[Char, C]](using Impl) extends Constraint[String, Exists[C]]:

override inline def test(value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) }
override inline def test(inline value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) }

override inline def message: String = "At least one element: (" + summonInline[Impl].message + ")"

Expand All @@ -288,7 +306,7 @@ object collection:

class HeadIterable[A, I <: Iterable[A], C, Impl <: Constraint[A, C]](using Impl) extends Constraint[I, Head[C]]:

override inline def test(value: I): Boolean = value.headOption.exists(summonInline[Impl].test(_))
override inline def test(inline value: I): Boolean = value.headOption.exists(summonInline[Impl].test(_))

override inline def message: String = "Head: (" + summonInline[Impl].message + ")"

Expand All @@ -297,7 +315,7 @@ object collection:

class HeadString[C, Impl <: Constraint[Char, C]](using Impl) extends Constraint[String, Head[C]]:

override inline def test(value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) }
override inline def test(inline value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) }

override inline def message: String = "Head: (" + summonInline[Impl].message + ")"

Expand All @@ -320,7 +338,7 @@ object collection:

class LastIterable[A, I <: Iterable[A], C, Impl <: Constraint[A, C]](using Impl) extends Constraint[I, Last[C]]:

override inline def test(value: I): Boolean = value.lastOption.exists(summonInline[Impl].test(_))
override inline def test(inline value: I): Boolean = value.lastOption.exists(summonInline[Impl].test(_))

override inline def message: String = "Last: (" + summonInline[Impl].message + ")"

Expand All @@ -329,7 +347,7 @@ object collection:

class LastString[C, Impl <: Constraint[Char, C]](using Impl) extends Constraint[String, Last[C]]:

override inline def test(value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) }
override inline def test(inline value: String): Boolean = ${ checkString('value, '{ summonInline[Impl] }) }

override inline def message: String = "Last: (" + summonInline[Impl].message + ")"

Expand Down
Loading

0 comments on commit a55d33c

Please sign in to comment.