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(string): add a splitAt method #1141

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ The library comes with these predefined predicates:
* `XPath`: checks if a `String` is a valid XPath expression
* `Trimmed`: checks if a `String` has no leading or trailing whitespace
* `HexStringSpec`: checks if a `String` represents a hexadecimal number
* `SplitAt`: split a string at an Index `N`, and then check the conjunction of the predicates `A` on the first part of the string and `B` on its second part

## Contributors and participation

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ object string extends StringInference {
/** Predicate that checks if a `String` represents a hexadecimal number. */
type HexStringSpec = MatchesRegex["""^(([0-9a-f]+)|([0-9A-F]+))$"""]

/** Predicate that split a `String` and check the conjunction of the predicates `A` and `B` */
final case class SplitAt[N, A, B](n: N, a: A, b: B)

object EndsWith {
implicit def endsWithValidate[S <: String](implicit
ws: ValueOf[S]
Expand Down Expand Up @@ -240,6 +243,54 @@ object string extends StringInference {
implicit def trimmedValidate: Validate.Plain[String, Trimmed] =
Validate.fromPredicate(s => s.trim == s, t => s"$t is trimmed", Trimmed())
}

object SplitAt {
implicit def splitAtValidate[N <: Int, A, RA, B, RB](
implicit
wn: Witness.Aux[N],
va: Validate.Aux[String, A, RA],
vb: Validate.Aux[String, B, RB]
)

: Validate.Aux[String, SplitAt[N, A, B], SplitAt[N, Option[va.Res], Option[vb.Res]]] = new Validate[String, SplitAt[N, A, B]] {

override type R = SplitAt[N, Option[va.Res], Option[vb.Res]]

override def validate(s: String): Res = {
try {
val (ra, rb) = (va.validate(s.substring(0, wn.value.toInt)), vb.validate(s.substring(wn.value.toInt)))
Result.fromBoolean(ra.isPassed && rb.isPassed, SplitAt(wn.value, Some(ra), Some(rb)))
} catch {
case _: StringIndexOutOfBoundsException => Failed(SplitAt(wn.value, None, None))
case NonFatal(_) => Failed(SplitAt(wn.value, None, None))
}
}

override def showExpr(s: String): String =
s"splitAt(${wn.value.toInt}, ${va.showExpr(s)} && ${vb.showExpr(s)})"

override def showResult(s: String, r: Res): String = {
val expr = showExpr(s)
val (ra, rb) = (r.detail.a, r.detail.b)
(ra, rb) match {
case (None, None) =>
Resources.showResultInputFailed(expr, s"${wn.value.toInt} should be between zero and this string length")
case (Some(_), None) =>
Resources.showResultInputFailed(expr, s"${wn.value.toInt} should be between zero and this string length")
case (None, Some(_)) =>
Resources.showResultInputFailed(expr, s"${wn.value.toInt} should be between zero and this string length")
case (Some(Passed(_)), Some(Passed(_))) =>
Resources.showResultAndBothPassed(expr)
case (Some(Passed(_)), Some(Failed(_))) =>
Resources.showResultAndRightFailed(expr, vb.showResult(s, rb.get))
case (Some(Failed(_)), Some(Passed(_))) =>
Resources.showResultAndLeftFailed(expr, va.showResult(s, ra.get))
case (Some(Failed(_)), Some(Failed(_))) =>
Resources.showResultAndBothFailed(expr, va.showResult(s, ra.get), vb.showResult(s, rb.get))
}
}
}
}
}

private[refined] trait StringInference {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package eu.timepit.refined

import eu.timepit.refined.api.{Inference, Validate}
import eu.timepit.refined.api.{Failed, Inference, Passed, Result, Validate}
import eu.timepit.refined.api.Inference.==>
import eu.timepit.refined.collection.NonEmpty
import eu.timepit.refined.collection.{NonEmpty}
import eu.timepit.refined.internal.Resources
import eu.timepit.refined.string._
import shapeless.Witness
import scala.util.control.NonFatal

/**
* Module for `String` related predicates. Note that most of the predicates
Expand Down Expand Up @@ -73,6 +75,9 @@ object string extends StringInference {
/** Predicate that checks if a `String` represents a hexadecimal number. */
type HexStringSpec = MatchesRegex[W.`"""^(([0-9a-f]+)|([0-9A-F]+))$"""`.T]

/** Predicate that split a `String` and check the conjunction of the predicates `A` and `B` */
final case class SplitAt[N, A, B](n: N, a: A, b: B)

object EndsWith {
implicit def endsWithValidate[S <: String](implicit
ws: Witness.Aux[S]
Expand Down Expand Up @@ -241,6 +246,55 @@ object string extends StringInference {
implicit def trimmedValidate: Validate.Plain[String, Trimmed] =
Validate.fromPredicate(s => s.trim == s, t => s"$t is trimmed", Trimmed())
}

object SplitAt {
implicit def splitAtValidate[N <: Int, A, RA, B, RB](
implicit
wn: Witness.Aux[N],
va: Validate.Aux[String, A, RA],
vb: Validate.Aux[String, B, RB]
)

: Validate.Aux[String, SplitAt[N, A, B], SplitAt[N, Option[va.Res], Option[vb.Res]]] = new Validate[String, SplitAt[N, A, B]] {

override type R = SplitAt[N, Option[va.Res], Option[vb.Res]]

override def validate(s: String): Res = {
try {
val (ra, rb) = (va.validate(s.substring(0, wn.value.toInt)), vb.validate(s.substring(wn.value.toInt)))
Result.fromBoolean(ra.isPassed && rb.isPassed, SplitAt(wn.value, Some(ra), Some(rb)))
} catch {
case NonFatal(_) => Failed(SplitAt(wn.value, None, None))
case _: Throwable =>
Failed(SplitAt(wn.value, None, None))
}
}

override def showExpr(s: String): String =
s"splitAt(${wn.value.toInt}, ${va.showExpr(s)} && ${vb.showExpr(s)})"

override def showResult(s: String, r: Res): String = {
val expr = showExpr(s)
val (ra, rb) = (r.detail.a, r.detail.b)
(ra, rb) match {
case (None, None) =>
Resources.showResultInputFailed(expr, s"${wn.value.toInt} should be between zero and this string length")
case (Some(_), None) =>
Resources.showResultInputFailed(expr, s"${wn.value.toInt} should be between zero and this string length")
case (None, Some(_)) =>
Resources.showResultInputFailed(expr, s"${wn.value.toInt} should be between zero and this string length")
case (Some(Passed(_)), Some(Passed(_))) =>
Resources.showResultAndBothPassed(expr)
case (Some(Passed(_)), Some(Failed(_))) =>
Resources.showResultAndRightFailed(expr, vb.showResult(s, rb.get))
case (Some(Failed(_)), Some(Passed(_))) =>
Resources.showResultAndLeftFailed(expr, va.showResult(s, ra.get))
case (Some(Failed(_)), Some(Failed(_))) =>
Resources.showResultAndBothFailed(expr, va.showResult(s, ra.get), vb.showResult(s, rb.get))
}
}
}
}
}

private[refined] trait StringInference {
Expand Down Expand Up @@ -294,4 +348,10 @@ private[refined] trait StringInference {

implicit def xPathNonEmptyInference: XPath ==> NonEmpty =
Inference.alwaysValid("xPathNonEmptyInference")

implicit def splitAtNonEmptyInference[N <: Int, A, B](implicit
wn: Witness.Aux[N]
): SplitAt[N, A, B] ==> NonEmpty =
Inference(wn.value.toInt > 0, s"splitAtNonEmptyInference(${wn.value})")

}
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ object Resources {
def showResultOrBothFailed(expr: String, left: String, right: String): String =
s"$Both $predicates of $expr $failed. $Left: $left $Right: $right"

def showResultInputFailed(expr: String, input: String): String =
s"$expr $failed: input $input"

//

val refineNonCompileTimeConstant =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,12 @@ class StringInferenceSpec extends Properties("StringInference") {
Inference[ValidBigDecimal, NonEmpty].isValid
}

property("SplitAt =!> NonEmpty") = secure {
Inference[SplitAt[W.`1`.T, IPv4, Uuid], NonEmpty].isValid
}

property("SplitAt =!> NonEmpty") = secure {
Inference[SplitAt[W.`-1`.T, IPv4, Uuid], NonEmpty].notValid
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import org.scalacheck.{Arbitrary, Properties}
import org.scalacheck.Prop._
import shapeless.Witness



class StringValidateSpec extends Properties("StringValidate") {

property("EndsWith.isValid") = secure {
Expand Down Expand Up @@ -143,4 +145,39 @@ class StringValidateSpec extends Properties("StringValidate") {
validNumber[Double, ValidDouble]("ValidDouble", "a")
validNumber[BigInt, ValidBigInt]("ValidBigInt", "1.0")
validNumber[BigDecimal, ValidBigDecimal]("ValidBigDecimal", "a")

property("SplitAt.isValid") = secure {
val ipv4 = "10.0.0.1"
val uuid = "9ecce884-47fe-4ba4-a1bb-1a3d71ed6530"
val length = Witness(8)
val s = s"$ipv4$uuid"
isValid[SplitAt[length.T, IPv4, Uuid]](s) ?= s.startsWith(ipv4) && s.endsWith(uuid)
}

property("SplitAt.showResult.example.Failed") = secure {
val ipv4 = "whops"
val uuid = "9ecce884-47fe-4ba4-a1bb-1a3d71ed6530"
val length = Witness(5)
val s = s"$ipv4$uuid"
showResult[SplitAt[length.T, IPv4, Uuid]](s) ?=
s"""Left predicate of splitAt(${length.value.toInt}, $s is a valid IPv4 && isValidUuid(\"$s\")) failed: Predicate failed: $s is a valid IPv4."""
}

property("SplitAt.showResult.length.negative.Failed") = secure {
val ipv4 = "whops"
val uuid = "9ecce884-47fe-4ba4-a1bb-1a3d71ed6530"
val length = Witness(-1)
val s = s"$ipv4$uuid"
showResult[SplitAt[length.T, IPv4, Uuid]](s) ?=
s"""splitAt(${length.value.toInt}, $s is a valid IPv4 && isValidUuid(\"$s\")) failed: input ${length.value.toInt} should be between zero and this string length"""
}

property("SplitAt.showResult.length.big.Failed") = secure {
val ipv4 = "whops"
val uuid = "9ecce884-47fe-4ba4-a1bb-1a3d71ed6530"
val length = Witness(1000)
val s = s"$ipv4$uuid"
showResult[SplitAt[length.T, IPv4, Uuid]](s) ?=
s"""splitAt(${length.value.toInt}, $s is a valid IPv4 && isValidUuid(\"$s\")) failed: input ${length.value.toInt} should be between zero and this string length"""
}
}