aboutsummaryrefslogtreecommitdiff
path: root/src/test/scala/xyz/driver/restquery/rest/parsers/SearchFilterParserSuite.scala
diff options
context:
space:
mode:
Diffstat (limited to 'src/test/scala/xyz/driver/restquery/rest/parsers/SearchFilterParserSuite.scala')
-rw-r--r--src/test/scala/xyz/driver/restquery/rest/parsers/SearchFilterParserSuite.scala259
1 files changed, 259 insertions, 0 deletions
diff --git a/src/test/scala/xyz/driver/restquery/rest/parsers/SearchFilterParserSuite.scala b/src/test/scala/xyz/driver/restquery/rest/parsers/SearchFilterParserSuite.scala
new file mode 100644
index 0000000..efa8666
--- /dev/null
+++ b/src/test/scala/xyz/driver/restquery/rest/parsers/SearchFilterParserSuite.scala
@@ -0,0 +1,259 @@
+package xyz.driver.restquery.http.parsers
+
+import java.util.UUID
+
+import fastparse.core.Parsed
+import org.scalacheck.Arbitrary.arbitrary
+import org.scalacheck.{Gen, Prop}
+import org.scalatest.FreeSpecLike
+import org.scalatest.prop.Checkers
+import xyz.driver.restquery.domain.SearchFilterBinaryOperation.Eq
+import xyz.driver.restquery.domain.SearchFilterExpr.Dimension
+import xyz.driver.restquery.domain.SearchFilterNAryOperation.In
+import xyz.driver.restquery.domain.{SearchFilterExpr, SearchFilterNAryOperation}
+import xyz.driver.restquery.http.parsers.TestUtils._
+import xyz.driver.restquery.utils.Utils
+import xyz.driver.restquery.utils.Utils._
+
+import scala.util._
+
+object SearchFilterParserSuite {
+
+ class UnexpectedSearchFilterExprException(x: SearchFilterExpr) extends Exception(s"unexpected $x")
+
+}
+
+class SearchFilterParserSuite extends FreeSpecLike with Checkers {
+
+ import SearchFilterParserSuite._
+
+ "parse" - {
+ "should convert column names to snake case" in {
+ import xyz.driver.restquery.domain.SearchFilterBinaryOperation._
+
+ val filter = SearchFilterParser.parse(
+ Seq(
+ "filters" -> "status IN Summarized,ReviewCriteria,Flagged,Done",
+ "filters" -> "previousStatus NOTEQ New",
+ "filters" -> "previousStatus NOTEQ ReviewSummary"
+ ))
+
+ assert(
+ filter === Success(SearchFilterExpr.Intersection(List(
+ SearchFilterExpr.Atom
+ .NAry(Dimension(None, "status"), In, Seq("Summarized", "ReviewCriteria", "Flagged", "Done")),
+ SearchFilterExpr.Atom.Binary(Dimension(None, "previous_status"), NotEq, "New"),
+ SearchFilterExpr.Atom.Binary(Dimension(None, "previous_status"), NotEq, "ReviewSummary")
+ ))))
+ }
+ "dimensions" - {
+ "with table name" in check {
+ val dimensionGen = {
+ for (left <- Gen.identifier; right <- Gen.identifier)
+ yield left -> right
+ }
+ Prop.forAllNoShrink(dimensionGen) {
+ case (left, right) =>
+ val raw = s"$left.$right"
+ val l = toSnakeCase(left)
+ val r = toSnakeCase(right)
+ SearchFilterParser.dimensionParser.parse(raw) match {
+ case Parsed.Success(Dimension(Some(`l`), `r`), _) => true
+ case _ => false
+ }
+ }
+ }
+ "just with field name" in check {
+ Prop.forAllNoShrink(Gen.identifier) { s =>
+ val databaseS = Utils.toSnakeCase(s)
+ SearchFilterParser.dimensionParser.parse(s) match {
+ case Parsed.Success(Dimension(None, `databaseS`), _) => true
+ case _ => false
+ }
+ }
+ }
+ }
+ "atoms" - {
+ "binary" - {
+ "common operators" - {
+ "should be parsed with text values" in check {
+ import xyz.driver.restquery.domain.SearchFilterBinaryOperation._
+
+ val testQueryGen = queryGen(
+ dimensionGen = Gen.identifier,
+ opGen = commonBinaryOpsGen,
+ valueGen = nonEmptyString
+ )
+
+ Prop.forAllNoShrink(testQueryGen) { query =>
+ SearchFilterParser
+ .parse(Seq("filters" -> query))
+ .map {
+ case SearchFilterExpr.Atom.Binary(_, Eq | NotEq | Like, _) => true
+ case x => throw new UnexpectedSearchFilterExprException(x)
+ }
+ .successProp
+ }
+ }
+ }
+
+ "numeric operators" - {
+ "should not be parsed with text values" in check {
+ val testQueryGen = queryGen(
+ dimensionGen = Gen.identifier,
+ opGen = numericBinaryOpsGen,
+ valueGen = nonEmptyString.filter { s =>
+ !s.matches("^\\d+$")
+ }
+ )
+
+ Prop.forAllNoShrink(testQueryGen) { query =>
+ SearchFilterParser.parse(Seq("filters" -> query)).failureProp
+ }
+ }
+ }
+
+ "actual recordId" - {
+ "should not be parsed with numeric values" in {
+ val filter = SearchFilterParser.parse(Seq("filters" -> "recordId EQ 1"))
+ assert(filter === Success(SearchFilterExpr.Atom.Binary(Dimension(None, "record_id"), Eq, Long.box(1))))
+ }
+ }
+
+ "actual isVisible boolean" - {
+ "should not be parsed with boolean values" in {
+ val filter = SearchFilterParser.parse(Seq("filters" -> "isVisible EQ true"))
+ assert(filter === Success(SearchFilterExpr.Atom.Binary(Dimension(None, "is_visible"), Eq, Boolean.box(true))))
+ }
+ }
+
+ "actual patientId uuid" - {
+ "should parse the full UUID as java.util.UUID type" in {
+ val filter = SearchFilterParser.parse(Seq("filters" -> "patientId EQ 4b4879f7-42b3-4b7c-a685-5c97d9e69e7c"))
+ assert(filter === Success(SearchFilterExpr.Atom.Binary(
+ Dimension(None, "patient_id"), Eq, UUID.fromString("4b4879f7-42b3-4b7c-a685-5c97d9e69e7c"))))
+ }
+ }
+
+ "all operators" - {
+ "should be parsed with numeric values" in check {
+ val testQueryGen = queryGen(
+ dimensionGen = Gen.identifier,
+ opGen = allBinaryOpsGen,
+ valueGen = numericBinaryAtomValuesGen
+ )
+
+ Prop.forAllNoShrink(testQueryGen) { query =>
+ SearchFilterParser
+ .parse(Seq("filters" -> query))
+ .map {
+ case _: SearchFilterExpr.Atom.Binary => true
+ case x => throw new UnexpectedSearchFilterExprException(x)
+ }
+ .successProp
+ }
+ }
+ }
+ }
+
+ "n-ary" - {
+ "actual record Ids" - {
+ "should not be parsed with text values" in {
+ val filter = SearchFilterParser.parse(Seq("filters" -> "id IN 1,5"))
+ filter match {
+ case Success(_) => ()
+ case Failure(t) => t.printStackTrace()
+ }
+ assert(
+ filter === Success(SearchFilterExpr.Atom.NAry(Dimension(None, "id"), In, Seq(Long.box(1), Long.box(5)))))
+ }
+ }
+
+ "in" in check {
+ val testQueryGen = queryGen(
+ dimensionGen = Gen.identifier,
+ opGen = Gen.const("in"),
+ valueGen = inValuesGen
+ )
+
+ Prop.forAllNoShrink(testQueryGen) { query =>
+ SearchFilterParser
+ .parse(Seq("filters" -> query))
+ .map {
+ case SearchFilterExpr.Atom.NAry(_, SearchFilterNAryOperation.In, _) => true
+ case x => throw new UnexpectedSearchFilterExprException(x)
+ }
+ .successProp
+ }
+ }
+ }
+ }
+
+ "intersections" - {
+ "should be parsed" in check {
+ val commonAtomsGen = queryGen(
+ dimensionGen = Gen.identifier,
+ opGen = commonBinaryOpsGen,
+ valueGen = nonEmptyString
+ )
+
+ val numericAtomsGen = queryGen(
+ dimensionGen = Gen.identifier,
+ opGen = numericBinaryOpsGen,
+ valueGen = numericBinaryAtomValuesGen
+ )
+
+ val allAtomsGen = Gen.oneOf(commonAtomsGen, numericAtomsGen)
+ val intersectionsGen = Gen.choose(1, 3).flatMap { size =>
+ Gen.containerOfN[Seq, String](size, allAtomsGen)
+ }
+
+ Prop.forAllNoShrink(intersectionsGen) { queries =>
+ SearchFilterParser.parse(queries.map(query => "filters" -> query)).successProp
+ }
+ }
+ }
+ }
+
+ private val CommonBinaryOps = Seq("eq", "noteq", "like")
+ private val NumericBinaryOps = Seq("gt", "gteq", "lt", "lteq")
+
+ private val allBinaryOpsGen: Gen[String] =
+ Gen.oneOf(CommonBinaryOps ++ NumericBinaryOps).flatMap(randomCapitalization)
+ private val commonBinaryOpsGen: Gen[String] = Gen.oneOf(CommonBinaryOps).flatMap(randomCapitalization)
+ private val numericBinaryOpsGen: Gen[String] = Gen.oneOf(NumericBinaryOps).flatMap(randomCapitalization)
+
+ private val inValueCharsGen: Gen[Char] = arbitrary[Char].filter(_ != ',')
+
+ private val nonEmptyString = arbitrary[String].filter { s =>
+ !Utils.safeTrim(s).isEmpty
+ }
+
+ private val numericBinaryAtomValuesGen: Gen[String] = arbitrary[Long].map(_.toString)
+ private val inValueGen: Gen[String] = {
+ Gen.nonEmptyContainerOf[Seq, Char](inValueCharsGen).map(_.mkString).filter(s => Utils.safeTrim(s).nonEmpty)
+ }
+ private val inValuesGen: Gen[String] = Gen.choose(1, 5).flatMap { size =>
+ Gen.containerOfN[Seq, String](size, inValueGen).map(_.mkString(","))
+ }
+
+ private def queryGen(dimensionGen: Gen[String], opGen: Gen[String], valueGen: Gen[String]): Gen[String] =
+ for {
+ dimension <- dimensionGen
+ op <- opGen
+ value <- valueGen
+ } yield s"$dimension $op $value"
+
+ private def randomCapitalization(input: String): Gen[String] = {
+ Gen.containerOfN[Seq, Boolean](input.length, arbitrary[Boolean]).map { capitalize =>
+ input.view
+ .zip(capitalize)
+ .map {
+ case (currChar, true) => currChar.toUpper
+ case (currChar, false) => currChar
+ }
+ .mkString
+ }
+ }
+
+}