diff --git a/core-v1/src/main/scala/algebra/Expr.scala b/core-v1/src/main/scala/algebra/Expr.scala index 4fea8e10..f96d089b 100644 --- a/core-v1/src/main/scala/algebra/Expr.scala +++ b/core-v1/src/main/scala/algebra/Expr.scala @@ -14,7 +14,7 @@ import cats.{Applicative, FlatMap, Foldable, Functor, SemigroupK, Traverse} import shapeless.{::, HList, HNil, Typeable} import scala.annotation.nowarn -import scala.reflect.ClassTag +import scala.util.matching.Regex /** * The root trait of all expression nodes. @@ -346,6 +346,8 @@ object Expr { opO: OP[W[B]], ): I ~:> W[B] + def visitRegexMatches[I, S, O : OP](expr: RegexMatches[I, S, O, OP]): I ~:> O + def visitRepeat[I, O](expr: Repeat[I, O, OP])(implicit opO: OP[IterableOnce[O]]): I ~:> IterableOnce[O] def visitSelect[I, A, B, O : OP](expr: Select[I, A, B, O, OP]): I ~:> O @@ -534,6 +536,9 @@ object Expr { opO: OP[W[B]], ): H[I, W[B]] = proxy(underlying.visitOr(expr)) + override def visitRegexMatches[I, S, O : OP](expr: RegexMatches[I, S, O, OP]): H[I, O] = + proxy(underlying.visitRegexMatches(expr)) + override def visitRepeat[I, O](expr: Repeat[I, O, OP])(implicit opO: OP[IterableOnce[O]]): H[I, IterableOnce[O]] = proxy(underlying.visitRepeat(expr)) @@ -1321,12 +1326,35 @@ object Expr { )(implicit ev: S <:< I, opO: OP[Option[O]], - ) extends Expr[I, Option[O], OP]("matching") { + ) extends Expr[I, Option[O], OP]("match") { override def visit[G[-_, +_]](v: Visitor[G, OP]): G[I, Option[O]] = v.visitMatch(this) override private[v1] def withDebugging(debugging: Debugging[Nothing, Nothing]): Match[I, S, B, O, OP] = copy(debugging = debugging) } + /** + * Attempts to match the input [[asString]] result against a [[Regex]] and applies the [[asOutput]] function. + * + * @note the matches are received lazily, so if you only need the first match, you can efficiently grab the first + * match and discard the rest. + * + * @param inputExpr an expression producing a stringify-able value + * @param asString a function to convert the input to a string + * @param regex a pre-compiled regular expression + * @param asOutput a function to convert the matched groups to an expected output type (as determined by the DSL + */ + final case class RegexMatches[-I, S : OP, +O : OP, OP[_]]( + inputExpr: Expr[I, S, OP], + asString: S => String, + regex: Regex, + asOutput: (S, LazyList[RegexMatch]) => O, + override private[v1] val debugging: Debugging[Nothing, Nothing] = NoDebugging, + ) extends Expr[I, O, OP]("matchesRegex") { + override def visit[G[-_, +_]](v: Visitor[G, OP]): G[I, O] = v.visitRegexMatches(this) + override private[v1] def withDebugging(debugging: Debugging[Nothing, Nothing]): RegexMatches[I, S, O, OP] = + copy(debugging = debugging) + } + /** * A container for a condition and an expression to compute if the condition is met. * diff --git a/core-v1/src/main/scala/data/Justified.scala b/core-v1/src/main/scala/data/Justified.scala index 15ff68b7..0b1a24cd 100644 --- a/core-v1/src/main/scala/data/Justified.scala +++ b/core-v1/src/main/scala/data/Justified.scala @@ -3,7 +3,7 @@ package com.rallyhealth.vapors.v1 package data import algebra.{EqualComparable, SizeComparable, SizeComparison} -import dsl.{WrapConst, WrapContained, WrapFact, WrapQuantifier, WrapSelected} +import dsl.{WrapConst, WrapContained, WrapFact, WrapQuantifier, WrapRegexMatches, WrapSelected} import lens.{DataPath, VariantLens} import logic.Logic import math._ @@ -15,6 +15,7 @@ import cats.{Applicative, Eq, Eval, Foldable, Functor, Order, Semigroupal, Trave import scala.annotation.nowarn import scala.collection.Factory +import scala.util.matching.Regex /** * Represents a result that contains a tree of justified inputs and operations, as well as the value @@ -306,6 +307,27 @@ object Justified extends LowPriorityJustifiedImplicits { } } + implicit def wrapRegexMatches[OP[_]]: WrapRegexMatches[Justified, OP] = + WrapRegexMatchesJustified.asInstanceOf[WrapRegexMatches[Justified, OP]] + + private final object WrapRegexMatchesJustified extends WrapRegexMatches[Justified, Any] { + + override def wrapMatched[O]( + in: Justified[String], + out: O, + pattern: Regex, + matches: LazyList[RegexMatch], + ): Justified[O] = + Justified.byInference( + "regex_matches", + out, + NonEmptySeq.of( + in, + Justified.byConst(pattern), + ), + ) + } + implicit def wrapSelected[OP[_]]: WrapSelected[Justified, OP] = WrapSelectedJustified.asInstanceOf[WrapSelected[Justified, OP]] diff --git a/core-v1/src/main/scala/data/RegexMatch.scala b/core-v1/src/main/scala/data/RegexMatch.scala new file mode 100644 index 00000000..cba70962 --- /dev/null +++ b/core-v1/src/main/scala/data/RegexMatch.scala @@ -0,0 +1,35 @@ +package com.rallyhealth.vapors.v1 + +package data + +import scala.util.matching.Regex + +/** + * Represents the data provided from a regular expression matched within some input string. + * + * @see [[Regex.Match]] + * + * @param matched the string that was matched in the original input + * @param slice the slice range of the matched string + * @param groups a map of group names AND group indexes to their matched strings + */ +final case class RegexMatch( + matched: String, + slice: SliceRange.Absolute, + groups: Map[String, String], +) + +object RegexMatch { + + def unapply(m: RegexMatch): Some[(String, Map[String, String])] = Some((m.matched, m.groups)) + def unapply(m: Regex.Match): Some[(String, Map[String, String])] = unapply(from(m)) + + def from(m: Regex.Match): RegexMatch = + RegexMatch( + m.matched, + SliceRange.Absolute(m.start, m.end), + (1 to m.groupCount).map(_.toString).zip(m.subgroups).toMap.withDefault { k => + Option(m.group(k)).getOrElse("") + }, + ) +} diff --git a/core-v1/src/main/scala/data/SliceRange.scala b/core-v1/src/main/scala/data/SliceRange.scala index de2f33fc..e8dc8f0e 100644 --- a/core-v1/src/main/scala/data/SliceRange.scala +++ b/core-v1/src/main/scala/data/SliceRange.scala @@ -32,7 +32,7 @@ object SliceRange { relativeStart => if (relativeStart < 0) size + relativeStart else relativeStart, relativeEnd => if (relativeEnd < 0) size + relativeEnd else relativeEnd, ) - Absolute(this, relative.left.getOrElse(0), relative.right.getOrElse(size) - 1) + Absolute(relative.left.getOrElse(0), relative.right.getOrElse(size) - 1, this) } } @@ -54,16 +54,29 @@ object SliceRange { * This could contain the total size as well, but I didn't see a need for it yet. */ final case class Absolute private[SliceRange] ( - relative: Relative, start: Int, end: Int, + relative: Relative, ) { + def this( + start: Int, + end: Int, + ) = this(start, end, Relative(Ior.Both(start, end))) + val toRange: Range = Range.inclusive(start, end) def contains(index: Int): Boolean = toRange.contains(index) } + object Absolute { + + def apply( + start: Int, + end: Int, + ): Absolute = new Absolute(start, end) + } + final class Syntax(private val num: Int) extends AnyVal { def fromEnd: Relative = Relative.fromEnd(num) diff --git a/core-v1/src/main/scala/debug/DebugArgs.scala b/core-v1/src/main/scala/debug/DebugArgs.scala index 3c0d929a..d504f9b5 100644 --- a/core-v1/src/main/scala/debug/DebugArgs.scala +++ b/core-v1/src/main/scala/debug/DebugArgs.scala @@ -8,11 +8,11 @@ import lens.VariantLens import cats.Eval import cats.data.{NonEmptySeq, NonEmptyVector} -import com.rallyhealth.vapors.v1.algebra.Expr.MatchCase import izumi.reflect.Tag -import shapeless.{unexpected, HList} +import shapeless.HList import scala.reflect.ClassTag +import scala.util.matching.Regex /** * A type-level programming trick that enlists the compiler to prove that the [[Debugging]] hook function @@ -312,6 +312,17 @@ object DebugArgs { override type Out = Option[O] } + implicit def debugRegexMatches[ + I, + S, + O, + OP[_], + ]: Aux[Expr.RegexMatches[I, S, O, OP], OP, (I, S, Regex, LazyList[RegexMatch]), O] = + new DebugArgs[Expr.RegexMatches[I, S, O, OP], OP] { + override type In = (I, S, Regex, LazyList[RegexMatch]) + override type Out = O + } + implicit def debugWhen[I, B, O, OP[_]]: Aux[Expr.When[I, B, O, OP], OP, (I, Int), O] = new DebugArgs[Expr.When[I, B, O, OP], OP] { override type In = (I, Int) diff --git a/core-v1/src/main/scala/dsl/BuildExprDsl.scala b/core-v1/src/main/scala/dsl/BuildExprDsl.scala index 8df80b1b..5fbb3969 100644 --- a/core-v1/src/main/scala/dsl/BuildExprDsl.scala +++ b/core-v1/src/main/scala/dsl/BuildExprDsl.scala @@ -5,7 +5,7 @@ package dsl import algebra.Expr.MatchCase import algebra._ import data._ -import lens.{CollectInto, IterableInto, VariantLens} +import lens.{CollectInto, DataPath, IterableInto, VariantLens} import logic.{Conjunction, Disjunction, Logic, Negation} import math.{Add, Power} @@ -14,6 +14,8 @@ import cats.{Applicative, FlatMap, Foldable, Functor, Id, Order, Reducible, Semi import izumi.reflect.Tag import shapeless.{Generic, HList, Typeable} +import scala.util.matching.Regex + trait BuildExprDsl extends DebugExprDsl with SliceRangeSyntax @@ -37,6 +39,8 @@ trait BuildExprDsl protected implicit def wrapContained: WrapContained[W, OP] + protected implicit def wrapRegexMatches: WrapRegexMatches[W, OP] + protected implicit def wrapSelected: WrapSelected[W, OP] def ident[I](implicit opI: OP[W[I]]): Expr.Identity[W[I], OP] @@ -175,6 +179,63 @@ You should prefer put your declaration of dependency on definitions close to whe ): Expr.Sequence[C, I, O, OP] = Expr.Sequence(expressions) + implicit def str[I](expr: I ~:> W[String]): StringExprOps[I] + + abstract class StringExprOps[-I](inputExpr: I ~:> W[String]) { + + def matchesRegex( + re: Regex, + )(implicit + opS: OP[W[String]], + opB: OP[W[Boolean]], + ): Expr.RegexMatches[I, W[String], W[Boolean], OP] = + Expr.RegexMatches( + inputExpr, + extract.extract(_), + re, + (in, ms) => wrapRegexMatches.wrapMatched(in, ms.nonEmpty, re, ms), + ) + + def findFirstMatch( + re: Regex, + )(implicit + opS: OP[W[String]], + opM: OP[RegexMatch], + opO: OP[Option[W[RegexMatch]]], + ): Expr.RegexMatches[I, W[String], Option[W[RegexMatch]], OP] = + Expr.RegexMatches[I, W[String], Option[W[RegexMatch]], OP]( + inputExpr, + extract.extract(_), + re, + (in, ms) => { + val wrappedMatches = wrapRegexMatches.wrapMatched(in, ms, re, ms) + ms.headOption.map { m => + wrapSelected.wrapSelected(wrappedMatches, DataPath.empty.atIndex(0), m) + } + }, + ) + + def findAllMatches( + re: Regex, + )(implicit + opS: OP[W[String]], + opM: OP[RegexMatch], + opO: OP[Seq[W[RegexMatch]]], + ): Expr.RegexMatches[I, W[String], Seq[W[RegexMatch]], OP] = + Expr.RegexMatches[I, W[String], Seq[W[RegexMatch]], OP]( + inputExpr, + extract.extract(_), + re, + (in, ms) => { + val wrappedMatches = wrapRegexMatches.wrapMatched(in, ms, re, ms) + ms.zipWithIndex.map { + case (m, idx) => + wrapSelected.wrapSelected(wrappedMatches, DataPath.empty.atIndex(idx), m) + } + }, + ) + } + def when[I](condExpr: I ~:> W[Boolean]): WhenBuilder[I, W[Boolean]] abstract class WhenBuilder[-I, B : ExtractValue.AsBoolean](firstCondExpr: I ~:> B) { diff --git a/core-v1/src/main/scala/dsl/JustifiedBuildExprDsl.scala b/core-v1/src/main/scala/dsl/JustifiedBuildExprDsl.scala index a2f49396..8b99e2af 100644 --- a/core-v1/src/main/scala/dsl/JustifiedBuildExprDsl.scala +++ b/core-v1/src/main/scala/dsl/JustifiedBuildExprDsl.scala @@ -36,6 +36,8 @@ trait JustifiedBuildExprDsl override protected implicit final def wrapFact: WrapFact[Justified, OP] = Justified.wrapFact + override protected implicit final def wrapRegexMatches: WrapRegexMatches[Justified, OP] = Justified.wrapRegexMatches + override protected implicit final def wrapSelected: WrapSelected[Justified, OP] = Justified.wrapSelected override protected implicit final def wrapQuantifier: WrapQuantifier[Justified, OP] = Justified.wrapQuantifier diff --git a/core-v1/src/main/scala/dsl/UnwrappedBuildExprDsl.scala b/core-v1/src/main/scala/dsl/UnwrappedBuildExprDsl.scala index b06d65d3..39a0fda0 100644 --- a/core-v1/src/main/scala/dsl/UnwrappedBuildExprDsl.scala +++ b/core-v1/src/main/scala/dsl/UnwrappedBuildExprDsl.scala @@ -13,6 +13,8 @@ import cats.{FlatMap, Foldable, Functor, Id, Order, Reducible, Traverse} import izumi.reflect.Tag import shapeless.{Generic, HList, Nat, Typeable} +import scala.util.matching.Regex + trait UnwrappedBuildExprDsl extends BuildExprDsl with UnwrappedUsingDefinitionArityMethods @@ -31,6 +33,8 @@ trait UnwrappedBuildExprDsl override protected implicit final def wrapContained: WrapContained[W, OP] = WrapContained.unwrapped + override protected implicit final def wrapRegexMatches: WrapRegexMatches[W, OP] = WrapRegexMatches.unwrapped + override protected implicit final def wrapSelected: WrapSelected[W, OP] = WrapSelected.unwrapped // TODO: Should this be visible outside this trait? @@ -127,6 +131,35 @@ trait UnwrappedBuildExprDsl ): CombineHolder[I, L, L, R, R, pow.Out, OP] = (leftExpr ^ rightExpr)(opR, pow) + override implicit final def str[I](expr: I ~:> String): UnwrappedStringExprOps[I] = new UnwrappedStringExprOps(expr) + + final class UnwrappedStringExprOps[-I](inputExpr: I ~:> String) extends StringExprOps(inputExpr) { + + override def matchesRegex( + re: Regex, + )(implicit + opS: OP[String], + opB: OP[Boolean], + ): Expr.RegexMatches[I, String, Boolean, OP] = + super.matchesRegex(re) + + override def findFirstMatch( + re: Regex, + )(implicit + opS: OP[String], + opM: OP[RegexMatch], + opO: OP[Option[RegexMatch]], + ): Expr.RegexMatches[I, String, Option[RegexMatch], OP] = super.findFirstMatch(re) + + override def findAllMatches( + re: Regex, + )(implicit + opS: OP[String], + opM: OP[RegexMatch], + opO: OP[Seq[RegexMatch]], + ): Expr.RegexMatches[I, String, Seq[RegexMatch], OP] = super.findAllMatches(re) + } + override final def when[I](condExpr: I ~:> Boolean): UnwrappedWhenBuilder[I] = new UnwrappedWhenBuilder(condExpr) final class UnwrappedWhenBuilder[-I](condExpr: I ~:> Boolean) extends WhenBuilder[I, Boolean](condExpr) { diff --git a/core-v1/src/main/scala/dsl/WrapRegexMatches.scala b/core-v1/src/main/scala/dsl/WrapRegexMatches.scala new file mode 100644 index 00000000..86548823 --- /dev/null +++ b/core-v1/src/main/scala/dsl/WrapRegexMatches.scala @@ -0,0 +1,33 @@ +package com.rallyhealth.vapors.v1 + +package dsl + +import data.RegexMatch + +import shapeless.Id + +import scala.util.matching.Regex + +trait WrapRegexMatches[W[+_], OP[_]] { + + def wrapMatched[O]( + in: W[String], + out: O, + pattern: Regex, + matches: LazyList[RegexMatch], + ): W[O] +} + +object WrapRegexMatches { + + @inline final def unwrapped[OP[_]]: WrapRegexMatches[Id, OP] = Unwrapped.asInstanceOf[WrapRegexMatches[Id, OP]] + + private final object Unwrapped extends WrapRegexMatches[Id, Any] { + override def wrapMatched[O]( + in: String, + out: O, + pattern: Regex, + matches: LazyList[RegexMatch], + ): O = out + } +} diff --git a/core-v1/src/main/scala/dsl/WrappedBuildExprDsl.scala b/core-v1/src/main/scala/dsl/WrappedBuildExprDsl.scala index e83fa32f..fb373972 100644 --- a/core-v1/src/main/scala/dsl/WrappedBuildExprDsl.scala +++ b/core-v1/src/main/scala/dsl/WrappedBuildExprDsl.scala @@ -12,6 +12,8 @@ import cats.{FlatMap, Foldable, Functor, Id, Order, Reducible, Traverse} import izumi.reflect.Tag import shapeless.{Generic, HList, Nat, Typeable} +import scala.util.matching.Regex + trait WrappedBuildExprDsl extends BuildExprDsl { self: DslTypes with WrappedExprHListDslImplicits with OutputTypeImplicits => @@ -23,6 +25,8 @@ trait WrappedBuildExprDsl extends BuildExprDsl { protected implicit def wrapFact: WrapFact[W, OP] + protected implicit def wrapRegexMatches: WrapRegexMatches[W, OP] + protected implicit def wrapSelected: WrapSelected[W, OP] override def ident[I](implicit opI: OP[W[I]]): Expr.Identity[W[I], OP] = Expr.Identity() @@ -36,6 +40,10 @@ trait WrappedBuildExprDsl extends BuildExprDsl { ): CombineHolder[I, W[L], W[L], W[R], W[R], pow.Out, OP] = (leftExpr ^ rightExpr)(opR, pow) + override implicit def str[I](expr: I ~:> W[String]): WrappedStringExprOps[I] = new WrappedStringExprOps(expr) + + class WrappedStringExprOps[-I](inputExpr: I ~:> W[String]) extends StringExprOps(inputExpr) + override def when[I](condExpr: I ~:> W[Boolean]): WrappedWhenBuilder[I] = new WrappedWhenBuilder(condExpr) class WrappedWhenBuilder[-I](firstCondExpr: I ~:> W[Boolean]) extends WhenBuilder[I, W[Boolean]](firstCondExpr) { diff --git a/core-v1/src/main/scala/engine/ImmutableCachingEngine.scala b/core-v1/src/main/scala/engine/ImmutableCachingEngine.scala index b9da83b8..1ee13070 100644 --- a/core-v1/src/main/scala/engine/ImmutableCachingEngine.scala +++ b/core-v1/src/main/scala/engine/ImmutableCachingEngine.scala @@ -440,6 +440,8 @@ object ImmutableCachingEngine { debugging(expr).invokeAndReturn(state((i, inputs), result)) } + override def visitRegexMatches[I, S, O : OP](expr: Expr.RegexMatches[I, S, O, OP]): I => CachedResult[O] = ??? + override def visitRepeat[I, O]( expr: Expr.Repeat[I, O, OP], )(implicit diff --git a/core-v1/src/main/scala/engine/SimpleEngine.scala b/core-v1/src/main/scala/engine/SimpleEngine.scala index ec3d02d0..25813646 100644 --- a/core-v1/src/main/scala/engine/SimpleEngine.scala +++ b/core-v1/src/main/scala/engine/SimpleEngine.scala @@ -13,6 +13,9 @@ import logic.{Conjunction, Disjunction, Negation} import cats.{Applicative, Eval, FlatMap, Foldable, Functor, SemigroupK, Traverse} import shapeless.{HList, TypeCase, Typeable} +import scala.collection.MapView +import scala.collection.immutable.IntMap + /** * A vapors [[Expr]] interpreter that just builds a simple function without providing any post-processing. * @@ -253,6 +256,17 @@ object SimpleEngine { debugging(expr).invokeAndReturn(state((i, results), finalResult)) } + override def visitRegexMatches[I, S, O : OP](expr: Expr.RegexMatches[I, S, O, OP]): I => O = { i => + val s = expr.inputExpr.visit(this)(i) + val str = expr.asString(s) + val allMatches = expr.regex + .findAllMatchIn(str) + .map(RegexMatch.from) + .to(LazyList) + val o = expr.asOutput(s, allMatches) + debugging(expr).invokeAndReturn(state((i, s, expr.regex, allMatches), o)) + } + override def visitRepeat[I, O]( expr: Expr.Repeat[I, O, OP], )(implicit diff --git a/core-v1/src/main/scala/engine/StandardEngine.scala b/core-v1/src/main/scala/engine/StandardEngine.scala index 78dff8cf..92f69713 100644 --- a/core-v1/src/main/scala/engine/StandardEngine.scala +++ b/core-v1/src/main/scala/engine/StandardEngine.scala @@ -271,6 +271,10 @@ object StandardEngine { ExprResult.Or(expr, finalState, results) } + override def visitRegexMatches[I, S, O : OP]( + expr: Expr.RegexMatches[I, S, O, OP], + ): PO <:< I => ExprResult[PO, I, O, OP] = ??? + override def visitRepeat[I, O]( expr: Expr.Repeat[I, O, OP], )(implicit diff --git a/core-v1/src/test/scala/SimpleJustifiedRegexMatchesSpec.scala b/core-v1/src/test/scala/SimpleJustifiedRegexMatchesSpec.scala new file mode 100644 index 00000000..82ce8342 --- /dev/null +++ b/core-v1/src/test/scala/SimpleJustifiedRegexMatchesSpec.scala @@ -0,0 +1,147 @@ +package com.rallyhealth.vapors.v1 + +import data.{Justified, RegexMatch, SliceRange} +import lens.DataPath + +import cats.data.NonEmptySeq +import munit.FunSuite + +class SimpleJustifiedRegexMatchesSpec extends FunSuite { + + import dsl.uncached.justified._ + + test("matchesRegex returns true with an exact string match") { + val exact = "exact" + val re = exact.r + val expr = exact.const.matchesRegex(re) + val obtained = expr.run() + val expected = Justified.byInference( + "regex_matches", + true, + NonEmptySeq.of(Justified.byConst(exact), Justified.byConst(re)), + ) + assertEquals(obtained, expected) + } + + test("matchesRegex returns false") { + val input = "correct" + val re = "wrong".r + val expr = input.const.matchesRegex(re) + val obtained = expr.run() + val expected = Justified.byInference( + "regex_matches", + false, + NonEmptySeq.of(Justified.byConst(input), Justified.byConst(re)), + ) + assertEquals(obtained, expected) + } + + test("matchesRegex returns true for multiple substrings") { + val input = "one match, two match, three match, more" + val re = "match".r + val expr = input.const.matchesRegex(re) + val obtained = expr.run() + val expected = Justified.byInference( + "regex_matches", + true, + NonEmptySeq.of(Justified.byConst(input), Justified.byConst(re)), + ) + assertEquals(obtained, expected) + } + + test("findFirstMatch returns some exact match") { + val exact = "exact" + val re = exact.r + val expr = exact.const.findFirstMatch(re) + val obtained = expr.run() + val firstMatch = RegexMatch(exact, SliceRange.Absolute(0, exact.length), Map()) + val expected = Justified.bySelection( + firstMatch, + DataPath.empty.atIndex(0), + Justified.byInference( + "regex_matches", + LazyList(firstMatch), + NonEmptySeq.of(Justified.byConst(exact), Justified.byConst(re)), + ), + ) + assertEquals(obtained, Some(expected)) + } + + test("findFirstMatch returns multiple matches") { + val input = "one match, two match, three match, more" + val exact = "match" + val re = exact.r + val expr = input.const.findFirstMatch(re) + val obtained = expr.run() + def matchAfter(prefix: String): RegexMatch = + RegexMatch(exact, SliceRange.Absolute(prefix.length, prefix.length + exact.length), Map()) + + val firstMatch = matchAfter("one ") + val expected = Justified.BySelection( + firstMatch, + DataPath.empty.atIndex(0), + Justified.byInference( + "regex_matches", + LazyList( + firstMatch, + matchAfter("one match, two "), + matchAfter("one match, two match, three "), + ), + NonEmptySeq.of(Justified.byConst(input), Justified.byConst(re)), + ), + ) + assertEquals(obtained, Some(expected)) + } + + test("findFirstMatch returns none") { + val expr = "correct".const.findFirstMatch("wrong".r) + val obtained = expr.run() + assertEquals(obtained, None) + } + + test("findAllMatches returns one exact match") { + val exact = "exact" + val re = exact.r + val expr = exact.const.findAllMatches(re) + val obtained = expr.run() + val firstMatch = RegexMatch(exact, SliceRange.Absolute(0, exact.length), Map()) + val expected = Justified.elements( + Justified.byInference( + "regex_matches", + LazyList(firstMatch), + NonEmptySeq.of(Justified.byConst(exact), Justified.byConst(re)), + ), + ) + assertEquals(obtained, expected) + } + + test("findAllMatches returns multiple matches") { + val input = "one match, two match, three match, more" + val exact = "match" + val re = exact.r + val expr = input.const.findAllMatches(re) + val obtained = expr.run() + def matchAfter(prefix: String): RegexMatch = + RegexMatch(exact, SliceRange.Absolute(prefix.length, prefix.length + exact.length), Map()) + + val firstMatch = matchAfter("one ") + val expected = Justified.elements( + Justified.byInference( + "regex_matches", + LazyList( + firstMatch, + matchAfter("one match, two "), + matchAfter("one match, two match, three "), + ), + NonEmptySeq.of(Justified.byConst(input), Justified.byConst(re)), + ), + ) + assertEquals(obtained, expected) + } + + test("findAllMatches returns none") { + val expr = "correct".const.findAllMatches("wrong".r) + val obtained = expr.run() + assertEquals(obtained, LazyList()) + } +} diff --git a/core-v1/src/test/scala/SimpleRegexMatchesSpec.scala b/core-v1/src/test/scala/SimpleRegexMatchesSpec.scala new file mode 100644 index 00000000..313edd03 --- /dev/null +++ b/core-v1/src/test/scala/SimpleRegexMatchesSpec.scala @@ -0,0 +1,85 @@ +package com.rallyhealth.vapors.v1 + +import data.{RegexMatch, SliceRange} + +import munit.FunSuite + +class SimpleRegexMatchesSpec extends FunSuite { + + import dsl.uncached._ + + test("matchesRegex returns true with an exact string match") { + val input = "exact" + val expr = input.const.matchesRegex(input.r) + val obtained = expr.run() + assertEquals(obtained, true) + } + + test("matchesRegex returns false") { + val expr = "correct".const.matchesRegex("wrong".r) + val obtained = expr.run() + assertEquals(obtained, false) + } + + test("matchesRegex returns true for multiple substrings") { + val expr = "one match, two match, three match, more".const.matchesRegex("match".r) + val obtained = expr.run() + assertEquals(obtained, true) + } + + test("findFirstMatch returns some exact match") { + val exact = "exact" + val re = exact.r + val expr = exact.const.findFirstMatch(re) + val obtained = expr.run() + assertEquals(obtained, Some(RegexMatch(exact, SliceRange.Absolute(0, exact.length), Map()))) + } + + test("findFirstMatch returns multiple matches") { + val input = "one match, two match, three match, more" + val exact = "match" + val re = exact.r + val expr = input.const.findFirstMatch(re) + val obtained = expr.run() + val firstIdx = "one ".length + assertEquals(obtained, Some(RegexMatch(exact, SliceRange.Absolute(firstIdx, firstIdx + exact.length), Map()))) + } + + test("findFirstMatch returns none") { + val expr = "correct".const.findFirstMatch("wrong".r) + val obtained = expr.run() + assertEquals(obtained, None) + } + + test("findAllMatches returns some exact match") { + val exact = "exact" + val re = exact.r + val expr = exact.const.findAllMatches(re) + val obtained = expr.run() + assertEquals(obtained, LazyList(RegexMatch(exact, SliceRange.Absolute(0, exact.length), Map()))) + } + + test("findAllMatches returns multiple matches") { + val input = "one match, two match, three match, more" + val exact = "match" + val re = exact.r + val expr = input.const.findAllMatches(re) + val obtained = expr.run() + def matchAfter(prefix: String): RegexMatch = + RegexMatch(exact, SliceRange.Absolute(prefix.length, prefix.length + exact.length), Map()) + + val expected = LazyList( + matchAfter("one "), + matchAfter("one match, two "), + matchAfter("one match, two match, three "), + ) + assertEquals(obtained, expected) + } + + test("findAllMatches returns empty") { + val expr = "correct".const.findAllMatches("wrong".r) + val obtained = expr.run() + assertEquals(obtained, LazyList()) + } + +}