aboutsummaryrefslogtreecommitdiff
path: root/src/main/scala/xyz/driver/restquery/query
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/scala/xyz/driver/restquery/query')
-rw-r--r--src/main/scala/xyz/driver/restquery/query/Pagination.scala12
-rw-r--r--src/main/scala/xyz/driver/restquery/query/SearchFilterBinaryOperation.scala5
-rw-r--r--src/main/scala/xyz/driver/restquery/query/SearchFilterExpr.scala196
-rw-r--r--src/main/scala/xyz/driver/restquery/query/SearchFilterNAryOperation.scala5
-rw-r--r--src/main/scala/xyz/driver/restquery/query/Sorting.scala56
5 files changed, 274 insertions, 0 deletions
diff --git a/src/main/scala/xyz/driver/restquery/query/Pagination.scala b/src/main/scala/xyz/driver/restquery/query/Pagination.scala
new file mode 100644
index 0000000..27b8f12
--- /dev/null
+++ b/src/main/scala/xyz/driver/restquery/query/Pagination.scala
@@ -0,0 +1,12 @@
+package xyz.driver.restquery.domain
+
+/**
+ * @param pageNumber Starts with 1
+ */
+final case class Pagination(pageSize: Int, pageNumber: Int)
+
+object Pagination {
+
+ // @see https://driverinc.atlassian.net/wiki/display/RA/REST+API+Specification#RESTAPISpecification-CommonRequestQueryParametersForWebServices
+ val Default = Pagination(pageSize = 100, pageNumber = 1)
+}
diff --git a/src/main/scala/xyz/driver/restquery/query/SearchFilterBinaryOperation.scala b/src/main/scala/xyz/driver/restquery/query/SearchFilterBinaryOperation.scala
new file mode 100644
index 0000000..dab466b
--- /dev/null
+++ b/src/main/scala/xyz/driver/restquery/query/SearchFilterBinaryOperation.scala
@@ -0,0 +1,5 @@
+package xyz.driver.restquery.query
+
+class SearchFilterBinaryOperation {
+
+}
diff --git a/src/main/scala/xyz/driver/restquery/query/SearchFilterExpr.scala b/src/main/scala/xyz/driver/restquery/query/SearchFilterExpr.scala
new file mode 100644
index 0000000..6d2cb9a
--- /dev/null
+++ b/src/main/scala/xyz/driver/restquery/query/SearchFilterExpr.scala
@@ -0,0 +1,196 @@
+package xyz.driver.restquery.domain
+
+sealed trait SearchFilterExpr {
+ def find(p: SearchFilterExpr => Boolean): Option[SearchFilterExpr]
+ def replace(f: PartialFunction[SearchFilterExpr, SearchFilterExpr]): SearchFilterExpr
+}
+
+object SearchFilterExpr {
+
+ val Empty: Intersection = Intersection.Empty
+ val Forbid = Atom.Binary(
+ dimension = Dimension(None, "true"),
+ op = SearchFilterBinaryOperation.Eq,
+ value = "false"
+ )
+
+ final case class Dimension(tableName: Option[String], name: String) {
+ def isForeign: Boolean = tableName.isDefined
+ }
+
+ sealed trait Atom extends SearchFilterExpr {
+ override def find(p: SearchFilterExpr => Boolean): Option[SearchFilterExpr] = {
+ Some(this).filter(p)
+ }
+
+ override def replace(f: PartialFunction[SearchFilterExpr, SearchFilterExpr]): SearchFilterExpr = {
+ if (f.isDefinedAt(this)) f(this)
+ else this
+ }
+ }
+
+ object Atom {
+ final case class Binary(dimension: Dimension, op: SearchFilterBinaryOperation, value: AnyRef) extends Atom
+ object Binary {
+ def apply(field: String, op: SearchFilterBinaryOperation, value: AnyRef): Binary =
+ Binary(Dimension(None, field), op, value)
+ }
+
+ final case class NAry(dimension: Dimension, op: SearchFilterNAryOperation, values: Seq[AnyRef]) extends Atom
+ object NAry {
+ def apply(field: String, op: SearchFilterNAryOperation, values: Seq[AnyRef]): NAry =
+ NAry(Dimension(None, field), op, values)
+ }
+
+ /** dimension.tableName extractor */
+ object TableName {
+ def unapply(value: Atom): Option[String] = value match {
+ case Binary(Dimension(tableNameOpt, _), _, _) => tableNameOpt
+ case NAry(Dimension(tableNameOpt, _), _, _) => tableNameOpt
+ }
+ }
+ }
+
+ final case class Intersection private (operands: Seq[SearchFilterExpr])
+ extends SearchFilterExpr with SearchFilterExprSeqOps {
+
+ override def replace(f: PartialFunction[SearchFilterExpr, SearchFilterExpr]): SearchFilterExpr = {
+ if (f.isDefinedAt(this)) f(this)
+ else {
+ this.copy(operands.map(_.replace(f)))
+ }
+ }
+
+ }
+
+ object Intersection {
+
+ val Empty = Intersection(Seq())
+
+ def create(operands: SearchFilterExpr*): SearchFilterExpr = {
+ val filtered = operands.filterNot(SearchFilterExpr.isEmpty)
+ filtered.size match {
+ case 0 => Empty
+ case 1 => filtered.head
+ case _ => Intersection(filtered)
+ }
+ }
+ }
+
+ final case class Union private (operands: Seq[SearchFilterExpr])
+ extends SearchFilterExpr with SearchFilterExprSeqOps {
+
+ override def replace(f: PartialFunction[SearchFilterExpr, SearchFilterExpr]): SearchFilterExpr = {
+ if (f.isDefinedAt(this)) f(this)
+ else {
+ this.copy(operands.map(_.replace(f)))
+ }
+ }
+
+ }
+
+ object Union {
+
+ val Empty = Union(Seq())
+
+ def create(operands: SearchFilterExpr*): SearchFilterExpr = {
+ val filtered = operands.filterNot(SearchFilterExpr.isEmpty)
+ filtered.size match {
+ case 0 => Empty
+ case 1 => filtered.head
+ case _ => Union(filtered)
+ }
+ }
+
+ def create(dimension: Dimension, values: String*): SearchFilterExpr = values.size match {
+ case 0 => SearchFilterExpr.Empty
+ case 1 => SearchFilterExpr.Atom.Binary(dimension, SearchFilterBinaryOperation.Eq, values.head)
+ case _ =>
+ val filters = values.map { value =>
+ SearchFilterExpr.Atom.Binary(dimension, SearchFilterBinaryOperation.Eq, value)
+ }
+
+ create(filters: _*)
+ }
+
+ def create(dimension: Dimension, values: Set[String]): SearchFilterExpr =
+ create(dimension, values.toSeq: _*)
+
+ // Backwards compatible API
+
+ /** Create SearchFilterExpr with empty tableName */
+ def create(field: String, values: String*): SearchFilterExpr =
+ create(Dimension(None, field), values: _*)
+
+ /** Create SearchFilterExpr with empty tableName */
+ def create(field: String, values: Set[String]): SearchFilterExpr =
+ create(Dimension(None, field), values)
+ }
+
+ case object AllowAll extends SearchFilterExpr {
+ override def find(p: SearchFilterExpr => Boolean): Option[SearchFilterExpr] = {
+ Some(this).filter(p)
+ }
+
+ override def replace(f: PartialFunction[SearchFilterExpr, SearchFilterExpr]): SearchFilterExpr = {
+ if (f.isDefinedAt(this)) f(this)
+ else this
+ }
+ }
+
+ case object DenyAll extends SearchFilterExpr {
+ override def find(p: SearchFilterExpr => Boolean): Option[SearchFilterExpr] = {
+ Some(this).filter(p)
+ }
+
+ override def replace(f: PartialFunction[SearchFilterExpr, SearchFilterExpr]): SearchFilterExpr = {
+ if (f.isDefinedAt(this)) f(this)
+ else this
+ }
+ }
+
+ def isEmpty(expr: SearchFilterExpr): Boolean = {
+ expr == Intersection.Empty || expr == Union.Empty
+ }
+
+ sealed trait SearchFilterExprSeqOps { this: SearchFilterExpr =>
+
+ val operands: Seq[SearchFilterExpr]
+
+ override def find(p: SearchFilterExpr => Boolean): Option[SearchFilterExpr] = {
+ if (p(this)) Some(this)
+ else {
+ // Search the first expr among operands, which satisfy p
+ // Is's ok to use foldLeft. If there will be performance issues, replace it by recursive loop
+ operands.foldLeft(Option.empty[SearchFilterExpr]) {
+ case (None, expr) => expr.find(p)
+ case (x, _) => x
+ }
+ }
+ }
+ }
+
+}
+
+sealed trait SearchFilterBinaryOperation
+
+object SearchFilterBinaryOperation {
+
+ case object Eq extends SearchFilterBinaryOperation
+ case object NotEq extends SearchFilterBinaryOperation
+ case object Like extends SearchFilterBinaryOperation
+ case object Gt extends SearchFilterBinaryOperation
+ case object GtEq extends SearchFilterBinaryOperation
+ case object Lt extends SearchFilterBinaryOperation
+ case object LtEq extends SearchFilterBinaryOperation
+
+}
+
+sealed trait SearchFilterNAryOperation
+
+object SearchFilterNAryOperation {
+
+ case object In extends SearchFilterNAryOperation
+ case object NotIn extends SearchFilterNAryOperation
+
+}
diff --git a/src/main/scala/xyz/driver/restquery/query/SearchFilterNAryOperation.scala b/src/main/scala/xyz/driver/restquery/query/SearchFilterNAryOperation.scala
new file mode 100644
index 0000000..0604c8f
--- /dev/null
+++ b/src/main/scala/xyz/driver/restquery/query/SearchFilterNAryOperation.scala
@@ -0,0 +1,5 @@
+package xyz.driver.restquery.query
+
+class SearchFilterNAryOperation {
+
+}
diff --git a/src/main/scala/xyz/driver/restquery/query/Sorting.scala b/src/main/scala/xyz/driver/restquery/query/Sorting.scala
new file mode 100644
index 0000000..e2642ad
--- /dev/null
+++ b/src/main/scala/xyz/driver/restquery/query/Sorting.scala
@@ -0,0 +1,56 @@
+package xyz.driver.restquery.domain
+
+import scala.collection.generic.CanBuildFrom
+
+sealed trait SortingOrder
+object SortingOrder {
+
+ case object Ascending extends SortingOrder
+ case object Descending extends SortingOrder
+
+}
+
+sealed trait Sorting
+
+object Sorting {
+
+ val Empty = Sequential(Seq.empty)
+
+ /**
+ * @param tableName None if the table is default (same)
+ * @param name Dimension name
+ * @param order Order
+ */
+ final case class Dimension(tableName: Option[String], name: String, order: SortingOrder) extends Sorting {
+ def isForeign: Boolean = tableName.isDefined
+ }
+
+ final case class Sequential(sorting: Seq[Dimension]) extends Sorting {
+ override def toString: String = if (isEmpty(this)) "Empty" else super.toString
+ }
+
+ def isEmpty(input: Sorting): Boolean = {
+ input match {
+ case Sequential(Seq()) => true
+ case _ => false
+ }
+ }
+
+ def filter(sorting: Sorting, p: Dimension => Boolean): Seq[Dimension] = sorting match {
+ case x: Dimension if p(x) => Seq(x)
+ case _: Dimension => Seq.empty
+ case Sequential(xs) => xs.filter(p)
+ }
+
+ def collect[B, That](sorting: Sorting)(f: PartialFunction[Dimension, B])(
+ implicit bf: CanBuildFrom[Seq[Dimension], B, That]): That = sorting match {
+ case x: Dimension if f.isDefinedAt(x) =>
+ val r = bf.apply()
+ r += f(x)
+ r.result()
+
+ case _: Dimension => bf.apply().result()
+ case Sequential(xs) => xs.collect(f)
+ }
+
+}