attoparsec-framer 0.1.0.0 → 0.1.0.1
raw patch · 4 files changed
+98/−31 lines, 4 filesdep ~bytestringPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependency ranges changed: bytestring
API changes (from Hackage documentation)
+ Data.Attoparsec.Framer.Testing: linkedSrcAndSink :: [ByteString] -> IO (ByteSource IO, ByteString -> IO ())
+ Data.Attoparsec.Framer.Testing: linkedSrcAndSink' :: [ByteString] -> IO (ByteSource IO, ByteString -> IO ())
- Data.Attoparsec.Framer: mkFramer :: MonadThrow m => Parser a -> (a -> m ()) -> (Word32 -> m ByteString) -> Framer m a
+ Data.Attoparsec.Framer: mkFramer :: MonadThrow m => Parser frame -> (frame -> m ()) -> ByteSource m -> Framer m frame
- Data.Attoparsec.Framer: runFramer :: MonadThrow m => Framer m a -> m ()
+ Data.Attoparsec.Framer: runFramer :: MonadThrow m => Framer m frame -> m ()
- Data.Attoparsec.Framer: runOneFrame :: MonadThrow m => Maybe ByteString -> Framer m a -> m (Maybe ByteString, Bool)
+ Data.Attoparsec.Framer: runOneFrame :: MonadThrow m => Maybe ByteString -> Framer m frame -> m (Maybe ByteString, Bool)
Files
- ChangeLog.md +3/−0
- attoparsec-framer.cabal +6/−6
- src/Data/Attoparsec/Framer.hs +33/−25
- src/Data/Attoparsec/Framer/Testing.hs +56/−0
ChangeLog.md view
@@ -2,6 +2,9 @@ `attoparsec-framer` uses [PVP Versioning][1]. +## 0.1.0.1 -- 2023-07-19++* Adjust upper bound for ByteString to allow use of 0.12.0.0 ## 0.1.0.0 -- 2023-03-01
attoparsec-framer.cabal view
@@ -1,11 +1,11 @@ cabal-version: 3.0 name: attoparsec-framer-version: 0.1.0.0-synopsis: Use Attoparsec to parse framed protocol bytestreams+version: 0.1.0.1+synopsis: Use Attoparsec to parse framed protocol byte streams description: A library that simplifies the use of [Attoparsec](https://hackage.haskell.org/package/attoparsec) when processing- framed protocol bytestreams.+ framed protocol byte streams. As well as reading the haddocks, please take a look at a [demo server](https://github.com/adetokunbo/attoparsec-framer/blob/main/toy/Server.hs)@@ -36,7 +36,7 @@ build-depends: , attoparsec >=0.14.4 && <0.15 , base >=4.10 && <5- , bytestring >=0.10.8.2 && <0.12.0.0+ , bytestring >=0.10.8.2 && <0.12.1.0 , exceptions >= 0.10.4 && < 0.11 , text >=1.2.3 && <2.2 @@ -55,7 +55,7 @@ , attoparsec-framer , exceptions , base >=4.10 && <5- , bytestring >=0.10.8.2 && <0.12.0.0+ , bytestring >=0.10.8.2 && <0.12.1.0 , network >=3.1.0 && <4 , network-run >=0.2.2 && <0.3 , text >=1.2.3 && <2.2@@ -78,7 +78,7 @@ , attoparsec-binary >=0.2 && <0.3 , attoparsec-framer , base >=4.10 && <5- , bytestring >=0.10.8.2 && <0.12.0.0+ , bytestring >=0.10.8.2 && <0.12.1.0 , network >=3.1.0 && <4 , network-run >=0.2.2 && <0.3 , text >=1.2.3 && <2.2
src/Data/Attoparsec/Framer.hs view
@@ -11,22 +11,21 @@ Provides the 'Framer' data type that combines an @Attoparsec 'A.Parser'@ with a a few additional combinators that allow the parser to be used to process frames-from the framed byte streams commonly used in network protocol implementations.+of the framed byte streams commonly used in network protocol implementations. A @'Framer'@ specifies how the processing function @'runFramer'@ should parse a byte stream. Minimally, a @Framer@ specifies -* An @'A.Parser'@, used to extract frames from the byte stream-* a @'FrameHandler'@ responsible using the parsed frames-* the bytestream source, represented a 'ByteSource'-+* a @'A.Parser'@, used to extract frames from the byte stream+* a @'FrameHandler'@ responsible for using the parsed frames+* the byte stream source, represented by a 'ByteSource' -@'runFramer'@ the 'FrameHandler' is invoked repeatedly; on each-invocation it returns a 'Progression', which indicates if processing should-continue. This makes it possible to terminate for the 'FrameHandler' to signal-that frame processing should terminate.+@'runFramer'@ reads chunks from the @ByteSource@, parses these into frames and+invokes the 'FrameHandler'. Each invocation returns a 'Progression', which+indicates if processing should continue. This allows the 'FrameHandler' to+trigger termination of 'runFramer'. -} module Data.Attoparsec.Framer ( -- * Framer@@ -71,7 +70,7 @@ type FrameHandler m frame = frame -> m Progression --- | A byte stream from which chunks are to be repeatedly retrieved.+-- | A byte stream from which chunks are to be retrieved. type ByteSource m = Word32 -> m ByteString @@ -83,7 +82,7 @@ deriving (Eq, Show) --- | Use 'A.Parser' to parse a stream of @frames@ from a bytestream+-- | Uses a 'A.Parser' to parse a stream of @frames@ from a byte stream data Framer m frame = Framer { framerChunkSize :: !Word32 , frameByteSource :: !(ByteSource m)@@ -94,8 +93,8 @@ } -{- | Construct @'Framer'@ that will handle @frames@ repeatedly until a returned- @'Progression'@ stops it.+{- | Construct a @'Framer'@ that will handle @frames@ repeatedly until the+@FrameHandler@ returns a @'Progression'@ that stops it. -} mkFramer' :: MonadThrow m =>@@ -114,13 +113,16 @@ } --- | Construct @'Framer'@ that loops continuously.+-- | Construct a @'Framer'@ that loops continuously. mkFramer :: MonadThrow m =>- A.Parser a ->- (a -> m ()) ->- (Word32 -> m ByteString) ->- Framer m a+ -- | parses frames from the byte stream+ A.Parser frame ->+ -- | handles parsed frames+ (frame -> m ()) ->+ -- | obtains the next chunk from the byte stream+ ByteSource m ->+ Framer m frame mkFramer parser onFrame fetchBytes = let onFrameContinue x = do onFrame x@@ -131,7 +133,7 @@ -- | Repeatedly parse and handle frames until the configured @FrameHandler@ ends handling. runFramer :: MonadThrow m =>- Framer m a ->+ Framer m frame -> m () runFramer f = let Framer@@ -147,13 +149,19 @@ {- | Parse and handle a single frame. -The result is tuple of the outstanding unparsed bytes from the bytestream if-any, and a value indicating if the bytestream has terminated.+The result is a tuple: (Maybe @unparsed@, @terminated@)++where++@unparsed@ are outstanding bytes fetched from the @ByteSource@ and+@terminated@ is @True@ if the @ByteSource@ has no further input. -} runOneFrame :: MonadThrow m =>+ -- | the unparsed bytes from an earlier invocation, if any Maybe ByteString ->- Framer m a ->+ -- | the 'Framer' used to parse the @frame@+ Framer m frame -> m ((Maybe ByteString), Bool) runOneFrame restMb f = let Framer@@ -255,9 +263,9 @@ On failures, @'runFramer'@ throws @'Exception's@ using @'MonadThrow'@ rather than using an @Either@ or @MonadError@ -This is because it is intended to be used to parse framed protocol byte streams;-where parsing or connection errors here are typically not recoverable. In haskell-non-recoverable failures are better modelled using @Exceptions@.+This is because its intended use is for parsing framed protocol byte streams;+where parsing or connection errors are typically not recoverable. In+haskell non-recoverable failures are better modelled using @Exceptions@. Although it throws 'NoMoreInput' or 'BrokenFrame' when appropriate, it provides hooks to override these when constructing a 'Framer'.
src/Data/Attoparsec/Framer/Testing.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# OPTIONS_HADDOCK prune not-home #-} @@ -15,13 +16,18 @@ -- * testing combinators parsesFromFramerOk, chunksOfN,+ linkedSrcAndSink,+ linkedSrcAndSink', ) where import Control.Exception (catch)+import Control.Monad (when) import qualified Data.Attoparsec.ByteString as A import Data.Attoparsec.Framer import Data.ByteString (ByteString) import qualified Data.ByteString as BS+import Data.ByteString.Builder (byteStringHex, toLazyByteString)+import qualified Data.ByteString.Lazy.Char8 as C8 import Data.IORef ( IORef, modifyIORef',@@ -70,3 +76,53 @@ Just (x : xs) -> do writeIORef chunkStore $ Just xs pure x+++{- | A @'ByteSource'@ linked to a byte sink.++Provides a @ByteSource@ and @byte sink@ that emulate a responding endpoint.++The @responses@ are consumed each time the byte sink is invoked.++Whenever the sink is invoked, the head of the provided responses is removed+and starts to be returned in chunks by the @ByteSource@,+-}+linkedSrcAndSink :: [ByteString] -> IO (ByteSource IO, (ByteString -> IO ()))+linkedSrcAndSink responses = do+ refSrc <- newIORef Nothing+ refSink <- newIORef responses+ pure (ioRefByteSource refSrc, ioRefByteSink False refSink refSrc)+++-- | Like 'linkedSrcAndSink', but prints the src and sink to output as debug+linkedSrcAndSink' :: [ByteString] -> IO (ByteSource IO, (ByteString -> IO ()))+linkedSrcAndSink' responses = do+ refSrc <- newIORef Nothing+ refSink <- newIORef responses+ pure (ioRefByteSource refSrc, ioRefByteSink True refSink refSrc)+++ioRefByteSource :: IORef (Maybe ByteString) -> ByteSource IO+ioRefByteSource refSrc size = do+ readIORef refSrc >>= \case+ Nothing -> pure BS.empty+ Just src -> do+ let taken = BS.take (fromIntegral size) src+ rest = BS.drop (fromIntegral size) src+ stored = if BS.null taken then Nothing else Just rest+ writeIORef refSrc stored+ pure taken+++ioRefByteSink :: Bool -> IORef [ByteString] -> IORef (Maybe ByteString) -> ByteString -> IO ()+ioRefByteSink debug refResponses refSrc _ignored = do+ let asHex = toLazyByteString . byteStringHex+ when debug $ C8.putStrLn $ "bytesink got: " <> (asHex _ignored)+ readIORef refResponses >>= \case+ [] -> do+ when debug $ C8.putStrLn "bytesource has nothing"+ writeIORef refSrc Nothing+ (x : xs) -> do+ when debug $ C8.putStrLn $ "bytesink will reply with: " <> (asHex x)+ writeIORef refSrc $ Just x+ writeIORef refResponses $ xs