hq-json-0.1.1: src/HQ/Transformation/Parser.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE NoImplicitPrelude #-}
module HQ.Transformation.Parser where
import Control.Monad.Combinators.Expr (Operator (..), makeExprParser)
import HQ.JSON.Parser (jsonArray, jsonNumber, jsonObject, jsonText, jsonValueParser)
import HQ.Parser (Parser, keyword, parens, parseTop, symbol)
import HQ.Transformation
import Relude hiding (Compose, Const, and, isPrefixOf, length, many, not, or, reverse, some, subtract, xor)
import Text.Megaparsec
-- | Parse a single transformation expression, e.g. @+1 . == 3@.
parseTransformation :: Text -> Either (ParseErrorBundle Text Void) Transformation
parseTransformation = parseTop "transformation" transformationParser
-- | Grammar, loosest to tightest: @or@, @xor@, @and@, @.@, atoms.
transformationParser :: Parser Transformation
transformationParser = makeExprParser atomParser table
where
table =
[ [InfixL (Compose <$ symbol ".")],
[InfixL (keyword "and" And <|> (And <$ symbol "&&"))],
[InfixL (keyword "xor" Xor <|> (Xor <$ symbol "^^"))],
[InfixL (keyword "or" Or <|> (Or <$ symbol "||"))]
]
atomParser :: Parser Transformation
atomParser =
choice
[ parenParser,
constParser,
equalParser,
try strConcatParser,
addParser,
multiplyParser,
subtractParser,
divideParser,
try lteParser,
ltParser,
try gteParser,
gtParser,
arrayConcatParser,
trimParser,
notParser,
replaceParser,
stripPrefixParser,
stripSuffixParser,
isPrefixOfParser,
isSuffixOfParser,
isInfixOfParser,
isEmptyParser,
lengthParser,
reverseParser,
uniqueParser,
sortParser,
mergeParser,
deepMergeParser
]
parenParser :: Parser Transformation
parenParser = parens transformationParser
constParser :: Parser Transformation
constParser = Const <$> (keyword "const" () *> jsonValueParser)
equalParser :: Parser Transformation
equalParser = do
void $ try (symbol "==") <|> symbol "="
Equal <$> jsonValueParser
addParser :: Parser Transformation
addParser = prefixOp "+" jsonNumber Add
multiplyParser :: Parser Transformation
multiplyParser = prefixOp "*" jsonNumber Multiply
subtractParser :: Parser Transformation
subtractParser = prefixOp "-" jsonNumber Subtract
divideParser :: Parser Transformation
divideParser = prefixOp "/" jsonNumber Divide
ltParser :: Parser Transformation
ltParser = prefixOp "<" jsonNumber Lt
lteParser :: Parser Transformation
lteParser = prefixOp "<=" jsonNumber Lte
-- | @> v@ is sugar for @not . <= v@.
gtParser :: Parser Transformation
gtParser = prefixOp ">" jsonNumber (Compose Not . Lte)
-- | @>= v@ is sugar for @not . < v@.
gteParser :: Parser Transformation
gteParser = prefixOp ">=" jsonNumber (Compose Not . Lt)
strConcatParser :: Parser Transformation
strConcatParser = prefixOp "++" jsonText ConcatString
arrayConcatParser :: Parser Transformation
arrayConcatParser = prefixWordOp "concat" jsonArray ConcatArray
trimParser :: Parser Transformation
trimParser = keyword "trim" Trim
notParser :: Parser Transformation
notParser = keyword "not" Not
replaceParser :: Parser Transformation
replaceParser = Replace <$> (keyword "replace" () *> jsonText) <*> jsonText
stripPrefixParser :: Parser Transformation
stripPrefixParser = prefixWordOp "stripPrefix" jsonText StripPrefix
stripSuffixParser :: Parser Transformation
stripSuffixParser = prefixWordOp "stripSuffix" jsonText StripSuffix
isPrefixOfParser :: Parser Transformation
isPrefixOfParser = prefixWordOp "isPrefixOf" jsonText IsPrefixOf
isSuffixOfParser :: Parser Transformation
isSuffixOfParser = prefixWordOp "isSuffixOf" jsonText IsSuffixOf
isInfixOfParser :: Parser Transformation
isInfixOfParser = prefixWordOp "isInfixOf" jsonText IsInfixOf
isEmptyParser :: Parser Transformation
isEmptyParser = keyword "isEmpty" IsEmpty
lengthParser :: Parser Transformation
lengthParser = keyword "length" ArrayLength
reverseParser :: Parser Transformation
reverseParser = keyword "reverse" ArrayReverse
uniqueParser :: Parser Transformation
uniqueParser = keyword "unique" ArrayUnique
-- NOTE: 'sort' is a bare keyword with no prefix relations today, so it
-- needs no 'try'. If a longer 'sort...'-prefixed atom (e.g. @sortOn@)
-- is ever added, it must come first with 'try', like '++' vs '+'.
sortParser :: Parser Transformation
sortParser = keyword "sort" ArraySort
mergeParser :: Parser Transformation
mergeParser = prefixWordOp "merge" jsonObject Merge
deepMergeParser :: Parser Transformation
deepMergeParser = prefixWordOp "deepMerge" jsonObject DeepMerge
-- | Parse @OP operand@ and apply the constructor: shared shape of the
-- symbolic and single-operand parsers.
prefixOp :: Text -> Parser a -> (a -> Transformation) -> Parser Transformation
prefixOp op operand constr = constr <$> (symbol op *> operand)
-- | Word-operator variant of 'prefixOp': the operator must end on a
-- word boundary so @sortOn@/@concatenate@ don't lex as @sort@/@concat@
-- plus residue. Symbolic operators ('+', '++', '<='...) keep 'prefixOp'
-- since their operands ('+1', '"x"') may follow without a space.
prefixWordOp :: Text -> Parser a -> (a -> Transformation) -> Parser Transformation
prefixWordOp op operand constr = constr <$> (keyword op () *> operand)