packages feed

grpc-spec-1.0.0: src/Network/GRPC/Spec/Serialization/LengthPrefixed.hs

-- | Length-prefixed messages
--
-- These are used both for inputs and outputs.
module Network.GRPC.Spec.Serialization.LengthPrefixed (
    -- * Message prefix
    MessagePrefix(..)
    -- * Length-prefixex messages
    -- ** Construction
  , OutboundMeta(..)
  , buildInput
  , buildOutput
    -- ** Parsing
  , InboundMeta(..)
  , parseInput
  , parseOutput
  ) where

import Data.Binary.Get (Get)
import Data.Binary.Get qualified as Binary
import Data.ByteString.Builder (Builder)
import Data.ByteString.Builder qualified as Builder
import Data.ByteString.Lazy qualified as BS.Lazy
import Data.ByteString.Lazy qualified as Lazy (ByteString)
import Data.Proxy
import Data.Word

import Network.GRPC.Spec
import Network.GRPC.Spec.Util.Parser (Parser)
import Network.GRPC.Spec.Util.Parser qualified as Parser

{-------------------------------------------------------------------------------
  Message prefix
-------------------------------------------------------------------------------}

data MessagePrefix = MessagePrefix {
      msgIsCompressed :: Bool
    , msgLength       :: Word32
    }
  deriving (Show)

buildMessagePrefix :: MessagePrefix -> Builder
buildMessagePrefix MessagePrefix{msgLength, msgIsCompressed} = mconcat [
      Builder.word8    $ if msgIsCompressed then 1 else 0
    , Builder.word32BE $ msgLength
    ]

getMessagePrefix :: Get MessagePrefix
getMessagePrefix = do
    msgIsCompressed <- Binary.getWord8 >>= \case
                         0 -> return False
                         1 -> return True
                         n -> fail $ "parseMessagePrefix: unxpected " ++ show n
    msgLength       <- Binary.getWord32be
    return MessagePrefix{msgIsCompressed, msgLength}

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

-- | Serialize RPC input
--
-- > Length-Prefixed-Message → Compressed-Flag Message-Length Message
-- >
-- > Compressed-Flag → 0 / 1
-- >                     # encoded as 1 byte unsigned integer
-- > Message-Length  → {length of Message}
-- >                     # encoded as 4 byte unsigned integer (big endian)
-- > Message         → *{binary octet}
buildInput ::
     SupportsClientRpc rpc
  => Proxy rpc
  -> Compression
  -> (OutboundMeta, Input rpc)
  -> Builder
buildInput = buildMsg . rpcSerializeInput

-- | Serialize RPC output
buildOutput ::
     SupportsServerRpc rpc
  => Proxy rpc
  -> Compression
  -> (OutboundMeta, Output rpc)
  -> Builder
buildOutput = buildMsg . rpcSerializeOutput

-- | Generalization of 'buildInput' and 'buildOutput'
buildMsg ::
     (x -> Lazy.ByteString)
  -> Compression
  -> (OutboundMeta, x)
  -> Builder
buildMsg build compr (meta, x) = mconcat [
      buildMessagePrefix prefix
    , Builder.lazyByteString $
        if shouldCompress
          then compressed
          else uncompressed
    ]
  where
    uncompressed, compressed :: Lazy.ByteString
    uncompressed = build x
    compressed   = compress compr uncompressed

    shouldCompress :: Bool
    shouldCompress = and [
          uncompressedSizeThreshold compr uncompressedLength
        , outboundEnableCompression meta
        , compressedLength < uncompressedLength
        ]
      where
        uncompressedLength = BS.Lazy.length uncompressed
        compressedLength = BS.Lazy.length compressed

    prefix :: MessagePrefix
    prefix
      | shouldCompress
      = MessagePrefix {
            msgIsCompressed = True
          , msgLength       = fromIntegral $ BS.Lazy.length compressed
          }

      | otherwise
      = MessagePrefix {
            msgIsCompressed = False
          , msgLength       = fromIntegral $ BS.Lazy.length uncompressed
          }

{-------------------------------------------------------------------------------
  Parsing
-------------------------------------------------------------------------------}

-- | Parse input
parseInput ::
     SupportsServerRpc rpc
  => Proxy rpc
  -> Compression
  -> Parser String (InboundMeta, Input rpc)
parseInput = parseMsg . rpcDeserializeInput

-- | Parse output
parseOutput ::
     SupportsClientRpc rpc
  => Proxy rpc
  -> Compression
  -> Parser String (InboundMeta, Output rpc)
parseOutput = parseMsg . rpcDeserializeOutput

parseMsg :: forall x.
     (Lazy.ByteString -> Either String x)
  -> Compression
  -> Parser String (InboundMeta, x)
parseMsg parse compr = do
    prefix <- Parser.getExactly 5 getMessagePrefix
    Parser.consumeExactly (fromIntegral $ msgLength prefix) $
      parseBody (msgIsCompressed prefix)
  where
    parseBody :: Bool -> Lazy.ByteString -> Either String (InboundMeta, x)
    parseBody False body =
        (meta,) <$> parse body
      where
        meta :: InboundMeta
        meta = InboundMeta {
              inboundCompressedSize   = Nothing
            , inboundUncompressedSize = lengthOf body
            }
    parseBody True compressed =
        (meta,) <$> parse uncompressed
      where
        uncompressed :: Lazy.ByteString
        uncompressed = decompress compr compressed

        meta :: InboundMeta
        meta = InboundMeta {
              inboundCompressedSize   = Just (lengthOf compressed)
            , inboundUncompressedSize = lengthOf uncompressed
            }

    lengthOf :: Num a => Lazy.ByteString -> a
    lengthOf = fromIntegral . BS.Lazy.length