packages feed

binary-io (empty) → 0.0.1

raw patch · 7 files changed

+539/−0 lines, 7 filesdep +basedep +binarydep +binary-iosetup-changed

Dependencies added: base, binary, binary-io, bytestring, hspec, process

Files

+ ChangeLog.md view
@@ -0,0 +1,5 @@+# binary-io++## 1.0.0++* Inception
+ LICENSE view
@@ -0,0 +1,27 @@+Copyright (c) Ole Krüger++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions+are met:++* Redistributions of source code must retain the above copyright+  notice, this list of conditions and the following disclaimer.+* Redistributions in binary form must reproduce the above copyright+  notice, this list of conditions and the following disclaimer in the+  documentation and/or other materials provided with the distribution.+* Neither the name of the author nor the+  names of its contributors may be used to endorse or promote products+  derived from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ binary-io.cabal view
@@ -0,0 +1,89 @@+cabal-version:+  >= 1.10++name:+  binary-io++version:+  0.0.1++category:+  Data, Parsing, IO++synopsis:+  Read and write values of types that implement Binary from and to Handles++description:+  Read and write values of types that implement Binary from and to Handles+  .+  See module "Data.Binary.IO".++author:+  Ole Krüger <haskell-binary-io@vprsm.de>++maintainer:+  Ole Krüger <haskell-binary-io@vprsm.de>++homepage:+  https://github.com/vapourismo/binary-io++license:+  BSD3++license-file:+  LICENSE++extra-source-files:+  ChangeLog.md++build-type:+  Simple++source-repository head+  type: git+  location: git://github.com/vapourismo/binary-io.git++library+  default-language:+    Haskell2010++  ghc-options:+    -Wall -Wextra -Wno-name-shadowing++  build-depends:+    base == 4.*,+    bytestring,+    binary >= 0.7.2++  hs-source-dirs:+    lib++  exposed-modules:+    Data.Binary.IO++test-suite binary-io-tests+  type:+    exitcode-stdio-1.0++  default-language:+    Haskell2010++  ghc-options:+    -Wall -Wextra -Wno-name-shadowing++  build-depends:+    base,+    binary,+    binary-io,+    bytestring,+    process,+    hspec++  hs-source-dirs:+    test++  other-modules:+    Data.Binary.IOSpec++  main-is:+    Main.hs
+ lib/Data/Binary/IO.hs view
@@ -0,0 +1,249 @@+{-# LANGUAGE DeriveAnyClass #-}++-- | Read and write values of types that implement 'Binary.Binary' from and to 'Handle's+module Data.Binary.IO+  ( -- * Readers+    ReaderError (..)++  , Reader+  , newReader++    -- * Writers+  , Writer+  , newWriter++    -- * Duplex+  , Duplex+  , newDuplex++    -- * Classes+  , CanGet (..)+  , read+  , readWith++  , CanPut (..)+  , write+  )+where++import Prelude hiding (read)++import qualified Control.Exception as Exception+import qualified Control.Concurrent.MVar as MVar++import qualified Data.ByteString.Lazy as ByteString+import qualified Data.ByteString as ByteString.Strict+import qualified Data.Binary.Get as Binary.Get+import qualified Data.Binary.Put as Binary.Put+import qualified Data.Binary as Binary++import System.IO (Handle, hSetBinaryMode)++-- * Reader++-- | An error that can occur during reading+--+-- @since 0.0.1+data ReaderError = ReaderGetError -- ^ Error from the 'Binary.Get' operation+  { readerErrorRemaining :: !ByteString.ByteString+  -- ^ Unconsumed part of the byte stream+  --+  -- @since 0.0.1++  , readerErrorOffset :: !Binary.Get.ByteOffset+  -- ^ Error location represented as an offset into the input+  --+  -- @since 0.0.1++  , readerErrorInput :: !ByteString.ByteString+  -- ^ Input to the 'Binary.Get' operation+  --+  -- @since 0.0.1++  , readerErrorMessage :: !String+  -- ^ Error message+  --+  -- @since 0.0.1+  }+  deriving (Show, Exception.Exception)++newtype StationaryReader = StationaryReader ByteString.ByteString++runStationaryReader :: StationaryReader -> Binary.Get.Get a -> IO (StationaryReader, a)+runStationaryReader (StationaryReader stream) getter =  do+  -- Evaluate the result of 'runGetOrFail' to WHNF. This should be enough because it means that+  -- the parser has decided between 'Left' and 'Right'.+  result <- Exception.evaluate (Binary.Get.runGetOrFail getter stream)+  case result of+    Left (remainingBody, offset, errorMessage) ->+      Exception.throw ReaderGetError+        { readerErrorRemaining = remainingBody+        , readerErrorOffset = offset+        , readerErrorInput = stream+        , readerErrorMessage = errorMessage+        }++    Right (tailStream, _, value) ->+      pure (StationaryReader tailStream, value)++newStationaryReader :: Handle -> IO StationaryReader+newStationaryReader handle = do+  hSetBinaryMode handle True+  StationaryReader <$> ByteString.hGetContents handle++-- | @since 0.0.1+newtype Reader = Reader (MVar.MVar StationaryReader)++runReader :: Reader -> Binary.Get a -> (a -> IO b) -> IO b+runReader (Reader readerVar) getter continue =+  MVar.modifyMVar readerVar $ \posReader -> do+    toReturn <- runStationaryReader posReader getter+    traverse continue toReturn++-- | Create a new reader.+--+-- Reading using the 'Reader' may throw 'ReaderError'.+--+-- The internal position of the 'Reader' is not advanced when it throws an exception during reading.+-- This has the consequence that if you're trying to read with the same faulty 'Binary.Get'+-- operation multiple times, you will always receive an exception. The same is true for follow-up+-- actions when using 'readWith'.+--+-- Other threads reading from the 'Handle' will interfere with read operations of the 'Reader'.+-- However, the 'Reader' itself is thread-safe and can be utilized concurrently.+--+-- Once the 'Handle' reaches EOF, it will be closed.+--+-- The given 'Handle' will be swiched to binary mode via 'hSetBinaryMode'.+--+-- @since 0.0.1+newReader+  :: Handle -- ^ Handle that will be read from+  -> IO Reader+newReader handle = do+  posReader <- newStationaryReader handle+  Reader <$> MVar.newMVar posReader++-- * Writer++-- | @since 0.0.1+newtype Writer = Writer Handle++runWriter :: Writer -> Binary.Put -> IO ()+runWriter (Writer handle) putter =+  writeBytesAtomically handle (Binary.Put.runPut putter)++-- | Create a writer.+--+-- Other threads writing to the same 'Handle' do not interfere with the resulting 'Writer'. The+-- 'Writer' may be used concurrently.+--+-- @since 0.0.1+newWriter+  :: Handle -- ^ Handle that will be written to+  -> Writer+newWriter = Writer++-- * Duplex++-- | Pair of 'Reader' and 'Writer'+--+-- @since 0.0.1+data Duplex = Duplex+  { duplexWriter :: !Writer+  , duplexReader :: !Reader+  }++-- | Create a new duplex. The 'Duplex' inherits all the properties of 'Reader' and 'Writer' when+-- created with 'newReader' and 'newWriter'.+--+-- @since 0.0.1+newDuplex+  :: Handle -- ^ Handle that will be read from and written to+  -> IO Duplex+newDuplex handle =+  Duplex (newWriter handle) <$> newReader handle++-- * Classes++-- | @r@ can execute 'Binary.Get' operations+--+-- @since 0.0.1+class CanGet r where+  runGet+    :: r -- ^ Reader / source+    -> Binary.Get a -- ^ Operation to execute+    -> (a -> IO b) -- ^ What to do with @a@+    -> IO b++instance CanGet Reader where+  runGet = runReader++instance CanGet Duplex where+  runGet = runGet . duplexReader++-- | @w@ can execute 'Binary.Put' operations+--+-- @since 0.0.1+class CanPut w where+  runPut+    :: w -- ^ Writer / target+    -> Binary.Put -- ^ Operation to execute+    -> IO ()++instance CanPut Handle where+  runPut handle putter = writeBytesAtomically handle (Binary.Put.runPut putter)++instance CanPut Writer where+  runPut = runWriter++instance CanPut Duplex where+  runPut = runPut . duplexWriter++-- | Read something from @r@.+--+-- @since 0.0.1+read+  :: (CanGet r, Binary.Binary a)+  => r -- ^ Read source+  -> IO a+read reader =+  runGet reader Binary.get pure++-- | Read something from @r@ and perform an 'IO' action with it.+--+-- If the given action throws an exception, the read is not considered successful and will not+-- advance the underlying read source.+--+-- Keep in mind, long running actions on @a@ will block other threads when they try to read the from+-- the same source @r@.+--+-- @since 0.0.1+readWith+  :: (CanGet r, Binary.Binary a)+  => r -- ^ Read source+  -> (a -> IO b) -- ^ What to do with @a@+  -> IO b+readWith reader =+  runGet reader Binary.get++-- | Write something to @w@.+--+-- @since 0.0.1+write+  :: (CanPut w, Binary.Binary a)+  => w -- ^ Write target+  -> a -- ^ Value to be written+  -> IO ()+write writer value =+  runPut writer (Binary.put value)++-- * Utilities++-- | Write contents of the given lazy byte string all at once.+writeBytesAtomically+  :: Handle -- ^ Handle to write to+  -> ByteString.ByteString -- ^ Bytes to be written+  -> IO ()+writeBytesAtomically handle payload =+  ByteString.Strict.hPut handle (ByteString.toStrict payload)
+ test/Data/Binary/IOSpec.hs view
@@ -0,0 +1,159 @@+{-# LANGUAGE DeriveAnyClass #-}++module Data.Binary.IOSpec (spec) where++import Prelude hiding (read)++import Control.Monad.IO.Class (MonadIO (liftIO))+import Control.Monad (join)+import Control.Exception (Exception, throw)++import Data.Typeable (typeOf)+import Data.List (isInfixOf)+import Data.Binary.IO+import Data.Binary (Binary (..))+import Data.Bifoldable (bitraverse_)++import qualified Test.Hspec as Hspec++import           System.Process (createPipe)+import qualified System.IO as IO+import           System.IO.Error (isIllegalOperation, ioeGetErrorString)++-- | Create a pipe with no buffering on read and write side.+createUnbufferedPipe :: IO (IO.Handle, IO.Handle)+createUnbufferedPipe = do+  handles <- createPipe+  join bitraverse_ (`IO.hSetBuffering` IO.NoBuffering) handles+  pure handles++-- | The 'Binary' instance of this type implements a 'get' that always fails+data BadGet++instance Binary BadGet where+  put = error "Not implemented"++  get = fail "get for BadGet will always"++data ExampleException = ExampleException+  deriving (Show, Exception)++-- | Check that a read from the 'IO.Handle' yields the given value.+shouldRead :: (Show a, Eq a, Binary a) => Reader -> a -> Hspec.Expectation+shouldRead reader expectedValue = do+  value <- read reader+  Hspec.shouldBe value expectedValue++-- | Close a handle and verify.+closeHandle :: IO.Handle -> Hspec.Expectation+closeHandle handle = do+  IO.hClose handle+  closed <- IO.hIsClosed handle+  Hspec.shouldBe closed True++spec :: Hspec.Spec+spec = Hspec.before createUnbufferedPipe $ do+  Hspec.describe "Reader" $ do+    let+      testReads value =+        Hspec.it ("reads " <> show (typeOf value)) $ \(handleRead, handleWrite) -> do+          reader <- liftIO (newReader handleRead)++          write handleWrite value+          shouldRead reader value++          write handleWrite value+          write handleWrite value++          shouldRead reader value+          shouldRead reader value++    -- Test something with 0 length+    testReads ()++    -- Test something with fixed non-zero length+    testReads (1337 :: Int)++    -- Test something with variable length+    testReads "Hello World"++    -- When the read handle has reached its end, reading from it should not throw an error.+    -- However, no more input can be read therefore the underling 'Get' parser should fail.+    Hspec.it "throws ReaderGetError when Handle is EOF" $ \(handleRead, handleWrite) -> do+      reader <- liftIO (newReader handleRead)++      IO.hClose handleWrite+      eof <- IO.hIsEOF handleRead+      Hspec.shouldBe eof True++      Hspec.shouldThrow (read reader :: IO String) (\ReaderGetError{} -> True)++    -- Reading from a closed handle should throw. That exception needs to surface.+    Hspec.it "throws IllegalOperation when read Handle is closed" $ \(handleRead, _handleWrite) -> do+      reader <- liftIO (newReader handleRead)++      closeHandle handleRead++      Hspec.shouldThrow (read reader :: IO String) isIllegalOperation++    -- Failing 'Get' operations should not advance the stream position.+    Hspec.it "preserves the stream position when Get operation fails" $ \(handleRead, handleWrite) -> do+      reader <- liftIO (newReader handleRead)++      write handleWrite "Hello World"+      Hspec.shouldThrow (read reader :: IO BadGet) (\ReaderGetError{} -> True)+      "Hello World" <- read reader++      pure ()++    -- Failing continuations should not advance the stream position.+    Hspec.it "preserves the stream position when continuation fails" $ \(handleRead, handleWrite) -> do+      reader <- liftIO (newReader handleRead)++      write handleWrite "Hello World"+      Hspec.shouldThrow+        (readWith reader (\() -> throw ExampleException))+        (\ExampleException -> True)+      "Hello World" <- read reader++      pure ()++  Hspec.describe "Writer" $ do+    let+      testWrites value =+        Hspec.it ("writes " <> show (typeOf value)) $ \(handleRead, handleWrite) -> do+          let writer = newWriter handleWrite+          reader <- newReader handleRead++          write writer value+          shouldRead reader value++          write writer value+          write writer value++          shouldRead reader value+          shouldRead reader value++    -- Test something with 0 length+    testWrites ()++    -- Test something with fixed non-zero length+    testWrites (1337 :: Int)++    -- Test something with variable length+    testWrites "Hello World"++    Hspec.it "throws ResourceVanished when read Handle is closed" $ \(handleRead, handleWrite) -> do+      let writer = newWriter handleWrite++      closeHandle handleRead++      Hspec.shouldThrow (write writer "Hello World") $ \exception ->+        isInfixOf "resource vanished" (ioeGetErrorString exception)++    Hspec.it "throws IllegalOperation when write Handle is closed" $ \(_handleRead, handleWrite) -> do+      let writer = newWriter handleWrite++      closeHandle handleWrite++      Hspec.shouldThrow (write writer "Hello World") isIllegalOperation
+ test/Main.hs view
@@ -0,0 +1,8 @@+module Main (main) where++import Test.Hspec++import qualified Data.Binary.IOSpec++main :: IO ()+main = hspec Data.Binary.IOSpec.spec