{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE ViewPatterns #-} module Gargantext.Core.Text.Corpus.Query ( Query -- * opaque , RawQuery(..) , Limit(..) , QueryTerm(..) , getQuery , parseQuery , mapQuery , renderQuery , renderQueryTerm , interpretQuery , ExternalAPIs(..) , module BoolExpr -- * Useful for testing , unsafeMkQuery ) where import Data.Bifunctor import Data.String import Gargantext.API.Admin.Orchestrator.Types import Gargantext.Core.Types import Prelude import Text.ParserCombinators.Parsec import Test.QuickCheck import qualified Data.Aeson as Aeson import Data.BoolExpr as BoolExpr import Data.BoolExpr.Parser as BoolExpr import Data.BoolExpr.Printer as BoolExpr import qualified Data.Swagger as Swagger import qualified Data.Text as T import qualified Servant.API as Servant import qualified Text.Parsec as P -- | A raw query, as typed by the user from the frontend. newtype RawQuery = RawQuery { getRawQuery :: T.Text } deriving newtype ( Show, Eq, IsString , Servant.FromHttpApiData, Servant.ToHttpApiData , Aeson.FromJSON, Aeson.ToJSON , Swagger.ToParamSchema, Swagger.ToSchema) instance Arbitrary RawQuery where arbitrary = RawQuery <$> arbitrary -- | A limit to the number of results we want to retrieve. newtype Limit = Limit { getLimit :: Int } deriving newtype ( Show, Eq, Num , Servant.FromHttpApiData, Servant.ToHttpApiData , Aeson.FromJSON, Aeson.ToJSON , Swagger.ToParamSchema, Swagger.ToSchema) -- | A /query/ term, i.e. a node of the query expression tree which can be -- either a Gargantext 'Term' (i.e. just a textual value) or something else, -- like a partial match (i.e. the user is asking to perform a search that would -- match only a suffix of a word). data QueryTerm = QT_exact_match Term | QT_partial_match Term deriving (Show, Eq, Ord) instance IsString QueryTerm where fromString input = case P.runParser queryTermToken () "Corpus.Query.fromString" input of Left _ -> QT_exact_match (Term $ T.pack input) Right [qt] -> qt Right _ -> QT_exact_match (Term $ T.pack input) renderQueryTerm :: QueryTerm -> T.Text renderQueryTerm (QT_exact_match (Term t)) = t renderQueryTerm (QT_partial_match (Term t)) = t -- | An opaque wrapper around a 'Query' type which can be parsed from a boolean -- expression like (a AND b) OR c, and which can be interpreted in many ways -- according to the particular service we are targeting. newtype Query = Query { getQuery :: (BoolExpr.CNF [QueryTerm]) } deriving Show interpretQuery :: Query -> (BoolExpr.BoolExpr [QueryTerm] -> ast) -> ast interpretQuery (Query q) transform = transform . simplify . BoolExpr.fromCNF $ q simplify :: BoolExpr.BoolExpr a -> BoolExpr.BoolExpr a simplify expr = case expr of BAnd sub BTrue -> simplify sub BAnd BTrue sub -> simplify sub BAnd BFalse _ -> BFalse BAnd _ BFalse -> BFalse BAnd sub1 sub2 -> BAnd (simplify sub1) (simplify sub2) BOr _ BTrue -> BTrue BOr BTrue _ -> BTrue BOr sub BFalse -> simplify sub BOr BFalse sub -> simplify sub BOr sub1 sub2 -> BOr (simplify sub1) (simplify sub2) BNot BTrue -> BFalse BNot BFalse -> BTrue BNot (BNot sub) -> simplify sub BNot sub -> BNot (simplify sub) BTrue -> BTrue BFalse -> BFalse BConst signed -> BConst signed unsafeMkQuery :: BoolExpr.BoolExpr [QueryTerm] -> Query unsafeMkQuery = Query . BoolExpr.boolTreeToCNF queryTermToken :: CharParser st [QueryTerm] queryTermToken = do map mkQueryTerm <$> termToken where mkQueryTerm :: Term -> QueryTerm mkQueryTerm (Term (T.unpack -> t)) = case t of '"' : '~' : rest -> QT_partial_match (Term $ T.pack $ '"' : rest) '~' : rest -> QT_partial_match (Term $ T.pack $ '"' : rest) _ -> QT_exact_match (Term $ T.pack t) termToken :: CharParser st [Term] termToken = (try ((:[]) . Term . T.pack <$> BoolExpr.identifier) <|> (between dubQuote dubQuote multipleTerms)) where dubQuote = BoolExpr.symbol "\"" multipleTerms = map (Term . T.pack) <$> sepBy BoolExpr.identifier BoolExpr.whiteSpace -- | Parses an input 'Text' into a 'Query', reporting an error if it fails. parseQuery :: RawQuery -> Either String Query parseQuery (RawQuery txt) = bimap show (Query . BoolExpr.boolTreeToCNF) $ P.runParser (BoolExpr.parseBoolExpr queryTermToken) () "Corpus.Query" (T.unpack txt) renderQuery :: Query -> RawQuery renderQuery (Query cnf) = RawQuery . T.pack $ BoolExpr.boolExprPrinter (showsPrec 0) (BoolExpr.fromCNF cnf) "" mapQuery :: (QueryTerm -> QueryTerm) -> Query -> Query mapQuery f = Query . fmap (map f) . getQuery