aboutsummaryrefslogtreecommitdiff
path: root/src/main/scala/xyz/driver/restquery/query/SearchFilterExpr.scala
blob: 6d2cb9ac09e19fbcfe0bf8cc91636a8901baddc5 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
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

}