packages feed

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

{-# LANGUAGE BinaryLiterals #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

-- | RON-Text serialization
module RON.Text.Serialize (
    serializeAtom,
    serializeObject,
    serializeOp,
    serializeOpenOp,
    serializePayload,
    serializeRawOp,
    serializeStateFrame,
    serializeString,
    serializeUuid,
    serializeWireFrame,
    serializeWireFrames,
    uuidToString,
    uuidToText,
) where

import RON.Prelude hiding (elem)

import Control.Monad.State.Strict (state)
import Data.Aeson qualified as Json
import Data.ByteString.Lazy.Char8 (cons, elem, snoc)
import Data.ByteString.Lazy.Char8 qualified as BSL
import Data.Map.Strict qualified as Map
import Data.Text qualified as Text

import RON.Text.Serialize.UUID (
    serializeUuid,
    serializeUuidAtom,
    serializeUuidKey,
    uuidToString,
    uuidToText,
 )
import RON.Types (
    Atom (AFloat, AInteger, AString, AUuid),
    ClosedOp (ClosedOp),
    ObjectFrame (ObjectFrame),
    Op (Op),
    Packer (PackChain, PackFixed, PackIncrement),
    Payload,
    StateFrame,
    WireChunk (Closed, Query, Value),
    WireFrame,
    WireReducedChunk (WireReducedChunk),
    WireStateChunk (WireStateChunk),
 )
import RON.Types qualified
import RON.UUID (UUID, addValue, succValue, zero)

-- | Serialize a common frame
serializeWireFrame :: WireFrame -> ByteStringL
serializeWireFrame =
    (`snoc` '.') . fold . (`evalState` closedOpZero) . traverse serializeChunk

-- | Serialize a sequence of common frames
serializeWireFrames :: [WireFrame] -> ByteStringL
serializeWireFrames = foldMap serializeWireFrame

-- | Serialize a common chunk
serializeChunk :: WireChunk -> State ClosedOp ByteStringL
serializeChunk = \case
    Closed op -> (<> " ;\n") <$> serializeClosedOpZip op
    Value chunk -> serializeReducedChunk False chunk
    Query chunk -> serializeReducedChunk True chunk

-- | Serialize a reduced chunk
serializeReducedChunk :: Bool -> WireReducedChunk -> State ClosedOp ByteStringL
serializeReducedChunk isQuery WireReducedChunk{wrcHeader, wrcBody} =
    BSL.unlines <$> liftA2 (:) serializeHeader serializeBody
  where
    serializeHeader = do
        h <- serializeClosedOpZip wrcHeader
        pure $ BSL.intercalate "\t" [h, if isQuery then "?" else "!"]
    serializeBody = state \ClosedOp{op = opBefore, ..} ->
        let (body, opAfter) =
                (`runState` opBefore) $
                    for (packOps wrcBody) $
                        fmap ("\t" <>) . serializeReducedOpPack objectId
        in  (body, ClosedOp{op = opAfter, ..})

data Pack p = Pack
    { firstOpId, firstRefId, lastOpId, lastRefId :: UUID
    , packedPayload :: p
    }

packOps :: [Op] -> [(Maybe Packer, Op)]
packOps = goWithoutPack
  where
    goWithoutPack :: [Op] -> [(Maybe Packer, Op)]
    goWithoutPack = \case
        [] -> []
        a@Op{payload = [AString ac]} : b@Op{payload = [AString bc]} : cont
            | Text.length ac == 1
            , Text.length bc == 1
            , b.opId == succValue a.opId
            , Just packer <- guessPacker a.opId a.refId b.refId ->
                goWithText
                    packer
                    Pack
                        { firstOpId = a.opId
                        , firstRefId = a.refId
                        , lastOpId = b.opId
                        , lastRefId = b.refId
                        , packedPayload = ac <> bc
                        }
                    cont
        a@Op{payload = []} : b@Op{payload = []} : cont
            | b.opId == succValue a.opId
            , b.refId == succValue a.refId ->
                goWithIncrement_
                    Pack
                        { firstOpId = a.opId
                        , firstRefId = a.refId
                        , lastOpId = b.opId
                        , lastRefId = b.refId
                        , packedPayload = 2
                        }
                    cont
        op : cont -> (Nothing, op) : goWithoutPack cont

    nextRef opId refId = \case
        PackChain -> opId
        PackFixed -> refId
        PackIncrement -> succValue refId

    guessPacker lastOpId lastRefId refId
        | refId == lastOpId = Just PackChain
        | refId == lastRefId = Just PackFixed
        | refId == succValue lastRefId = Just PackIncrement
        | otherwise = Nothing

    goWithText :: Packer -> Pack Text -> [Op] -> [(Maybe Packer, Op)]
    goWithText packer pack@Pack{lastOpId, lastRefId, packedPayload} = \case
        Op{opId, refId, payload = [AString c]} : cont
            | Text.length c == 1
            , opId == succValue lastOpId
            , refId == nextRef lastOpId lastRefId packer ->
                goWithText
                    packer
                    pack
                        { packedPayload = packedPayload <> c
                        , lastOpId = opId
                        , lastRefId = refId
                        }
                    cont
        cont ->
            wrap (fromIntegral . Text.length) AString packer pack
                : goWithoutPack cont

    goWithIncrement_ :: Pack Int64 -> [Op] -> [(Maybe Packer, Op)]
    goWithIncrement_ pack@Pack{lastOpId, lastRefId, packedPayload} = \case
        Op{opId, refId, payload = []} : cont
            | opId == succValue lastOpId
            , refId == succValue lastRefId ->
                goWithIncrement_
                    pack
                        { packedPayload = succ packedPayload
                        , lastOpId = opId
                        , lastRefId = refId
                        }
                    cont
        cont -> wrap id AInteger PackIncrement pack : goWithoutPack cont

    wrap ::
        (a -> Int64) -> (a -> Atom) -> Packer -> Pack a -> (Maybe Packer, Op)
    wrap getSize mkAtom packer Pack{firstOpId, firstRefId, packedPayload} =
        ( guard (getSize packedPayload > 1) $> packer
        , Op
            { opId = firstOpId
            , refId = firstRefId
            , payload = [mkAtom packedPayload]
            }
        )

-- | Serialize a context-free raw op
serializeRawOp :: ClosedOp -> ByteStringL
serializeRawOp op = evalState (serializeClosedOpZip op) closedOpZero

-- | Serialize a raw op with compression in stream context
serializeClosedOpZip :: ClosedOp -> State ClosedOp ByteStringL
serializeClosedOpZip this = state \prev ->
    let typ = serializeUuidKey prev.reducerId zero this.reducerId
        obj = serializeUuidKey prev.objectId this.reducerId this.objectId
        evt = serializeUuidKey prev.op.opId this.objectId this.op.opId
        ref = serializeUuidKey prev.op.refId this.op.opId this.op.refId
        payloadAtoms = serializePayloadZip this.objectId this.op.payload
    in  ( BSL.intercalate "\t" $
            key '*' typ
                ++ key '#' obj
                ++ key '@' evt
                ++ key ':' ref
                ++ [payloadAtoms | not $ BSL.null payloadAtoms]
        , this
        )
  where
    key c u = [c `cons` u | not $ BSL.null u]

-- | Serialize a reduced op with compression in stream context
serializeReducedOpPack ::
    -- | enclosing object
    UUID ->
    (Maybe Packer, Op) ->
    State Op ByteStringL
serializeReducedOpPack object (packerM, this) =
    state \prev ->
        let evt = serializeUuidKey prev.opId object this.opId
            ref = serializeUuidKey prev.refId this.opId this.refId
            payload =
                serializePacker packerM
                    <> serializePayloadZip object this.payload
            keys
                | BSL.null evt && BSL.null ref = ["@"]
                | otherwise = key '@' evt ++ key ':' ref
            op = keys ++ [payload | not $ BSL.null payload]
        in  (BSL.intercalate "\t" op, lastOp)
  where
    key c u = [c `cons` u | not $ BSL.null u]
    packSize =
        case this.payload of
            [AString s] -> fromIntegral $ Text.length s
            [AInteger i] -> fromIntegral i
            _ -> undefined
    lastOp =
        case packerM of
            Nothing -> this
            Just packer ->
                Op
                    { opId = this.opId `addValue` (packSize - 1)
                    , refId =
                        case packer of
                            PackChain -> this.opId `addValue` (packSize - 2)
                            PackFixed -> this.refId
                            PackIncrement ->
                                this.refId `addValue` (packSize - 1)
                    , payload = []
                    }

serializePacker :: Maybe Packer -> ByteStringL
serializePacker = \case
    Nothing -> ""
    Just PackChain -> "%c "
    Just PackFixed -> "%f "
    Just PackIncrement -> "%i "

serializeOp :: Op -> ByteStringL
serializeOp Op{opId, refId, payload} =
    BSL.intercalate
        "\t"
        [ '@' `cons` serializeUuid opId
        , ':' `cons` serializeUuid refId
        , serializePayloadZip opId payload
        ]

serializeOpenOp ::
    -- | Previous op id
    UUID ->
    -- | Current op
    Op ->
    ByteStringL
serializeOpenOp prevId Op{opId, refId, payload} =
    BSL.intercalate "\t" $ idS : refS : payloadS
  where
    idS
        | opId /= succValue prevId = '@' `cons` serializeUuid opId
        | otherwise = ""
    refS
        | refId /= prevId = ':' `cons` serializeUuid refId
        | otherwise = ""
    payloadS = [serializePayloadZip opId payload | not $ null payload]

-- | Serialize a context-free atom
serializeAtom :: Atom -> ByteStringL
serializeAtom a = evalState (serializeAtomZip a) zero

-- | Serialize an atom with compression for UUID in stream context
serializeAtomZip :: Atom -> State UUID ByteStringL
serializeAtomZip = \case
    AFloat f -> pure $ serializeFloatAtom f
    AInteger i -> pure $ serializeIntegerAtom i
    AString s -> pure $ serializeString s
    AUuid u -> serializeUuidAtom' u

{- | Serialize a float atom.
If unambiguous, i.e. contains a '.' or an 'e'/'E', the prefix '^' is skipped.
-}
serializeFloatAtom :: Double -> ByteStringL
serializeFloatAtom float
    | isDistinguishableFromUuid = bs
    | otherwise = '^' `cons` bs
  where
    isDistinguishableFromUuid = '.' `elem` bs || 'e' `elem` bs || 'E' `elem` bs
    bs = BSL.pack $ show float

{- | Serialize an integer atom.
Since integers are always unambiguous, the prefix '=' is always skipped.
-}
serializeIntegerAtom :: Int64 -> ByteStringL
serializeIntegerAtom = BSL.pack . show

-- | Serialize a string atom
serializeString :: Text -> ByteStringL
serializeString =
    wrapSingleQuotes . escapeApostrophe . stripDoubleQuotes . Json.encode
  where
    wrapSingleQuotes = (`snoc` '\'') . cons '\''
    stripDoubleQuotes = BSL.init . BSL.tail
    escapeApostrophe s
        | BSL.null s2 = s1
        | otherwise = s1 <> "\\'" <> escapeApostrophe (BSL.tail s2)
      where
        (s1, s2) = BSL.break (== '\'') s

serializeUuidAtom' :: UUID -> State UUID ByteStringL
serializeUuidAtom' u =
    -- TODO(2019-08-19, cblp): Check if uuid can be unambiguously serialized and
    -- if so, skip the prefix.
    state \prev -> (cons '>' $ serializeUuidAtom prev u, u)

-- | Serialize a payload in stream context
serializePayloadZip ::
    -- | previous UUID (default is 'zero')
    UUID ->
    Payload ->
    ByteStringL
serializePayloadZip prev =
    BSL.unwords . (`evalState` prev) . traverse serializeAtomZip

-- | Serialize an abstract payload
serializePayload :: Payload -> ByteStringL
serializePayload = serializePayloadZip zero

-- | Serialize a state frame
serializeStateFrame :: StateFrame -> ByteStringL
serializeStateFrame = serializeWireFrame . map wrapChunk . Map.assocs
  where
    wrapChunk (objectId, WireStateChunk{stateType, stateBody}) =
        Value
            WireReducedChunk
                { wrcHeader =
                    ClosedOp{reducerId = stateType, objectId, op = opZero}
                , wrcBody = stateBody
                }

-- | Serialize an object. Return object id that must be stored separately.
serializeObject :: ObjectFrame a -> (UUID, ByteStringL)
serializeObject (ObjectFrame oid frame) = (oid, serializeStateFrame frame)

closedOpZero :: ClosedOp
closedOpZero = ClosedOp{reducerId = zero, objectId = zero, op = opZero}

opZero :: Op
opZero = Op{opId = zero, refId = zero, payload = []}