packages feed

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

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

-- | RON-Text parsing
module RON.Text.Parse (
    parseAtom,
    parseObject,
    parseOp,
    parseOpenFrame,
    parseOpenOp,
    parsePayload,
    parseStateChunk,
    parseStateFrame,
    parseString,
    parseUuid,
    parseUuidKey,
    parseUuidAtom,
    parseWireFrame,
    parseWireFrames,
    uuidFromString,
    uuidFromText,
) where

import RON.Prelude hiding (takeWhile)

import Attoparsec.Extra (
    Parser,
    char,
    definiteDouble,
    endOfInputEx,
    isSuccessful,
    label,
    manyTill,
    parseOnly,
    satisfy,
    (<+>),
    (??),
 )
import Data.Aeson qualified as Json
import Data.Attoparsec.ByteString (takeWhile1)
import Data.Attoparsec.ByteString.Char8 (
    anyChar,
    decimal,
    double,
    signed,
    skipSpace,
    takeWhile,
 )
import Data.Bits (complement, shiftL, shiftR, (.&.), (.|.))
import Data.ByteString qualified as BS
import Data.ByteString.Char8 qualified as BSC
import Data.ByteString.Lazy qualified as BSL
import Data.Map.Strict qualified as Map
import Data.Maybe (isJust, isNothing)
import Data.Text qualified as Text
import Data.Text.Encoding qualified as Text

import RON.Base64 qualified as Base64
import RON.Types (
    Atom (AFloat, AInteger, AString, AUuid),
    ClosedOp (..),
    ObjectFrame (ObjectFrame),
    Op (..),
    OpTerm (TClosed, THeader, TQuery, TReduced),
    Packer (PackChain, PackFixed, PackIncrement),
    Payload,
    StateFrame,
    UUID (UUID),
    WireChunk (Closed, Query, Value),
    WireFrame,
    WireReducedChunk (..),
    WireStateChunk (..),
 )
import RON.UUID (UuidFields (..), addValue, succValue)
import RON.UUID qualified as UUID
import RON.Util.Word (
    Word2,
    Word4,
    Word60,
    b00,
    b0000,
    b01,
    b10,
    b11,
    ls60,
    safeCast,
 )

-- | Parse a common frame
parseWireFrame :: ByteStringL -> Either String WireFrame
parseWireFrame = parseOnly frame

chunksTill :: Parser () -> Parser [WireChunk]
chunksTill end = label "[WireChunk]" $ go closedOpZero
  where
    go prev = do
        skipSpace
        atEnd <- isSuccessful end
        if atEnd then
            pure []
        else do
            (ch, lastOp) <- pChunk prev
            (ch :) <$> go lastOp

-- | Returns a chunk and the last op in it
pChunk :: ClosedOp -> Parser (WireChunk, ClosedOp)
pChunk prev = label "WireChunk" $ wireReducedChunk prev <+> chunkClosed prev

chunkClosed :: ClosedOp -> Parser (WireChunk, ClosedOp)
chunkClosed prev =
    label "WireChunk-closed" do
        skipSpace
        (_, x) <- closedOp prev
        skipSpace
        void $ char ';'
        pure (Closed x, x)

-- | Returns a chunk and the last op (converted to closed) in it
wireReducedChunk :: ClosedOp -> Parser (WireChunk, ClosedOp)
wireReducedChunk prev =
    label "WireChunk-reduced" do
        (wrcHeader, isQuery) <- header prev
        wrcBody <- reducedOps wrcHeader.objectId wrcHeader.op <|> stop
        let lastOp = lastDef wrcHeader.op wrcBody
            lastClosedOp = wrcHeader{op = lastOp}
        pure
            ( (if isQuery then Query else Value) WireReducedChunk{..}
            , lastClosedOp
            )
  where
    stop = pure []

parseStateChunk :: ByteStringL -> Either String WireStateChunk
parseStateChunk =
    parseOnly do
        (Value value, _) <- wireReducedChunk closedOpZero
        let WireReducedChunk{wrcHeader, wrcBody} = value
            ClosedOp{reducerId} = wrcHeader
        pure WireStateChunk{stateType = reducerId, stateBody = wrcBody}

frame :: Parser WireFrame
frame = label "WireFrame" $ chunksTill (endOfFrame <|> endOfInputEx)

-- | Parse a sequence of common frames
parseWireFrames :: ByteStringL -> Either String [WireFrame]
parseWireFrames = parseOnly $ manyTill frameInStream endOfInputEx

frameInStream :: Parser WireFrame
frameInStream = label "WireFrame-stream" $ chunksTill endOfFrame

-- | Parse a single context-free op
parseOp :: ByteStringL -> Either String ClosedOp
parseOp =
    parseOnly do
        (_, x) <- closedOp closedOpZero <* skipSpace <* endOfInputEx
        pure x

-- | Parse a single context-free UUID
parseUuid :: ByteStringL -> Either String UUID
parseUuid =
    parseOnly $
        uuid UUID.zero UUID.zero PrevOpSameKey <* skipSpace <* endOfInputEx

uuidFromText :: Text -> Either String UUID
uuidFromText = parseUuid . BSL.fromStrict . Text.encodeUtf8

uuidFromString :: String -> Either String UUID
uuidFromString = uuidFromText . Text.pack

-- | Parse a UUID in key position
parseUuidKey ::
    -- | same key in the previous op (default is 'UUID.zero')
    UUID ->
    -- | previous key of the same op (default is 'UUID.zero')
    UUID ->
    ByteStringL ->
    Either String UUID
parseUuidKey prevKey prev =
    parseOnly $ uuid prevKey prev PrevOpSameKey <* skipSpace <* endOfInputEx

-- | Parse a UUID in value (atom) position
parseUuidAtom ::
    -- | previous
    UUID ->
    ByteStringL ->
    Either String UUID
parseUuidAtom prev = parseOnly $ uuidAtom prev <* skipSpace <* endOfInputEx

endOfFrame :: Parser ()
endOfFrame = label "end of frame" $ void $ skipSpace *> char '.'

closedOp :: ClosedOp -> Parser (Bool, ClosedOp)
closedOp prev =
    label "ClosedOp-cont" do
        (hasTyp, reducerId) <- key "reducer" '*' prev.reducerId UUID.zero
        (hasObj, objectId) <- key "object" '#' prev.objectId reducerId
        (hasEvt, opId) <- key "opId" '@' prev.op.opId objectId
        (hasRef, refId) <- key "ref" ':' prev.op.refId opId
        payload <- pPayload objectId
        let op = Op{..}
        pure
            ( hasTyp || hasObj || hasEvt || hasRef || not (null payload)
            , ClosedOp{..}
            )

reducedOps :: UUID -> Op -> Parser [Op]
reducedOps objectId y = do
    skipSpace
    (isNotEmpty, packerM, x) <- reducedOpOrPack objectId y
    t <- optional term
    unless (t == Just TReduced || isNothing t) $
        fail "reduced op may end with `,` only"
    unless (isNotEmpty || t == Just TReduced) $ fail "Empty reduced op"
    let (ops, lastOp) = maybe ([x], x) (unpackOps x) packerM
    cont <- reducedOps objectId lastOp <|> pure []
    pure $ ops ++ cont

unpackOps :: Op -> Packer -> ([Op], Op)
unpackOps first packer =
    case first.payload of
        [AString s] ->
            ( unpackLetters first.opId first.refId $ Text.unpack s
            , lastOp $ fromIntegral $ Text.length s
            )
        [AInteger i] ->
            (unpackEmpties first.opId first.refId i, lastOp $ fromIntegral i)
        _ -> undefined
  where
    nextRef opId refId =
        case packer of
            PackChain -> opId
            PackFixed -> refId
            PackIncrement -> succValue refId

    lastRef size =
        case packer of
            PackChain -> first.opId `addValue` (size - 2)
            PackFixed -> first.refId
            PackIncrement -> first.refId `addValue` (size - 1)

    lastOp size =
        Op
            { opId = first.opId `addValue` (size - 1)
            , refId = lastRef size
            , payload = ["CANNOT HAPPEN! TODO OpHead"]
            }

    unpackLetters opId refId = \case
        [] -> []
        c : cs ->
            Op{opId, refId, payload = [AString $ Text.singleton c]}
                : unpackLetters (succValue opId) (nextRef opId refId) cs

    unpackEmpties opId refId size
        | size > 0 =
            Op{opId, refId, payload = []}
                : unpackEmpties (succValue opId) (nextRef opId refId) (size - 1)
        | otherwise = []

reducedOpOrPack :: UUID -> Op -> Parser (Bool, Maybe Packer, Op)
reducedOpOrPack opObject prev =
    label "Op-reduced-cont" do
        (hasEvt, opId) <- key "event" '@' prev.opId opObject
        (hasRef, refId) <- key "ref" ':' prev.refId opId
        packerM <- optional pPacker
        payload <- pPayload opObject
        let op = Op{opId, refId, payload}
        pure (hasEvt || hasRef || not (null payload), packerM, op)

openOp :: UUID -> Parser Op
openOp prev =
    label "Op-open-cont" do
        opId <- openKey "event" '@' <|> pure (UUID.succValue prev)
        refId <- openKey "ref" ':' <|> pure prev
        payload <- pPayload opId
        t <- term
        guard $ t == TReduced || t == TClosed
        pure Op{opId, refId, payload}

key :: String -> Char -> UUID -> UUID -> Parser (Bool, UUID)
key name keyChar prevOpSameKey sameOpPrevUuid =
    label name do
        skipSpace
        isKeyPresent <- isSuccessful $ char keyChar
        if isKeyPresent then do
            u <- uuid prevOpSameKey sameOpPrevUuid PrevOpSameKey
            pure (True, u)
        else
            -- no key => use previous key
            pure (False, prevOpSameKey)

openKey :: String -> Char -> Parser UUID
openKey name keyChar =
    label name do
        skipSpace
        _ <- char keyChar
        uuid UUID.zero UUID.zero PrevOpSameKey

uuid :: UUID -> UUID -> UuidZipBase -> Parser UUID
uuid prevOpSameKey sameOpPrevUuid defaultZipBase =
    label "UUID" $
        uuid22
            <+> uuid11
            <+> uuidZip prevOpSameKey sameOpPrevUuid defaultZipBase

uuid11 :: Parser UUID
uuid11 =
    label "UUID-RON-11-letter-value" do
        rawX <- base64word 11
        guard $ BS.length rawX == 11
        x <- Base64.decode64 rawX ?? fail "Base64.decode64"
        rawUuidVersionM <- optional pUuidVersion
        y <-
            case rawUuidVersionM of
                Nothing -> pure 0
                Just rawUuidVersion -> do
                    rawOrigin <- optional $ base64word 10
                    origin <- case rawOrigin of
                        Nothing -> pure $ ls60 0
                        Just origin ->
                            Base64.decode60 origin ?? fail "Base64.decode60"
                    pure $ UUID.buildY b00 rawUuidVersion origin
        pure $ UUID x y

data UuidZipBase = PrevOpSameKey | SameOpPrevUuid

uuidZip' :: Parser UUID
uuidZip' =
    label "UUID-zip'" do
        rawVariety <- optional pVariety
        rawValue <- base64word60 10
        rawUuidVersion <- optional pUuidVersion
        rawOrigin <- case rawUuidVersion of
            Just _ -> optional $ base64word60 10
            Nothing -> pure Nothing

        pure $
            UUID.build
                UuidFields
                    { uuidVariety = fromMaybe b0000 rawVariety
                    , uuidValue = rawValue
                    , uuidVariant = b00
                    , uuidVersion = fromMaybe b00 rawUuidVersion
                    , uuidOrigin = fromMaybe (ls60 0) rawOrigin
                    }

{-# DEPRECATED uuidZip "Deprecated since RON 2.1 ." #-}
uuidZip :: UUID -> UUID -> UuidZipBase -> Parser UUID
uuidZip prevOpSameKey sameOpPrevUuid defaultZipBase =
    label "UUID-zip" do
        changeZipBase <- isSuccessful $ char '`'
        rawVariety <- optional pVariety
        rawReuseValue <- optional pReuse
        rawValue <- optional $ base64word60 $ 10 - (rawReuseValue ?: 0)
        rawUuidVersion <- optional pUuidVersion
        rawReuseOrigin <- optional pReuse
        rawOrigin <- optional $ base64word60 $ 10 - (rawReuseOrigin ?: 0)

        let prev = UUID.split $ whichPrev changeZipBase
        let isSimple =
                prev.uuidVariant /= b00
                    || ( not changeZipBase
                            && isNothing rawReuseValue
                            && isJust rawValue
                            && isNothing rawReuseOrigin
                            && (isNothing rawUuidVersion || isJust rawOrigin)
                       )

        let fields
                | isSimple =
                    UuidFields
                        { uuidVariety = rawVariety ?: b0000
                        , uuidValue = rawValue ?: 0
                        , uuidVariant = b00
                        , uuidVersion = rawUuidVersion ?: b00
                        , uuidOrigin = rawOrigin ?: 0
                        }
                | otherwise =
                    UuidFields
                        { uuidVariety = rawVariety ?: prev.uuidVariety
                        , uuidValue =
                            reuse rawReuseValue rawValue prev.uuidValue
                        , uuidVariant = b00
                        , uuidVersion = rawUuidVersion ?: prev.uuidVersion
                        , uuidOrigin =
                            reuse rawReuseOrigin rawOrigin prev.uuidOrigin
                        }
        pure $ UUID.build fields
  where
    whichPrev changeZipBase
        | changeZipBase = sameOpPrevUuid
        | otherwise = case defaultZipBase of
            PrevOpSameKey -> prevOpSameKey
            SameOpPrevUuid -> sameOpPrevUuid

reuse :: Maybe Int -> Maybe Word60 -> Word60 -> Word60
reuse Nothing Nothing prev = prev
reuse Nothing (Just new) _ = new
reuse (Just prefixLen) Nothing prev =
    ls60 $ safeCast prev .&. complement 0 `shiftL` (60 - 6 * prefixLen)
reuse (Just prefixLen) (Just new) prev = ls60 $ prefix .|. postfix
  where
    prefix = safeCast prev .&. complement 0 `shiftL` (60 - 6 * prefixLen)
    postfix = safeCast new `shiftR` (6 * prefixLen)

pReuse :: Parser Int
pReuse =
    anyChar >>= \case
        '(' -> pure 4
        '[' -> pure 5
        '{' -> pure 6
        '}' -> pure 7
        ']' -> pure 8
        ')' -> pure 9
        _ -> fail "not a reuse symbol"

uuid22 :: Parser UUID
uuid22 =
    label "UUID-Base64-double-word" do
        xy <- base64word 22
        guard $ BS.length xy == 22
        maybe (fail "Base64 decoding error") pure $
            UUID
                <$> Base64.decode64 (BS.take 11 xy)
                <*> Base64.decode64 (BS.drop 11 xy)

base64word :: Int -> Parser ByteString
base64word maxSize =
    label "Base64 word" do
        word <- takeWhile1 Base64.isLetter
        guard $ BS.length word <= maxSize
        pure word

base64word60 :: Int -> Parser Word60
base64word60 maxSize =
    label "Base64 word60" do
        word <- base64word maxSize
        Base64.decode60 word ?? fail "decode60"

isUpperHexDigit :: Word8 -> Bool
isUpperHexDigit c =
    (fromIntegral (c - fromIntegral (ord '0')) :: Word) <= 9
        || (fromIntegral (c - fromIntegral (ord 'A')) :: Word) <= 5

pVariety :: Parser Word4
pVariety =
    label "variety" do
        letter <- satisfy isUpperHexDigit <* "/"
        Base64.decodeLetter4 letter ?? fail "Base64.decodeLetter4"

pUuidVersion :: Parser Word2
pUuidVersion =
    label "UUID-version" $
        anyChar >>= \case
            '$' -> pure b00
            '%' -> pure b01
            '+' -> pure b10
            '-' -> pure b11
            _ -> fail "not a UUID-version"

pPacker :: Parser Packer
pPacker = do
    skipSpace
    _ <- char '%'
    anyChar >>= \case
        'c' -> pure PackChain
        'f' -> pure PackFixed
        'i' -> pure PackIncrement
        _ -> fail "bad packer"

pPayload :: UUID -> Parser Payload
pPayload = label "payload" . go
  where
    go prevUuid = do
        ma <- optional $ atom prevUuid
        case ma of
            Nothing -> pure []
            Just a -> (a :) <$> go newUuid
              where
                newUuid = case a of
                    AUuid u -> u
                    _ -> prevUuid

parsePayload :: ByteStringL -> Either String Payload
parsePayload = parseOnly $ pPayload UUID.zero <* endOfInputEx

atom :: UUID -> Parser Atom
atom prevUuid = skipSpace *> atom'
  where
    atom' =
        (char '^' *> skipSpace *> (AFloat <$> double))
            <+> (char '=' *> skipSpace *> (AInteger <$> integer))
            <+> (char '>' *> skipSpace *> (AUuid <$> uuid'))
            <+> (AString <$> string)
            <+> atomUnprefixed
    integer = signed decimal
    uuid' = uuidAtom prevUuid

atomUnprefixed :: Parser Atom
atomUnprefixed =
    (AFloat <$> definiteDouble)
        <+> (AInteger <$> integer)
        <+> (AUuid <$> uuidUnzipped)
  where
    integer = signed decimal
    uuidUnzipped = uuid22 <+> uuid11 <+> uuidZip'

uuidAtom :: UUID -> Parser UUID
uuidAtom prev = uuid UUID.zero prev SameOpPrevUuid

-- | Parse an atom
parseAtom :: ByteStringL -> Either String Atom
parseAtom = parseOnly $ atom UUID.zero <* endOfInputEx

string :: Parser Text
string = do
    bs <- char '\'' *> content
    case Json.decodeStrict $ '"' `BSC.cons` (bs `BSC.snoc` '"') of
        Just s -> pure s
        Nothing -> fail "bad string"
  where
    content = do
        chunk <- takeWhile \c -> c /= '\'' && c /= '\\'
        anyChar >>= \case
            '\'' -> pure chunk
            '\\' ->
                anyChar >>= \case
                    '\'' -> (chunk <>) . BSC.cons '\'' <$> content
                    c -> (chunk <>) . BSC.cons '\\' . BSC.cons c <$> content
            _ -> fail "cannot happen"

-- | Parse a string atom
parseString :: ByteStringL -> Either String Text
parseString = parseOnly $ string <* endOfInputEx

-- | Return 'ClosedOp' and 'chunkIsQuery'
header :: ClosedOp -> Parser (ClosedOp, Bool)
header prev = do
    (_, x) <- closedOp prev
    t <- term
    case t of
        THeader -> pure (x, False)
        TQuery -> pure (x, True)
        _ -> fail "not a header"

term :: Parser OpTerm
term = do
    skipSpace
    anyChar >>= \case
        '!' -> pure THeader
        '?' -> pure TQuery
        ',' -> pure TReduced
        ';' -> pure TClosed
        _ -> fail "not a term"

{- | Parse a state frame
TODO deprecate multi-object states
-}
parseStateFrame :: ByteStringL -> Either String StateFrame
parseStateFrame = parseWireFrame >=> findObjects

-- | Parse a state frame as an object
parseObject :: UUID -> ByteStringL -> Either String (ObjectFrame a)
parseObject oid bytes = ObjectFrame oid <$> parseStateFrame bytes

{- | Extract object states from a common frame
TODO deprecate multi-object states
-}
findObjects :: WireFrame -> Either String StateFrame
findObjects = fmap Map.fromList . traverse loadBody
  where
    loadBody = \case
        Value WireReducedChunk{wrcHeader, wrcBody} -> do
            let ClosedOp{reducerId, objectId} = wrcHeader
            pure
                ( objectId
                , WireStateChunk{stateType = reducerId, stateBody = wrcBody}
                )
        _ -> Left "expected reduced chunk"

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

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

parseOpenFrame :: ByteStringL -> Either String [Op]
parseOpenFrame =
    parseOnly $ go UUID.zero <* skipSpace <* endOfInputEx
  where
    go :: UUID -> Parser [Op]
    go prev =
        ( do
            op@Op{opId} <- openOp prev
            (op :) <$> go opId
        )
            <|> pure []

parseOpenOp :: ByteStringL -> Either String Op
parseOpenOp = parseOnly $ openOp UUID.zero <* skipSpace <* endOfInputEx