strelka-2: library/Strelka/RequestBodyParsing/Parser.hs
module Strelka.RequestBodyParsing.Parser
where
import Strelka.Prelude hiding (fail)
import qualified Data.Attoparsec.ByteString
import qualified Data.Attoparsec.Text
import qualified Data.Attoparsec.Types
import qualified Data.ByteString
import qualified Data.ByteString.Lazy
import qualified Data.ByteString.Lazy.Internal
import qualified Data.ByteString.Builder
import qualified Data.Text
import qualified Data.Text.Encoding
import qualified Data.Text.Encoding.Error
import qualified Data.Text.Lazy
import qualified Data.Text.Lazy.Encoding
import qualified Data.Text.Lazy.Builder
import qualified Data.Text.Internal.Lazy
import qualified Data.HashMap.Strict as C
import qualified Strelka.ParamsParsing.Params as A
import qualified URLDecoders as B
import qualified Text.Builder as D
import qualified ByteString.TreeBuilder as E
{-|
A specification of how to consume the request body byte-stream.
-}
newtype Parser a =
Parser (IO ByteString -> IO (Either Text a))
deriving (Functor)
instance Applicative Parser where
pure =
return
(<*>) =
ap
instance Monad Parser where
return x =
Parser (\_ -> pure (Right x))
(>>=) (Parser def1) cont2 =
Parser def
where
def input =
def1 input >>= \case
Right result -> case cont2 result of Parser def2 -> def2 input
Left failure -> return (Left failure)
{-|
Result of a folding step.
-}
data Folded a =
Unfinished !a |
Finished !a |
Failed Text
deriving (Functor)
{-|
Fail with a message.
-}
{-# INLINE fail #-}
fail :: Text -> Parser a
fail message =
Parser (\_ -> return (Left message))
{-|
Fold with support for early termination and failure.
-}
{-# INLINABLE foldBytes #-}
foldBytes :: (a -> ByteString -> Folded a) -> a -> Parser a
foldBytes step init =
Parser consumer
where
consumer getChunk =
recur init
where
recur !state =
getChunk >>= onChunk
where
onChunk chunk =
if Data.ByteString.null chunk
then return (Right state)
else case step state chunk of
Unfinished newState -> recur newState
Finished newState -> return (Right newState)
Failed failure -> return (Left failure)
{-|
Fold with support for early termination and failure.
-}
{-# INLINABLE foldText #-}
foldText :: (a -> Text -> Folded a) -> a -> Parser a
foldText step init =
Parser consumer
where
consumer getChunk =
recur Data.Text.Encoding.streamDecodeUtf8 init
where
recur !decode !accumulator =
do
chunk <- getChunk
if Data.ByteString.null chunk
then return (Right accumulator)
else catch (decodeChunk chunk) fail
where
decodeChunk chunk =
case decode chunk of
Data.Text.Encoding.Some textChunk leftovers newDecode ->
if Data.Text.null textChunk
then recur newDecode accumulator
else case step accumulator textChunk of
Unfinished newAccumulator -> recur newDecode newAccumulator
Finished newAccumulator -> return (Right accumulator)
Failed failure -> return (Left failure)
fail (Data.Text.Encoding.Error.DecodeError message byte) =
return (Left ("UTF8 decoding failure: " <> fromString message))
{- |
Fold over the input chunks, projecting them into a monoid.
Similar to "Foldable"\'s 'foldMap'.
-}
{-# INLINE buildFromBytes #-}
buildFromBytes :: Monoid a => (ByteString -> a) -> Parser a
buildFromBytes proj =
foldBytes (\l r -> Unfinished (mappend l (proj r))) mempty
{- |
Fold over the input chunks, projecting them into a monoid.
Similar to "Foldable"\'s 'foldMap'.
-}
{-# INLINE buildFromText #-}
buildFromText :: Monoid a => (Text -> a) -> Parser a
buildFromText proj =
foldText (\l r -> Unfinished (mappend l (proj r))) mempty
{-|
Consume as ByteString.
-}
{-# INLINE bytes #-}
bytes :: Parser ByteString
bytes =
fmap E.toByteString bytesBuilder
{-|
Consume as a strict ByteString builder.
-}
{-# INLINE bytesBuilder #-}
bytesBuilder :: Parser E.Builder
bytesBuilder =
buildFromBytes E.byteString
{-|
Consume as lazy ByteString.
-}
{-# INLINE lazyBytes #-}
lazyBytes :: Parser Data.ByteString.Lazy.ByteString
lazyBytes =
fmap fromAccumulator (buildFromBytes toAccumulator)
where
toAccumulator chunk =
Endo (Data.ByteString.Lazy.Internal.Chunk chunk)
fromAccumulator (Endo fn) =
fn Data.ByteString.Lazy.Internal.Empty
{-|
Consume as a lazy ByteString builder.
-}
{-# INLINE lazyBytesBuilder #-}
lazyBytesBuilder :: Parser Data.ByteString.Builder.Builder
lazyBytesBuilder =
buildFromBytes Data.ByteString.Builder.byteString
{-|
Consume as Text.
-}
{-# INLINE text #-}
text :: Parser Text
text =
fmap D.run textBuilder
{-|
Consume as a strict Text builder.
-}
{-# INLINE textBuilder #-}
textBuilder :: Parser D.Builder
textBuilder =
buildFromText D.text
{-|
Consume as lazy Text.
-}
{-# INLINE lazyText #-}
lazyText :: Parser Data.Text.Lazy.Text
lazyText =
fmap fromAccumulator (buildFromText toAccumulator)
where
toAccumulator chunk =
Endo (Data.Text.Internal.Lazy.Chunk chunk)
fromAccumulator (Endo fn) =
fn Data.Text.Internal.Lazy.Empty
{-|
Consume as a lazy Text builder.
-}
{-# INLINE lazyTextBuilder #-}
lazyTextBuilder :: Parser Data.Text.Lazy.Builder.Builder
lazyTextBuilder =
buildFromText Data.Text.Lazy.Builder.fromText
{-|
Lift an Attoparsec ByteString parser.
Consumption is non-greedy and terminates when the parser is done.
-}
{-# INLINE parseBytes #-}
parseBytes :: Data.Attoparsec.ByteString.Parser a -> Parser a
parseBytes parser =
processParserResult foldBytes (Data.Attoparsec.ByteString.Partial (Data.Attoparsec.ByteString.parse parser))
{-|
Lift an Attoparsec Text parser.
Consumption is non-greedy and terminates when the parser is done.
-}
{-# INLINE parseText #-}
parseText :: Data.Attoparsec.Text.Parser a -> Parser a
parseText parser =
processParserResult foldText (Data.Attoparsec.Text.Partial (Data.Attoparsec.Text.parse parser))
{-|
Given a chunk-specialized terminating fold implementation lifts a generic Attoparsec result.
-}
{-# INLINE processParserResult #-}
processParserResult :: Monoid chunk => (forall a. (a -> chunk -> Folded a) -> a -> Parser a) -> Data.Attoparsec.Types.IResult chunk a -> Parser a
processParserResult fold result =
fold step result >>= finalise
where
step result chunk =
case result of
Data.Attoparsec.Types.Partial chunkToResult ->
Unfinished (chunkToResult chunk)
_ ->
Finished result
finalise =
\case
Data.Attoparsec.Types.Done leftovers resultValue ->
Parser (\_ -> return (Right resultValue))
Data.Attoparsec.Types.Fail leftovers contexts message ->
Parser (\_ -> return (Left (fromString (intercalate " > " contexts <> ": " <> message))))
Data.Attoparsec.Types.Partial chunkToResult ->
finalise (chunkToResult mempty)
{-|
Parses the input stream as \"application/x-www-form-urlencoded\".
-}
{-# INLINE parseParams #-}
parseParams :: A.Params a -> Parser a
parseParams parser =
do
queryBytes <- bytes
case B.utf8Query queryBytes of
Right query -> case A.run parser (flip C.lookup query) of
Right result -> return result
Left message -> fail ("Query params parsing error: " <> message)
Left message -> fail ("Query parsing error: " <> message)