grpc-spec-1.0.0: src/Network/GRPC/Spec/Util/Parser.hs
-- | Incremental parser interface
--
-- Intended for qualified import.
--
-- > import Network.GRPC.Spec.Util.Parser (Parser)
-- > import Network.GRPC.Spec.Util.Parser qualified as Parser
module Network.GRPC.Spec.Util.Parser (
Parser -- opaque
-- * Construction
, consumeExactly
, getExactly
-- * Execution
, IsFinal
, Leftover
, ProcessResult(..)
, processAll
) where
import Control.Monad
import Data.Bifunctor
import Data.Binary (Get)
import Data.Binary.Get qualified as Binary
import Data.ByteString qualified as BS.Strict
import Data.ByteString qualified as Strict (ByteString)
import Data.ByteString.Lazy qualified as BS.Lazy
import Data.ByteString.Lazy qualified as Lazy (ByteString)
import Data.Int
{-------------------------------------------------------------------------------
Definition
-------------------------------------------------------------------------------}
-- | Simple incremental parser
--
-- This is used to parse a stream of values, where we know ahead of time for
-- each value how much data to expect (perhaps based on the previous value).
-- Individual values are not parsed incrementally; see 'consumeExactly' or
-- 'getExactly'.
newtype Parser e a = Parser {
runParser :: Accumulator -> Result e a
}
data Result e a =
-- | Parsing failed
--
-- This implies that we can stop parsing: getting more data won't fix the
-- problem (see also 'NeedData')
Failed e
-- | We make some partial progress, but we need more data to continue
| NeedData (Parser e a) Accumulator
-- | Parsing succeeded
--
-- Also returns the left-over data
| Done a Accumulator
instance Bifunctor Result where
bimap f _ (Failed e) = Failed (f e)
bimap _ g (Done a bs) = Done (g a) bs
bimap f g (NeedData p bs) = NeedData (bimap f g p) bs
instance Functor (Result e) where
fmap = second
instance Bifunctor Parser where
bimap f g (Parser p) = Parser (bimap f g . p)
instance Functor (Parser e) where
fmap = second
instance Applicative (Parser e) where
pure x = Parser $ Done x
(<*>) = ap
instance Monad (Parser e) where
f >>= g = Parser $ \bs ->
case runParser f bs of
Failed e -> Failed e
NeedData f' bs' -> NeedData (f' >>= g) bs'
Done a bs' -> runParser (g a) bs'
instance MonadFail (Parser String) where
fail err = Parser $ \_ -> Failed err
{-------------------------------------------------------------------------------
Accumulated input
This is an internal abstraction only; client code never sees this.
-------------------------------------------------------------------------------}
data Accumulator = Accumulator {
-- | All the chunks received so far, in reverse order
accumulatedChunks :: [Strict.ByteString]
-- | Total accumulated length
, accumulatedLength :: !Int64
}
nil :: Accumulator
nil = Accumulator {
accumulatedChunks = []
, accumulatedLength = 0
}
-- | Append chunks at the end of the accumulator
--
-- @O(1)@ (this is the raison d'être of this abstraction)
snoc :: Accumulator -> Strict.ByteString -> Accumulator
snoc acc chunk = Accumulator {
accumulatedChunks =
chunk : accumulatedChunks acc
, accumulatedLength =
accumulatedLength acc + fromIntegral (BS.Strict.length chunk)
}
toLazy :: Accumulator -> Lazy.ByteString
toLazy = BS.Lazy.fromChunks . reverse . accumulatedChunks
split :: Int64 -> Accumulator -> Maybe (Lazy.ByteString, Accumulator)
split n acc
| n > accumulatedLength acc
= Nothing
| otherwise
= let bs = toLazy acc
(front, back) = BS.Lazy.splitAt n bs
remainder = Accumulator {
accumulatedChunks = reverse (BS.Lazy.toChunks back)
, accumulatedLength = accumulatedLength acc - n
}
in Just (front, remainder)
{-------------------------------------------------------------------------------
Construction
-------------------------------------------------------------------------------}
-- | Consume a specified number of bytes
--
-- In order to use the t'Parser' interface we must know for each value exactly
-- how big it will be ahead of time. Typically this will be done by first
-- calling 'consumeExactly' for some kind of fixed size header, indicating how
-- big the value actual value is, which will then inform the next call to
-- 'consumeExactly'.
consumeExactly :: forall e a.
Int64 -- ^ Length
-> (Lazy.ByteString -> Either e a) -- ^ Parser
-> Parser e a
consumeExactly len parse = go
where
go :: Parser e a
go = Parser $ \acc ->
case split len acc of
Nothing -> NeedData go acc
Just (raw, left) -> either Failed (flip Done left) $ parse raw
-- | Convenience wrapper around 'consumeExactly'
getExactly :: Int64 -> Get a -> Parser String a
getExactly len get =
consumeExactly len $ either failed ok . Binary.runGetOrFail get
where
failed :: (Lazy.ByteString, Binary.ByteOffset, String) -> Either String a
failed (_, _, err) = Left err
ok :: (Lazy.ByteString, Binary.ByteOffset, a) -> Either String a
ok (unconsumed, lenConsumed, a)
| not (BS.Lazy.null unconsumed) = Left "Unconsumed data"
| lenConsumed /= len = error "impossible"
| otherwise = Right a
{-------------------------------------------------------------------------------
Execution
-------------------------------------------------------------------------------}
-- | Is this the final chunk in the input?
type IsFinal = Bool
-- | Leftover data
type Leftover = Lazy.ByteString
-- | Result from processing all chunks in the input
--
-- See 'processAll'.
data ProcessResult e b =
-- | Parse error during processing
ProcessError e
-- | Parsing succeeded (compare to 'ProcessedWithoutFinal')
| ProcessedWithFinal b Leftover
-- | Parsing succeeded, but we did not recognize the final message on time
--
-- There are two ways that parsing can terminate: the final few chunks may
-- look like this:
--
-- > chunk1 -- not marked final
-- > chunk2 -- not marked final
-- > chunk3 -- marked final
--
-- or like this:
--
-- > chunk1 -- not marked final
-- > chunk2 -- not marked final
-- > chunk3 -- not marked final
-- > empty chunk -- marked final
--
-- In the former case, we know that we are processing the final message /as/
-- we are processing it ('ProcessedWithFinal'); in the latter case, we
-- realize this only after we receive the final empty chunk.
| ProcessedWithoutFinal Leftover
-- | Process all incoming data
--
-- Returns any unprocessed data.
-- Also returns if we knew that the final result
-- was in fact the final result when we received it (this may or may not be the
-- case, depending on
processAll :: forall m e a b.
Monad m
=> m (Strict.ByteString, IsFinal) -- ^ Get next chunk
-> (a -> m ()) -- ^ Process value
-> (a -> m b) -- ^ Process final value
-> Parser e a -- ^ Parser
-> m (ProcessResult e b)
processAll getChunk processOne processFinal parser =
go $ runParser parser nil
where
go :: Result e a -> m (ProcessResult e b)
go (Failed err) = return $ ProcessError err
go (Done a left) = processOne a >> go (runParser parser left)
go (NeedData parser' left) = do
(bs, isFinal) <- getChunk
if not isFinal
then go $ runParser parser' (left `snoc` bs)
else goFinal [] $ runParser parser' (left `snoc` bs)
-- We have received the final chunk; extract all messages until we are done
goFinal :: [a] -> Result e a -> m (ProcessResult e b)
goFinal _ (Failed err) = return $ ProcessError err
goFinal acc (Done a left) = goFinal (a:acc) $ runParser parser left
goFinal acc (NeedData _ left) = do
mb <- processLastFew (reverse acc)
return $ case mb of
Just b -> ProcessedWithFinal b $ toLazy left
Nothing -> ProcessedWithoutFinal $ toLazy left
processLastFew :: [a] -> m (Maybe b)
processLastFew [] = return Nothing
processLastFew [a] = Just <$> processFinal a
processLastFew (a:as) = processOne a >> processLastFew as