jordan-servant-0.1.0.0: lib/Jordan/Servant/Query/Parse.hs
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TupleSections #-}
-- | Turn your jordan parsers into query-string parsers.
--
-- This module should be considered internal.
-- Import Jordan.Servant.Query instead.
module Jordan.Servant.Query.Parse where
import Control.Applicative (Alternative (..), Applicative (..))
import Control.Monad
import Control.Parallel
import qualified Data.Attoparsec.ByteString as AP
import Data.Bifunctor
import qualified Data.ByteString as BS
import Data.Functor
import Data.Maybe (mapMaybe)
import Data.Monoid
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8', encodeUtf8)
import Debug.Trace
import GHC.Generics
import Jordan (parseViaAttoparsec)
import Jordan.FromJSON.Class
import Jordan.FromJSON.Internal.Permutation
import Jordan.Types.Internal.AccumE
import Jordan.Types.JSONError
import Network.HTTP.Types.URI
data QueryKeyComponent
= RawValue T.Text
| EmptyBraces
| BracedValue T.Text
deriving (Show, Read, Eq, Ord, Generic)
newtype QueryKey = QueryKey {queryKeyComponents :: [QueryKeyComponent]}
deriving (Show, Read, Eq, Ord, Generic)
labelParser = (AP.<?>)
escapedBrace :: AP.Parser BS.ByteString
escapedBrace = do
AP.string "]]" `labelParser` "escaped ending brace"
("]" <>) <$> afterBrace
afterBrace :: AP.Parser BS.ByteString
afterBrace = do
taken <- AP.takeWhile (/= 93)
AP.string "]"
pure (urlDecode False taken)
escapedBraceEnding :: AP.Parser BS.ByteString
escapedBraceEnding = do
r <- AP.string "[[" $> "["
(r <>) <$> unbracedValueInner
unbracedValueInner :: AP.Parser BS.ByteString
unbracedValueInner = do
keyChars <- AP.takeWhile (/= 91) `labelParser` "until starting brace"
pure $ urlDecode False keyChars
unbracedValue :: AP.Parser T.Text
unbracedValue = do
inner <- unbracedValueInner
case decodeUtf8' inner of
Left err -> fail (show err)
Right res
| res == mempty -> fail "empty unbraced value, invalid"
| otherwise -> pure res
bracedValue :: AP.Parser T.Text
bracedValue = do
AP.word8 91 `labelParser` "starting brace"
after <- afterBrace
case decodeUtf8' after of
Left err -> fail $ show err
Right txt -> pure txt
emptyBraces = do
AP.string "[]"
m <- AP.peekWord8
case m of
Nothing -> pure EmptyBraces
Just 93 -> fail "escaped brace"
Just _ -> pure EmptyBraces
queryComponent :: AP.Parser QueryKeyComponent
queryComponent =
emptyBraces
<|> (RawValue <$> unbracedValue)
<|> (BracedValue <$> bracedValue)
parseQueryKey :: AP.Parser QueryKey
parseQueryKey = QueryKey <$> some queryComponent
newtype QueryParser a = QueryParser
{ runQueryParser ::
(QueryKey -> Maybe QueryKey) ->
[(QueryKey, Maybe BS.ByteString)] ->
Either String (a, [(QueryKey, Maybe BS.ByteString)])
}
deriving instance Functor QueryParser
instance Applicative QueryParser where
pure a = QueryParser $ \_ q -> Right (a, q)
(<*>) = ap
instance Monad QueryParser where
cb >>= transform = QueryParser $ \read q ->
case runQueryParser cb read q of
Left s -> Left s
Right (a, q') -> runQueryParser (transform a) read q'
-- | Alternative tries the left, than the right.
--
-- Both brances will be sparked off and tried in parallel.
instance Alternative QueryParser where
empty = QueryParser $ \_ _ -> Left "Alternative.Empty"
lhs <|> rhs = QueryParser $ \bs q ->
let lhsR = runQueryParser lhs bs q
rhsR = runQueryParser rhs bs q
in rhsR `par` lhsR `pseq` lhsR <|> rhsR
peekTransformedKeyHead :: QueryParser QueryKey
peekTransformedKeyHead = QueryParser $ \transform q ->
case q of
[] -> Left "no query elements to peek"
r@((k, _) : rest) ->
case transform k of
Nothing -> Left "could not transform head"
Just v -> Right (v, r)
readBracketKey :: QueryKey -> QueryParser (T.Text, QueryKeyComponent)
readBracketKey (QueryKey query) = do
case query of
(b@(BracedValue val) : rest) -> pure (val, b)
(b@EmptyBraces : rest) -> pure (mempty, b)
_ -> failParse "no braced value to consume"
modifyTransformed :: (QueryKey -> Maybe QueryKey) -> QueryParser a -> QueryParser a
modifyTransformed tf (QueryParser cb) = QueryParser $ \transform key ->
cb (transform >=> tf) key
droppingItem :: QueryKeyComponent -> QueryParser a -> QueryParser a
droppingItem item = modifyTransformed modify
where
modify (QueryKey (i : xs))
| i == item = Just (QueryKey xs)
| otherwise = Nothing
modify _ = Nothing
droppingFirst :: QueryParser a -> QueryParser a
droppingFirst = modifyTransformed dropHead
where
dropHead (QueryKey (x : xs)) = Just (QueryKey xs)
dropHead _ = Nothing
asTuple :: QueryParser a -> QueryParser (T.Text, a)
asTuple qp = do
keyHead <- peekTransformedKeyHead
(parsedText, item) <- readBracketKey keyHead
(parsedText,) <$> droppingItem item qp
getTransform :: QueryParser (QueryKey -> Maybe QueryKey)
getTransform = QueryParser $ curry Right
failParse msg = QueryParser $ \_ _ -> Left msg
-- | Take the value at the head, ensuring along the way that the entire query matches.
takeValue :: QueryParser (Maybe BS.ByteString)
takeValue = QueryParser $ \transform queries ->
case queries of
[] -> Left "no values to take"
((k, v) : rest) ->
case transform k of
Nothing -> Left "bad transform"
Just (QueryKey []) -> Right (v, rest)
Just r -> Left $ "more query elements to consume: " <> show r
takeElement :: QueryParser BS.ByteString
takeElement = do
r <- takeValue
case r of
Nothing -> failParse "No value"
Just bs -> pure bs
takeNull :: QueryParser ()
takeNull = do
r <- takeValue
case r of
Nothing -> pure ()
Just x
| x == "null" -> pure ()
| otherwise -> failParse "encountered values where null was expected"
orParseError :: (Show err) => QueryParser (Either err a) -> QueryParser a
orParseError (QueryParser cb) = QueryParser $ \transform query ->
case cb transform query of
Left s -> Left s
Right (r, q) -> case r of
Left err -> Left $ show err
Right a -> Right (a, q)
{-
manyEnding parser = many_v
where
many_v = some_v <|> (endpure []
some_v = liftA2 (:) parser many_v
-}
ensureConsumes :: QueryParser a -> QueryParser a
ensureConsumes a = QueryParser $ \transform q ->
case runQueryParser a transform q of
Left s -> Left s
r@(Right (res, nq))
| nq == q -> Left "did not consume input"
| otherwise -> r
newtype JordanQueryParser a = JordanQueryParser {runJordanQueryParser :: QueryParser a}
deriving (Functor, Applicative) via QueryParser
deriving (Semigroup) via (Alt QueryParser a)
newtype JordanQueryObjectParser a = JordanQueryObjectParser {runJordanQueryObjectParser :: Permutation QueryParser a}
deriving (Functor, Applicative) via (Permutation QueryParser)
addArrayBrackets :: QueryParser q -> QueryParser q
addArrayBrackets = modifyTransformed cb
where
cb (QueryKey (EmptyBraces : rest)) = Just (QueryKey rest)
cb _ = Nothing
addJSONKey :: T.Text -> QueryParser q -> QueryParser q
addJSONKey k = modifyTransformed cb
where
cb (QueryKey (BracedValue val : rest))
| val == k = Just (QueryKey rest)
| otherwise = Nothing
cb _ = Nothing
instance JSONObjectParser JordanQueryObjectParser where
parseFieldWith field = \(JordanQueryParser q) ->
JordanQueryObjectParser $
asPermutation $
addJSONKey field q
parseFieldWithDefault f = \(JordanQueryParser q) def ->
JordanQueryObjectParser $
asPermutationWithDefault
(addJSONKey f q)
def
toBool :: BS.ByteString -> Either JSONError Bool
toBool "t" = pure True
toBool "true" = pure True
toBool "f" = pure False
toBool "false" = pure False
toBool _ = Left ErrorInvalidJSON
takeText :: QueryParser T.Text
takeText =
orParseError $
decodeUtf8' <$> takeElement
instance JSONTupleParser JordanQueryParser where
consumeItemWith = \x -> x
instance JSONParser JordanQueryParser where
parseTuple (JordanQueryParser p) =
JordanQueryParser $
addArrayBrackets p
parseObject = \(JordanQueryObjectParser perm) ->
JordanQueryParser $
asParser perm
parseArrayWith = \(JordanQueryParser parse) ->
JordanQueryParser $ many $ ensureConsumes (addArrayBrackets parse)
parseNull = JordanQueryParser takeNull
parseText = JordanQueryParser takeText
parseBool =
JordanQueryParser $
orParseError $ toBool <$> takeElement
parseDictionary = \(JordanQueryParser parseValue) ->
JordanQueryParser $ many $ ensureConsumes (asTuple parseValue)
validateJSON (JordanQueryParser qp) =
JordanQueryParser $
orParseError qp
parseNumber =
JordanQueryParser $
orParseError $
first (const "not a valid number")
. parseViaAttoparsec
<$> takeElement
parseQueryToKeys :: Query -> [(QueryKey, Maybe BS.ByteString)]
parseQueryToKeys = mapMaybe transformElement
where
transformElement (k, v) = case AP.parseOnly parseQueryKey k of
Left s -> Nothing
Right qk -> Just (qk, v)
filterToStarting :: T.Text -> [(QueryKey, Maybe BS.ByteString)] -> [(QueryKey, Maybe BS.ByteString)]
filterToStarting key = mapMaybe keepElement
where
keepElement (k, v) =
case k of
(QueryKey (RawValue r : rest))
| r == key -> Just (QueryKey rest, v)
| otherwise -> Nothing
_ -> Nothing
transformToKey key = filterToStarting key . parseQueryToKeys
-- | Use Jordan to parse a query at a given \"base\" key.
--
-- We need a base key in case the JSON type is \"just an int\" or something.
parseQueryAtKeyWith ::
-- | JSON parser to use.
-- Note the rank-N type.
(forall jsonParser. (JSONParser jsonParser) => jsonParser a) ->
-- | Base key to use in the query string.
T.Text ->
-- | Query string
Query ->
-- | Either a value, or a brief (not super helpful) description of what went wrong.
Either String a
parseQueryAtKeyWith (JordanQueryParser (QueryParser q)) key queryString = do
fst <$> q Just (transformToKey key queryString)
-- | Determine if there are any query keys that match this base key.
--
-- >>> hasQueryAtKey "foo" (parseQuery "foo[bar][baz]=true")
-- True
--
-- >>> hasQueryAtKey "foo" (parseQuery "bar[baz]=true&bar[foo]=true&foo=true")
-- True
--
-- >>> hasQueryAtKey "foo" (parseQuery "bar[baz]=true&bar[foo]=true")
-- False
hasQueryAtKey :: T.Text -> Query -> Bool
hasQueryAtKey k q = not (null $ transformToKey k q)
-- | Like 'parseQueryAtKeyWith', but uses the 'FromJSON' instance, which is what you want 90% of the time.
parseQueryAtKey :: (FromJSON a) => T.Text -> Query -> Either String a
parseQueryAtKey = parseQueryAtKeyWith fromJSON