aboutsummaryrefslogtreecommitdiff
path: root/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala
blob: 768e5f5a5d0f5338696dcbb4cb5ba4ba07f8a064 (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
package xyz.driver.pdsuicommon.parsers

import xyz.driver.pdsuicommon.utils.Implicits.{toCharOps, toStringOps}
import fastparse.all._
import fastparse.core.Parsed
import fastparse.parsers.Intrinsics.CharPred
import xyz.driver.pdsuicommon.db.{SearchFilterBinaryOperation, SearchFilterExpr, SearchFilterNAryOperation}

import scala.util.Try

@SuppressWarnings(Array("org.wartremover.warts.Product", "org.wartremover.warts.Serializable"))
object SearchFilterParser {

  private object BinaryAtomFromTuple {
    def unapply(input: (SearchFilterExpr.Dimension, (String, String))): Option[SearchFilterExpr.Atom.Binary] = {
      val (dimensionName, (strOperation, value)) = input
      parseOperation(strOperation.toLowerCase).map { op =>
        SearchFilterExpr.Atom.Binary(dimensionName, op, value.safeTrim)
      }
    }
  }

  private object NAryAtomFromTuple {
    // Compiler warning: unchecked since it is eliminated by erasure, if we user Seq[String]
    def unapply(input: (SearchFilterExpr.Dimension, (String, Seq[_]))): Option[SearchFilterExpr.Atom.NAry] = {
      val (dimensionName, (strOperation, xs)) = input
      if (strOperation.toLowerCase == "in") {
        val values = xs.asInstanceOf[Seq[String]].map(_.safeTrim)
        Some(SearchFilterExpr.Atom.NAry(dimensionName, SearchFilterNAryOperation.In, values))
      } else {
        None
      }
    }
  }

  private val operationsMapping = {
    import xyz.driver.pdsuicommon.db.SearchFilterBinaryOperation._

    Map[String, SearchFilterBinaryOperation](
      "eq"    -> Eq,
      "noteq" -> NotEq,
      "like"  -> Like,
      "gt"    -> Gt,
      "gteq"  -> GtEq,
      "lt"    -> Lt,
      "lteq"  -> LtEq
    )
  }

  private def parseOperation(x: String): Option[SearchFilterBinaryOperation] = operationsMapping.get(x)

  private val whitespaceParser = P(CharPred(_.isSafeWhitespace))

  val dimensionParser: Parser[SearchFilterExpr.Dimension] = {
    val identParser = P(
      CharPred(c => c.isLetterOrDigit)
        .rep(min = 1)).!.map(SearchFilterExpr.Dimension(None, _))
    val pathParser = P(identParser.! ~ "." ~ identParser.!) map {
      case (left, right) => SearchFilterExpr.Dimension(Some(left), right)
    }
    P(pathParser | identParser)
  }

  private val commonOperatorParser: Parser[String] = {
    P(IgnoreCase("eq") | IgnoreCase("like") | IgnoreCase("noteq")).!
  }

  private val numericOperatorParser: Parser[String] = {
    P((IgnoreCase("gt") | IgnoreCase("lt")) ~ IgnoreCase("eq").?).!
  }

  private val naryOperatorParser: Parser[String] = P(IgnoreCase("in")).!

  private val isPositiveParser: Parser[Boolean] = P(CharIn("-+").!.?).map {
    case Some("-") => false
    case _         => true
  }

  // Exclude Unicode "digits"
  private val digitsParser: Parser[String] = P(CharIn('0' to '9').rep(min = 1).!)

  // @TODO Make complex checking here
  private val numberParser: Parser[String] = P(isPositiveParser ~ digitsParser.! ~ ("." ~ digitsParser).!.?).map {
    case (false, intPart, Some(fracPart)) => s"-$intPart.${fracPart.tail}"
    case (false, intPart, None)           => s"-$intPart"
    case (_, intPart, Some(fracPart))     => s"$intPart.${fracPart.tail}"
    case (_, intPart, None)               => s"$intPart"
  }

  private val nAryValueParser: Parser[String] = P(CharPred(_ != ',').rep(min = 1).!)

  private val binaryAtomParser: Parser[SearchFilterExpr.Atom.Binary] = P(
    dimensionParser ~ whitespaceParser ~ (
      (commonOperatorParser.! ~/ whitespaceParser ~/ AnyChar.rep(min = 1).!)
        | (numericOperatorParser.! ~/ whitespaceParser ~/ numberParser.!)
    ) ~ End
  ).map {
    case BinaryAtomFromTuple(atom) => atom
  }

  private val nAryAtomParser: Parser[SearchFilterExpr.Atom.NAry] = P(
    dimensionParser ~ whitespaceParser ~ (
      naryOperatorParser ~/ whitespaceParser ~/ nAryValueParser.!.rep(min = 1, sep = ",")
    ) ~ End
  ).map {
    case NAryAtomFromTuple(atom) => atom
  }

  private val atomParser: Parser[SearchFilterExpr.Atom] = P(binaryAtomParser | nAryAtomParser)

  @deprecated("play-akka transition", "0")
  def parse(query: Map[String, Seq[String]]): Try[SearchFilterExpr] =
    parse(query.toSeq.flatMap {
      case (key, values) =>
        values.map(value => key -> value)
    })

  def parse(query: Seq[(String, String)]): Try[SearchFilterExpr] = Try {
    query.toList.collect { case ("filters", value) => value } match {
      case Nil => SearchFilterExpr.Empty

      case head :: Nil =>
        atomParser.parse(head) match {
          case Parsed.Success(x, _) => x
          case e: Parsed.Failure    => throw new ParseQueryArgException("filters" -> formatFailure(1, e))
        }

      case xs =>
        val parsed = xs.map(x => atomParser.parse(x))
        val failures: Seq[String] = parsed.zipWithIndex.collect {
          case (e: Parsed.Failure, index) => formatFailure(index, e)
        }

        if (failures.isEmpty) {
          val filters = parsed.collect {
            case Parsed.Success(x, _) => x
          }

          SearchFilterExpr.Intersection.create(filters: _*)
        } else {
          throw new ParseQueryArgException("filters" -> failures.mkString("; "))
        }
    }
  }

  private def formatFailure(sectionIndex: Int, e: Parsed.Failure): String = {
    s"section $sectionIndex: ${ParseError.msg(e.extra.input, e.extra.traced.expected, e.index)}"
  }

}