packages feed

ron-0.13: lib/RON/Binary/Serialize.hs

{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

-- | Binary serializer elements
module RON.Binary.Serialize (
    serialize,
    serializeAtom,
    serializeString,
) where

import RON.Prelude

import Data.Binary qualified as Binary
import Data.Binary.Put (putDoublebe, runPut)
import Data.Bits (bit, shiftL, (.|.))
import Data.ByteString.Lazy (cons, fromStrict)
import Data.ByteString.Lazy qualified as BSL
import Data.Text.Encoding (encodeUtf8)
import Data.ZigZag (zzEncode)

import RON.Binary.Types (Desc (..), Size, descIsOp)
import RON.Types (
    Atom (AFloat, AInteger, AString, AUuid),
    ClosedOp (..),
    Op (..),
    UUID (UUID),
    WireChunk (Closed, Query, Value),
    WireFrame,
    WireReducedChunk (..),
 )
import RON.Util.Word (Word4, b0000, leastSignificant4, safeCast)

-- | Serialize a frame
serialize :: WireFrame -> Either String ByteStringL
serialize chunks = ("RON2" <>) <$> serializeBody
  where
    serializeBody = foldChunks =<< traverse serializeChunk chunks

    chunkSize :: Bool -> Int64 -> Either String ByteStringL
    chunkSize continue x
        | x < bit 31 = Right $ Binary.encode s'
        | otherwise = Left $ "chunk size is too big: " ++ show x
      where
        s = fromIntegral x :: Size
        s'
            | continue = s .|. bit 31
            | otherwise = s

    foldChunks :: [ByteStringL] -> Either String ByteStringL
    foldChunks = \case
        [] -> chunkSize False 0
        [c] -> (<> c) <$> chunkSize False (BSL.length c)
        c : cs ->
            fold
                <$> sequence
                    [chunkSize True (BSL.length c), pure c, foldChunks cs]

-- | Serialize a chunk
serializeChunk :: WireChunk -> Either String ByteStringL
serializeChunk = \case
    Closed op -> serializeClosedOp DOpClosed op
    Value rchunk -> serializeReducedChunk False rchunk
    Query rchunk -> serializeReducedChunk True rchunk

-- | Serialize a closed op
serializeClosedOp :: Desc -> ClosedOp -> Either String ByteStringL
serializeClosedOp desc ClosedOp{..} = do
    keys <-
        sequenceA
            [ serializeUuidReducer reducerId
            , serializeUuidObject objectId
            , serializeUuidOpId opId
            , serializeUuidRef refId
            ]
    payloadValue <- traverse serializeAtom payload
    serializeWithDesc desc $ fold $ keys ++ payloadValue
  where
    Op{opId, refId, payload} = op
    serializeUuidReducer = serializeWithDesc DUuidReducer . serializeUuid
    serializeUuidObject = serializeWithDesc DUuidObject . serializeUuid
    serializeUuidOpId = serializeWithDesc DUuidOp . serializeUuid
    serializeUuidRef = serializeWithDesc DUuidRef . serializeUuid

-- | Serialize a reduced op
serializeReducedOp :: Desc -> UUID -> UUID -> Op -> Either String ByteStringL
serializeReducedOp d reducerId objectId op =
    serializeClosedOp d ClosedOp{reducerId, objectId, op}

-- | Serialize a 'UUID'
serializeUuid :: UUID -> ByteStringL
serializeUuid (UUID x y) = Binary.encode x <> Binary.encode y

-- | Encode descriptor
encodeDesc :: Desc -> Word4
encodeDesc = leastSignificant4 . fromEnum

-- | Prepend serialized bytes with descriptor
serializeWithDesc ::
    Desc ->
    -- | body
    ByteStringL ->
    Either String ByteStringL
serializeWithDesc d body = do
    (lengthDesc, lengthExtended) <- lengthFields
    let descByte = safeCast (encodeDesc d) `shiftL` 4 .|. safeCast lengthDesc
    pure $ descByte `cons` lengthExtended <> body
  where
    len = BSL.length body
    lengthFields = case d of
        DAtomString
            | len == 0 -> Right (b0000, mkLengthExtended)
            | len < 16 -> Right (leastSignificant4 len, BSL.empty)
            | len < bit 31 -> Right (b0000, mkLengthExtended)
            | otherwise -> Left "String is too long"
        _
            | descIsOp d -> Right (b0000, BSL.empty)
            | len < 16 -> Right (leastSignificant4 len, BSL.empty)
            | len == 16 -> Right (b0000, BSL.empty)
            | otherwise -> Left "impossible"
    mkLengthExtended
        | len < 128 = Binary.encode (fromIntegral len :: Word8)
        | otherwise = Binary.encode (fromIntegral len .|. bit 31 :: Word32)

-- | Serialize an 'Atom'
serializeAtom :: Atom -> Either String ByteStringL
serializeAtom = \case
    AFloat f -> serializeWithDesc DAtomFloat $ serializeFloat f
    AInteger i -> serializeWithDesc DAtomInteger $ Binary.encode $ zzEncode64 i
    AString s -> serializeWithDesc DAtomString $ serializeString s
    AUuid u -> serializeWithDesc DAtomUuid $ serializeUuid u
  where
    {-# INLINE zzEncode64 #-}
    zzEncode64 :: Int64 -> Word64
    zzEncode64 = zzEncode

-- | Serialize a float atom
serializeFloat :: Double -> ByteStringL
serializeFloat = runPut . putDoublebe

-- | Serialize a reduced chunk
serializeReducedChunk :: Bool -> WireReducedChunk -> Either String ByteStringL
serializeReducedChunk isQuery WireReducedChunk{..} = do
    header <-
        serializeClosedOp
            (if isQuery then DOpQueryHeader else DOpHeader)
            wrcHeader
    body <- foldMapA (serializeReducedOp DOpReduced reducerId objectId) wrcBody
    pure $ header <> body
  where
    ClosedOp{..} = wrcHeader

-- | Serialize a string atom
serializeString :: Text -> ByteStringL
serializeString = fromStrict . encodeUtf8

foldMapA :: (Applicative f, Foldable t, Monoid b) => (a -> f b) -> t a -> f b
foldMapA f = fmap fold . traverse f . toList