hask-redis-mux-0.1.0.0: lib/resp/Database/Redis/Resp.hs
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Serialization and parsing for the Redis Serialization Protocol (RESP).
-- Supports RESP2 and RESP3 wire types including bulk strings, arrays, maps, and sets.
--
-- @since 0.1.0.0
module Database.Redis.Resp
( RespData (..)
, Encodable (..)
, parseRespData
, parseStrict
) where
import Control.Applicative ((<|>))
import qualified Data.Attoparsec.ByteString as StrictParse
import qualified Data.Attoparsec.ByteString.Char8 as Char8
import Data.ByteString (ByteString)
import qualified Data.ByteString.Builder as Builder
import qualified Data.ByteString.Char8 as BS8
import qualified Data.Map as M
import qualified Data.Set as S
-- | A value in the RESP wire protocol. Each constructor corresponds to a RESP type prefix.
data RespData where
RespSimpleString :: !ByteString -> RespData
RespError :: !ByteString -> RespData
RespInteger :: !Integer -> RespData
RespBulkString :: !ByteString -> RespData
RespNullBulkString :: RespData
RespArray :: ![RespData] -> RespData
RespMap :: !(M.Map RespData RespData) -> RespData
RespSet :: !(S.Set RespData) -> RespData
deriving (Eq, Ord)
instance Show RespData where
show :: RespData -> String
show = BS8.unpack . byteShow
byteShow :: RespData -> ByteString
byteShow (RespSimpleString !s) = s
byteShow (RespError !s) = "ERROR: " <> s
byteShow (RespInteger !i) = BS8.pack . show $ i
byteShow (RespBulkString !s) = s
byteShow RespNullBulkString = "NULL"
byteShow (RespArray !xs) = BS8.intercalate "\n" (map byteShow xs)
byteShow (RespSet !s) = BS8.intercalate "\n" (map byteShow (S.toList s))
byteShow (RespMap !m) = BS8.intercalate "\n" (map showPair (M.toList m))
where
showPair (k, v) = byteShow k <> ": " <> byteShow v
-- | Types that can be serialized to the RESP wire format.
class Encodable a where
encode :: a -> Builder.Builder
instance Encodable RespData where
encode :: RespData -> Builder.Builder
encode (RespSimpleString !s) = Builder.char8 '+' <> Builder.byteString s <> Builder.byteString "\r\n"
encode (RespError !s) = Builder.char8 '-' <> Builder.byteString s <> Builder.byteString "\r\n"
encode (RespInteger !i) = Builder.char8 ':' <> Builder.integerDec i <> Builder.byteString "\r\n"
encode (RespBulkString !s) = Builder.char8 '$' <> Builder.intDec (BS8.length s) <> Builder.byteString "\r\n" <> Builder.byteString s <> Builder.byteString "\r\n"
encode RespNullBulkString = Builder.byteString "$-1\r\n"
encode (RespArray !xs) = Builder.char8 '*' <> Builder.intDec (length xs) <> Builder.byteString "\r\n" <> foldMap encode xs
encode (RespSet !s) = Builder.char8 '~' <> Builder.intDec (S.size s) <> Builder.byteString "\r\n" <> foldMap encode (S.toList s)
encode (RespMap !m) = Builder.char8 '*' <> Builder.intDec (M.size m) <> Builder.byteString "\r\n" <> foldMap encodePair (M.toList m)
where
encodePair (k, v) = encode k <> encode v
-- | Attoparsec parser for a single RESP value. Can be composed with other parsers
-- or used with 'parseStrict' for one-shot parsing.
parseRespData :: Char8.Parser RespData
parseRespData =
Char8.anyChar >>= \case
'+' -> parseSimpleString
'-' -> parseError
':' -> parseInteger
'$' -> parseBulkString <|> parseNullBulkString
'*' -> parseArray
'~' -> parseSet
'%' -> parseMap
v -> do
rest <- Char8.takeByteString
let catted = BS8.cons v rest
fail ("Invalid RESP data type: Remaining string " <> BS8.unpack catted)
{-# INLINE parseRespData #-}
parseSimpleString :: Char8.Parser RespData
parseSimpleString = do
!s <- Char8.takeTill (== '\r')
_ <- Char8.take 2
return $ RespSimpleString s
{-# INLINE parseSimpleString #-}
parseError :: Char8.Parser RespData
parseError = do
!s <- Char8.takeTill (== '\r')
_ <- Char8.take 2
return $ RespError s
{-# INLINE parseError #-}
parseInteger :: Char8.Parser RespData
parseInteger = do
!i <- Char8.signed Char8.decimal
_ <- Char8.endOfLine
return $ RespInteger i
{-# INLINE parseInteger #-}
parseBulkString :: Char8.Parser RespData
parseBulkString = do
!len <- Char8.decimal
_ <- Char8.endOfLine
!s <- Char8.take len
_ <- Char8.endOfLine
return $ RespBulkString s
{-# INLINE parseBulkString #-}
parseNullBulkString :: Char8.Parser RespData
parseNullBulkString = do
_ <- Char8.char '-'
_ <- Char8.char '1'
_ <- Char8.endOfLine
return RespNullBulkString
parseArray :: Char8.Parser RespData
parseArray = do
!len <- Char8.decimal
_ <- Char8.endOfLine
!xs <- Char8.count len parseRespData
return $ RespArray xs
parseSet :: Char8.Parser RespData
parseSet = do
!len <- Char8.decimal
_ <- Char8.endOfLine
!xs <- Char8.count len parseRespData
return $ RespSet (S.fromList xs)
parseMap :: Char8.Parser RespData
parseMap = do
!len <- Char8.decimal
_ <- Char8.endOfLine
!pairs <- Char8.count len parsePair
return $ RespMap (M.fromList pairs)
where
parsePair = do
k <- parseRespData
v <- parseRespData
return (k, v)
-- | Parse strict ByteString into RespData
parseStrict :: BS8.ByteString -> Either String RespData
parseStrict = StrictParse.parseOnly parseRespData