Skip to content

Commit

Permalink
feat: Add common string constraints (#92)
Browse files Browse the repository at this point in the history
  • Loading branch information
Iltotore authored Dec 29, 2022
1 parent b5aed9d commit d600c10
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 37 deletions.
9 changes: 8 additions & 1 deletion main/src/io/github/iltotore/iron/constraint/collection.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package io.github.iltotore.iron.constraint

import io.github.iltotore.iron.{:|, ==>, Constraint, Implication}
import io.github.iltotore.iron.compileTime.*
import io.github.iltotore.iron.constraint.any.DescribedAs
import io.github.iltotore.iron.constraint.any.{DescribedAs, StrictEqual}
import io.github.iltotore.iron.constraint.numeric.{GreaterEqual, LessEqual}

import scala.compiletime.{constValue, summonInline}
Expand Down Expand Up @@ -36,6 +36,11 @@ object collection:
*/
type MaxLength[V <: Int] = Length[LessEqual[V]] DescribedAs "Should have a maximum length of" + V

/**
* Tests if the input is empty.
*/
type Empty = Length[StrictEqual[0]] DescribedAs "Should be empty"

/**
* Tests if the given collection contains a specific value.
*
Expand Down Expand Up @@ -108,6 +113,8 @@ object collection:
case Some(value) => applyConstraint(Expr(value.length), constraintExpr)

case None => applyConstraint('{$expr.length}, constraintExpr)

given [C1, C2](using C1 ==> C2): (Length[C1] ==> Length[C2]) = Implication()

object Contain:
inline given [A, V <: A, I <: Iterable[A]]: Constraint[I, Contain[V]] with
Expand Down
58 changes: 55 additions & 3 deletions main/src/io/github/iltotore/iron/constraint/string.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.github.iltotore.iron.constraint

import io.github.iltotore.iron.Constraint
import io.github.iltotore.iron.{==>, Constraint, Implication}
import io.github.iltotore.iron.constraint.any.*
import io.github.iltotore.iron.constraint.collection.*
import io.github.iltotore.iron.compileTime.*
Expand All @@ -18,9 +18,17 @@ object string:

/**
* Tests if the input only contains whitespaces.
* @see [[Whitespace]]
*/
type Blank = ForAll[Whitespace] DescribedAs "Should only contain whitespaces"

/**
* Tests if the input does not have leading or trailing whitespaces.
* @see [[Whitespace]]
*/
type Trimmed = (Empty | Not[Head[Whitespace] | Last[Whitespace]]) DescribedAs
"Should not have leading or trailing whitespaces"

/**
* Tests if all letters of the input are lower cased.
*/
Expand All @@ -36,6 +44,19 @@ object string:
*/
type Alphanumeric = ForAll[Digit | Letter] DescribedAs "Should be alphanumeric"

/**
* Tests if the input starts with the given prefix.
* @tparam V the string to compare with the start of the input.
*/
final class StartWith[V <: String]

/**
* Tests if the input ends with the given suffix.
*
* @tparam V the string to compare with the end of the input.
*/
final class EndWith[V <: String]

/**
* Tests if the input matches the given regex.
*
Expand All @@ -48,18 +69,49 @@ object string:
*
* @note it only checks if the input fits the URL pattern. Not if the given URL exists/is accessible.
*/
type URLLike =
type ValidURL =
Match[
"((\\w+:)+\\/\\/)?(([-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6})|(localhost))(:\\d{1,5})?(\\/|\\/([-a-zA-Z0-9@:%_\\+.~#?&//=]*))?"
] DescribedAs "Should be an URL"

/**
* Tests if the input is a valid UUID.
*/
type UUIDLike =
type ValidUUID =
Match["^([0-9a-fA-F]{8}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{12})"] DescribedAs "Should be an UUID"

object Blank:

given (Empty ==> Blank) = Implication()

object StartWith:

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

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

override inline def message: String = "Should start with " + stringValue[V]

private def check(expr: Expr[String], prefixExpr: Expr[String])(using Quotes): Expr[Boolean] =
(expr.value, prefixExpr.value) match
case (Some(value), Some(prefix)) => Expr(value.startsWith(prefix))
case _ => '{$expr.startsWith($prefixExpr)}

object EndWith:

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

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

override inline def message: String = "Should end with " + stringValue[V]

private def check(expr: Expr[String], prefixExpr: Expr[String])(using Quotes): Expr[Boolean] =
(expr.value, prefixExpr.value) match
case (Some(value), Some(prefix)) => Expr(value.endsWith(prefix))
case _ => '{ $expr.endsWith($prefixExpr) }

object Match:

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

override inline def test(value: String): Boolean = ${ check('value, '{ constValue[V] }) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,51 @@ object CollectionSuite extends TestSuite:
}

test("minLength") {
test - List(1, 2, 3, 4).assertRefine[MinLength[4]]
test - List(1, 2, 3).assertNotRefine[MinLength[4]]
test("iterable") {
test - List(1, 2, 3, 4).assertRefine[MinLength[4]]
test - List(1, 2, 3).assertNotRefine[MinLength[4]]
}

test("string") {
test - "abc".assertNotRefine[MinLength[4]]
test - "abcd".assertRefine[MinLength[4]]
}
}

test("maxLength") {
test - List(1, 2, 3).assertRefine[MaxLength[3]]
test - List(1, 2, 3, 4).assertNotRefine[MaxLength[3]]
test("iterable") {
test - List(1, 2, 3).assertRefine[MaxLength[3]]
test - List(1, 2, 3, 4).assertNotRefine[MaxLength[3]]
}

test("string") {
test - "abc".assertRefine[MaxLength[3]]
test - "abcd".assertNotRefine[MaxLength[3]]
}
}

test("empty") {
test("iterable") {
test - Nil.assertRefine[Empty]
test - List(1, 2, 3).assertNotRefine[Empty]
}

test("string") {
test - "".assertRefine[Empty]
test - "abc".assertNotRefine[Empty]
}
}

test("contains") {
test - List(1, 2, 3).assertRefine[Contain[3]]
test - List(1, 2, 4).assertNotRefine[Contain[3]]
test("contain") {
test("iterable") {
test - List(1, 2, 3).assertRefine[Contain[3]]
test - List(1, 2, 4).assertNotRefine[Contain[3]]
}

test("string") {
test - "abc".assertRefine[Contain["c"]]
test - "abd".assertNotRefine[Contain["c"]]
}
}

test("forAll") {
Expand Down
72 changes: 46 additions & 26 deletions main/test/src/io/github/iltotore/iron/testing/StringSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,18 @@ object StringSuite extends TestSuite:

val tests: Tests = Tests {

test("minLength") {
test - "abc".assertNotRefine[MinLength[4]]
test - "abcd".assertRefine[MinLength[4]]
test("blank") {
test - "".assertRefine[Blank]
test - " \t\n\u000B\f\r\u001C\u001D\u001E\u001F".assertRefine[Blank]
test - "a".assertNotRefine[Blank]
}

test("maxLength") {
test - "abc".assertRefine[MaxLength[3]]
test - "abcd".assertNotRefine[MaxLength[3]]
}

test("contains") {
test - "abc".assertRefine[Contain["c"]]
test - "abd".assertNotRefine[Contain["c"]]
test("trimmed") {
test - "".assertRefine[Trimmed]
test - "abc".assertRefine[Trimmed]
test - " ".assertNotRefine[Trimmed]
test - " abc ".assertNotRefine[Trimmed]
test - "abc\n".assertNotRefine[Trimmed]
}

test("lowercase") {
Expand All @@ -33,28 +32,49 @@ object StringSuite extends TestSuite:
test - "ABC 123 \n".assertRefine[LettersUpperCase]
}

test("alphanumeric") {
test - "abc".assertRefine[Alphanumeric]
test - "123".assertRefine[Alphanumeric]
test - "abc123".assertRefine[Alphanumeric]
test - "".assertRefine[Alphanumeric]
test - "abc123_".assertNotRefine[Alphanumeric]
test - " ".assertNotRefine[Alphanumeric]
}

test("startWith") {
test - "abc".assertRefine[StartWith["abc"]]
test - "abc123".assertRefine[StartWith["abc"]]
test - "ab".assertNotRefine[StartWith["abc"]]
}

test("endWith") {
test - "abc".assertRefine[EndWith["abc"]]
test - "123abc".assertRefine[EndWith["abc"]]
test - "ab".assertNotRefine[EndWith["abc"]]
}

test("match") {
test - "998".assertRefine[Match["[0-9]+"]]
test - "abc".assertNotRefine[Match["[0-9]+"]]
test - "".assertNotRefine[Match["[0-9]+"]]
}

test("url") {
test - "localhost".assertRefine[URLLike]
test - "localhost:8080".assertRefine[URLLike]
test - "example.com".assertRefine[URLLike]
test - "example.com:8080".assertRefine[URLLike]
test - "http://example.com/".assertRefine[URLLike]
test - "https://example.com/".assertRefine[URLLike]
test - "file://example.com/".assertRefine[URLLike]
test - "mysql:jdbc://example.com/".assertRefine[URLLike]
test - "http://example.com/index.html".assertRefine[URLLike]
test - "http://example.com/#section".assertRefine[URLLike]
test - "http://example.com/?q=with%20space".assertRefine[URLLike]
test - "http://example.com/?q=with+space".assertRefine[URLLike]
test - "/example.com".assertNotRefine[URLLike]
test - "://example.com".assertNotRefine[URLLike]
test - "http:///".assertNotRefine[URLLike]
test - "localhost".assertRefine[ValidURL]
test - "localhost:8080".assertRefine[ValidURL]
test - "example.com".assertRefine[ValidURL]
test - "example.com:8080".assertRefine[ValidURL]
test - "http://example.com/".assertRefine[ValidURL]
test - "https://example.com/".assertRefine[ValidURL]
test - "file://example.com/".assertRefine[ValidURL]
test - "mysql:jdbc://example.com/".assertRefine[ValidURL]
test - "http://example.com/index.html".assertRefine[ValidURL]
test - "http://example.com/#section".assertRefine[ValidURL]
test - "http://example.com/?q=with%20space".assertRefine[ValidURL]
test - "http://example.com/?q=with+space".assertRefine[ValidURL]
test - "/example.com".assertNotRefine[ValidURL]
test - "://example.com".assertNotRefine[ValidURL]
test - "http:///".assertNotRefine[ValidURL]
}

}

0 comments on commit d600c10

Please sign in to comment.