aboutsummaryrefslogblamecommitdiff
path: root/src/main/scala/xyz/driver/restquery/rest/parsers/SearchFilterParser.scala
blob: 13f82a6047c6a6ff8313af519c6fbdf1766c2d61 (plain) (tree)
1
2
3
4
5
6
7
8
9
                                         
 

                     

                            
                                                                                                            

                                         






                                                                                               
                                                                                                             
                                                        
                                                                      
 
                                                                
                                                                                          







                                                                                                              

                                                                    

                                                       
                               
                                                                                


     

                                        
                                         


                         

                                                                                   
 

                                                                               
 
                                                                    



                                                             
                                                                                   
                                                                 

                                                                               




                                                      




                                                                                       


                                                       





                                                                                                    

   





                                                                                   





                                                                           
                                                                                                             
 






                                                                            


                                                                                    
                                                                                           
 


                                                                    



                                                                                                            


                                                                  

                 

                                                                   

     
                                                                         
                                        
                                                                                           


                                                                                          





                                                                     
                                             

                                                                    

                                                            





                                                                                              

                                                                     


                                         

      

                                                                        

                                        
                         
                                      


                                                                              




                                                                 
                                                                          













                                                                                
                                                                                   

                                                             


   
package xyz.driver.restquery.rest.parsers

import java.util.UUID

import fastparse.all._
import fastparse.core.Parsed
import xyz.driver.restquery.query.{SearchFilterBinaryOperation, SearchFilterExpr, SearchFilterNAryOperation}
import xyz.driver.restquery.utils.Utils
import xyz.driver.restquery.utils.Utils._

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, Any))): Option[SearchFilterExpr.Atom.Binary] = {
      val (dimensionName, (strOperation, value)) = input
      val updatedValue                           = trimIfString(value)

      parseBinaryOperation(strOperation.toLowerCase).map { op =>
        SearchFilterExpr.Atom.Binary(dimensionName, op, updatedValue.asInstanceOf[AnyRef])
      }
    }
  }

  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
      val updatedValues                       = xs.map(trimIfString)

      parseNAryOperation(strOperation.toLowerCase).map(
        op =>
          SearchFilterExpr.Atom
            .NAry(dimensionName, op, updatedValues.map(_.asInstanceOf[AnyRef])))
    }
  }

  private def trimIfString(value: Any) =
    value match {
      case s: String => Utils.safeTrim(s)
      case a         => a
    }

  private def parseBinaryOperation: String => Option[SearchFilterBinaryOperation] =
    SearchFilterBinaryOperation.binaryOperationsFromString.get

  private def parseNAryOperation: String => Option[SearchFilterNAryOperation] =
    SearchFilterNAryOperation.nAryOperationsFromString.get

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

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

  private val commonOperatorParser: Parser[String] = {
    import xyz.driver.restquery.query.SearchFilterBinaryOperation.binaryOperationToName
    val eq    = binaryOperationToName(SearchFilterBinaryOperation.Eq)
    val like  = binaryOperationToName(SearchFilterBinaryOperation.Like)
    val noteq = binaryOperationToName(SearchFilterBinaryOperation.NotEq)
    P(IgnoreCase(eq) | IgnoreCase(like) | IgnoreCase(noteq)).!
  }

  private val numericOperatorParser: Parser[String] = {
    import xyz.driver.restquery.query.SearchFilterBinaryOperation.binaryOperationToName
    val eq    = binaryOperationToName(SearchFilterBinaryOperation.Eq)
    val noteq = binaryOperationToName(SearchFilterBinaryOperation.NotEq)
    val gt    = binaryOperationToName(SearchFilterBinaryOperation.Gt)
    val lt    = binaryOperationToName(SearchFilterBinaryOperation.Lt)
    P(IgnoreCase(eq) | IgnoreCase(noteq) | ((IgnoreCase(gt) | IgnoreCase(lt)) ~ IgnoreCase(eq).?)).!
  }

  private val naryOperatorParser: Parser[String] = {
    import xyz.driver.restquery.query.SearchFilterNAryOperation.nAryOperationToName
    val in    = nAryOperationToName(SearchFilterNAryOperation.In)
    val notin = nAryOperationToName(SearchFilterNAryOperation.NotIn)
    P(IgnoreCase(in) | IgnoreCase(notin)).!
  }

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

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

  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 longParser: Parser[Long] = P(CharIn('0' to '9').rep(min = 1).!.map(_.toLong))

  private val booleanParser: Parser[Boolean] =
    P((IgnoreCase("true") | IgnoreCase("false")).!.map(_.toBoolean))

  private val hexDigit: Parser[String] = P((CharIn('a' to 'f') | CharIn('A' to 'F') | CharIn('0' to '9')).!)

  private val uuidParser: Parser[UUID] =
    P(
      hexDigit.rep(8).! ~ "-" ~ hexDigit.rep(4).! ~ "-" ~ hexDigit
        .rep(4)
        .! ~ "-" ~ hexDigit.rep(4).! ~ "-" ~ hexDigit
        .rep(12)
        .!).map {
      case (group1, group2, group3, group4, group5) =>
        UUID.fromString(s"$group1-$group2-$group3-$group4-$group5")
    }

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

  private val nAryAtomParser: Parser[SearchFilterExpr.Atom.NAry] = P(
    dimensionParser ~ whitespaceParser ~ (
      naryOperatorParser ~ whitespaceParser ~
        ((longParser.rep(min = 1, sep = ",") ~ End) | (booleanParser
          .rep(min = 1, sep = ",") ~ End) |
          (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: ${fastparse.core.ParseError
      .msg(e.extra.input, e.extra.traced.expected, e.index)}"
  }

}