arxiv-client-0.1.0.0: src/Arxiv/Query/Parser.hs
{-# LANGUAGE OverloadedStrings #-}
-- | Parse algebraic query expressions
module Arxiv.Query.Parser
( parseQueryTerm
, queryTermParser
, runQueryTermTests
) where
import Arxiv.Query.Algebraic
import Control.Applicative (asum)
import Control.Monad (forM_)
import Data.Text (Text, pack)
import Data.Void (Void)
import Text.Megaparsec
import Text.Megaparsec.Char
import qualified Data.Text.IO as TIO
data AndOrBracket a
= AndTerm !(AndOrBracket a) !(AndOrBracket a)
| OrTerm !(AndOrBracket a) !(AndOrBracket a)
| NotTerm !(AndOrBracket a)
| BracketTerm !(AndOrBracket a)
| SingleTerm !a
deriving (Show, Eq)
queryTermReduction :: AndOrBracket QueryTerm -> QueryTerm
queryTermReduction (AndTerm t1 t2) = And (queryTermReduction t1) (queryTermReduction t2)
queryTermReduction (OrTerm t1 t2) = Or (queryTermReduction t1) (queryTermReduction t2)
queryTermReduction (NotTerm t) = Not (queryTermReduction t)
queryTermReduction (BracketTerm t) = queryTermReduction t
queryTermReduction (SingleTerm t) = t
andOrBracketParser :: Int -> (Char, Char) -> Parsec Void Text a -> Parsec Void Text (AndOrBracket a)
andOrBracketParser !braLevel (bra, ket) termParser = asum
[
try $ (AndTerm <$> bracketTermP) <* space <* string "&&" <* space
<*> andOrBracketParser braLevel (bra, ket) termParser
, try $ (OrTerm <$> bracketTermP) <* space <* string "||" <* space
<*> andOrBracketParser braLevel (bra, ket) termParser
, bracketTermP
, try $ (AndTerm . SingleTerm <$> termParser) <* space <* string "&&" <* space
<*> andOrBracketParser braLevel (bra, ket) termParser
, try $ (OrTerm . SingleTerm <$> termParser) <* space <* string "||" <* space
<*> andOrBracketParser braLevel (bra, ket) termParser
, NotTerm <$> (string "not" >> space1 >> andOrBracketParser braLevel (bra, ket) termParser)
, SingleTerm <$> termParser
] where bracketTermP = do
_ <- char bra
term <- andOrBracketParser (braLevel + 1) (bra, ket) termParser
_ <- char ket
return (BracketTerm term)
{- example:
title is coleman ---> Title (Is "coleman")
title has "coleman integral" ---> Title (Has "coleman integral")
ands [<queryTerm>, ...] ---> Ands [<queryTerm>, ...]
-}
queryTermParser :: Parsec Void Text QueryTerm
queryTermParser = asum
[ titleParser
, abstractParser
, authorParser
, categoryParser
, anywhereParser
, fmap Ands $ string "ands" >> space >> listOf queryTermParser
, fmap Ors $ string "ors" >> space >> listOf queryTermParser
, AnyWhere . Has <$> quotedTextParser -- without any prefix, "text" is treated as anywhere has "text"
]
where
titleParser = string "title" >> space1 >> Title <$> isHasAnyAllParser
abstractParser = string "abstract" >> space1 >> Abstract <$> isHasAnyAllParser
authorParser = string "author" >> space1 >> Author <$> isHasAnyAllParser
categoryParser = string "category" >> space1 >> Category <$> isHasAnyAllParser
anywhereParser = string "anywhere" >> space1 >> AnyWhere <$> isHasAnyAllParser
isHasAnyAllParser = asum
[ Is <$> (string "is" >> space1 >> quotedTextParser)
, Has <$> (string "has" >> space1 >> quotedTextParser)
, Any <$> (string "any" >> space1 >> listOf quotedTextParser)
, All <$> (string "all" >> space1 >> listOf quotedTextParser)
]
quotedTextParser = pack <$> (char '"' >> manyTill anySingle (char '"'))
listOf p = between (char '[') (char ']') (sepBy p (char ',' >> space))
fullQueryTermParser :: Parsec Void Text QueryTerm
fullQueryTermParser = queryTermReduction <$> andOrBracketParser 0 ('(', ')') queryTermParser
parseQueryTerm :: Text -> Either (ParseErrorBundle Text Void) QueryTerm
parseQueryTerm = runParser (fullQueryTermParser <* eof) ""
queryTermTestItem :: Text -> QueryTerm -> Maybe String
queryTermTestItem input expected =
case runParser (fullQueryTermParser <* eof) "" input of
Left err -> Just ("Parse error: " ++ errorBundlePretty err)
Right actual ->
if actual == expected
then Nothing
else Just ("Expected: " ++ show expected ++ ", but got: " ++ show actual)
queryTermTests :: [(Text, QueryTerm)]
queryTermTests =
[ ("title is \"coleman\"", Title (Is "coleman"))
, ("abstract has \"p-adic integral\"", Abstract (Has "p-adic integral"))
, ("author any [\"john doe\", \"jane smith\"]", Author (Any ["john doe", "jane smith"]))
, ("category all [\"math.NT\", \"math.AG\"]", Category (All ["math.NT", "math.AG"]))
, ("anywhere is \"quantum mechanics\"", AnyWhere (Is "quantum mechanics"))
, ("ands [title is \"coleman\", author has \"doe\"]", Ands [Title (Is "coleman"), Author (Has "doe")])
, ("ors [category is \"math.NT\", category is \"math.AG\"]", Ors [Category (Is "math.NT"), Category (Is "math.AG")])
, ("(title has \"haskell\" && author has \"simon\")",
And
(Title (Has "haskell"))
(Author (Has "simon")))
, ("(title has \"haskell\" && author has \"simon\") || abstract has \"functional programming\"",
Or
(And
(Title (Has "haskell"))
(Author (Has "simon")))
(Abstract (Has "functional programming")))
, ("\"haskell\" && \"simon\"",
And
(AnyWhere (Has "haskell"))
(AnyWhere (Has "simon")))
, ("not (title is \"quantum\")",
Not
(Title (Is "quantum")))
]
runQueryTermTests :: IO ()
runQueryTermTests = do
let results = map (\(input, expected) -> (input, queryTermTestItem input expected)) queryTermTests
let failures = [(input, err) | (input, Just err) <- results]
if null failures
then putStrLn "All query term parser tests passed."
else do
putStrLn "Some query term parser tests failed:"
forM_ failures $ \(input, err) -> do
putStr "Input: " >> TIO.putStr input
putStrLn $ "\nError: " ++ err