packages feed

grapesy-1.0.0: src/Network/GRPC/Util/AccumulatedByteString.hs

-- | Accumulate strict bytestrings
--
-- Intended for qualified import.
--
-- > import Network.GRPC.Util.AccumulatedByteString (AccumulatedByteString)
-- > import Network.GRPC.Util.AccumulatedByteString qualified as AccBS
module Network.GRPC.Util.AccumulatedByteString (
    AccumulatedByteString -- opaque
    -- * Construction
  , init
  , append
    -- * Query
  , length
    -- * Destruction
  , splitAt
  ) where

import Prelude hiding (init, splitAt, length)

import Data.ByteString qualified as BS.Strict
import Data.ByteString qualified as Strict (ByteString)

{-------------------------------------------------------------------------------
  Definition
-------------------------------------------------------------------------------}

-- | Accumulate strict bytestrings until we have enough bytes
data AccumulatedByteString = AccBS {
       -- | Total accumulated length
       accLength  :: !Word

       -- | Accumulated bytestrings, in reverse order
     , accStrings :: [Strict.ByteString]
     }

{-------------------------------------------------------------------------------
  Construction
-------------------------------------------------------------------------------}

init :: Maybe Strict.ByteString -> AccumulatedByteString
init Nothing   = AccBS 0 []
init (Just bs) = AccBS (fromIntegral $ BS.Strict.length bs) [bs]

append :: AccumulatedByteString -> Strict.ByteString -> AccumulatedByteString
append AccBS{accLength, accStrings} bs = AccBS{
      accLength  = accLength + fromIntegral (BS.Strict.length bs)
    , accStrings = bs : accStrings
    }

{-------------------------------------------------------------------------------
  Query
-------------------------------------------------------------------------------}

length :: AccumulatedByteString -> Word
length = accLength

{-------------------------------------------------------------------------------
  Destruction
-------------------------------------------------------------------------------}

-- | Split the accumulated bytestrings after @n@ bytes
--
-- Preconditions:
--
-- 1. The accumulated length must be @>= n@
-- 2. The accumulated length without the most recently added bytestring
--    must be @<= n@
--
-- These two preconditions together imply that all accumulated bytestrings
-- except the most recent will be required.
splitAt ::
     Word -- ^ @n@
  -> AccumulatedByteString
  -> (Strict.ByteString, Maybe Strict.ByteString)
splitAt n AccBS{accLength, accStrings} =
    if leftoverLen == 0 then
      (BS.Strict.concat $ reverse accStrings, Nothing)
    else
      -- If @leftoverLen > 0@ then @accLength > 0@: we must have some strings
      case accStrings of
        []              -> error "splitAt: invalid AccumulatedByteString"
        mostRecent:rest ->
          let neededLen =
                -- cannot underflow due to precondition (2)
                BS.Strict.length mostRecent - fromIntegral leftoverLen
              (needed, leftover) =
                BS.Strict.splitAt neededLen mostRecent
          in ( BS.Strict.concat $ reverse (needed : rest)
             , if BS.Strict.null leftover
                 then Nothing
                 else Just leftover
             )
  where
    -- cannot underflow due to precondition (1)
    leftoverLen :: Word
    leftoverLen = accLength - n