{-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} -- | -- Module: Network.Nats.Message.Writer -- Copyright: (c) 2016 Patrik Sandahl -- License: MIT -- Maintainer: Patrik Sandahl -- Stability: experimental -- Portability: portable -- -- Serialize NATS 'Message's to 'LBS.ByteString's. module Network.Nats.Message.Writer ( writeMessage ) where import Data.ByteString (ByteString) import Data.ByteString.Builder import Data.Monoid ((<>)) import Data.List (foldl', intersperse) import qualified Data.ByteString.Lazy as LBS import Network.Nats.Message.Message (Message (..), ProtocolError (..)) import Network.Nats.Types (Sid) -- | Existentially quantified Field type, to allow for a polymorph -- list of Fields. All fields with the contraint of beeing Writeable. data Field = forall w. Writeable w => Field !w -- | Helper class used for the writing of "handshake message" fields. class Writeable w where write :: w -> Builder -- | Instance for 'Bool'. instance Writeable Bool where write False = byteString "false" write True = byteString "true" -- | Instance for 'Int'. instance Writeable Int where write = intDec -- | Instance for 'ByteString'. instance Writeable ByteString where write value = charUtf8 '\"' <> byteString value <> charUtf8 '\"' -- | Translate a 'Message' value to a 'LBS.ByteString'. writeMessage :: Message -> LBS.ByteString writeMessage = toLazyByteString . writeMessage' -- | Translate a Message value to a Builder. writeMessage' :: Message -> Builder -- The first of the handshake messages; INFO. writeMessage' INFO {..} = let fields = foldl' writeField [] [ ("\"server_id\"", Field <$> serverId) , ("\"version\"", Field <$> serverVersion) , ("\"go\"", Field <$> goVersion) , ("\"host\"", Field <$> serverHost) , ("\"port\"", Field <$> serverPort) , ("\"auth_required\"", Field <$> serverAuthRequired) , ("\"ssl_required\"", Field <$> serverSslRequired) , ("\"tls_required\"", Field <$> serverTlsRequired) , ("\"tls_verify\"", Field <$> serverTlsVerify) , ("\"max_payload\"", Field <$> maxPayload) ] fields' = intersperse (charUtf8 ',') $ reverse fields in mconcat $ byteString "INFO {":(fields' ++ [charUtf8 '}']) -- The second of the handshake messages; CONNECT. writeMessage' CONNECT {..} = let fields = foldl' writeField [] [ ("\"verbose\"", Field <$> clientVerbose) , ("\"pedantic\"", Field <$> clientPedantic) , ("\"ssl_required\"", Field <$> clientSslRequired) , ("\"auth_token\"", Field <$> clientAuthToken) , ("\"user\"", Field <$> clientUser) , ("\"pass\"", Field <$> clientPass) , ("\"name\"", Field <$> clientName) , ("\"lang\"", Field <$> clientLang) , ("\"version\"", Field <$> clientVersion) ] fields' = intersperse (charUtf8 ',') $ reverse fields in mconcat $ byteString "CONNECT {":(fields' ++ [byteString "}\r\n"]) -- MSG message without a reply subject. writeMessage' (MSG subject sid Nothing payload) = byteString "MSG " <> byteString subject <> charUtf8 ' ' <> writeSid sid <> charUtf8 ' ' <> int64Dec (LBS.length payload) <> byteString "\r\n" <> lazyByteString payload <> byteString "\r\n" -- MSG message with a reply subject. writeMessage' (MSG subject sid (Just reply) payload) = byteString "MSG " <> byteString subject <> charUtf8 ' ' <> writeSid sid <> charUtf8 ' ' <> byteString reply <> charUtf8 ' ' <> int64Dec (LBS.length payload) <> byteString "\r\n" <> lazyByteString payload <> byteString "\r\n" -- PUB message without a reply subject. writeMessage' (PUB subject Nothing payload) = byteString "PUB " <> byteString subject <> charUtf8 ' ' <> int64Dec (LBS.length payload) <> byteString "\r\n" <> lazyByteString payload <> byteString "\r\n" -- PUB message with a reply subject. writeMessage' (PUB subject (Just reply) payload) = byteString "PUB " <> byteString subject <> charUtf8 ' ' <> byteString reply <> charUtf8 ' ' <> int64Dec (LBS.length payload) <> byteString "\r\n" <> lazyByteString payload <> byteString "\r\n" -- SUB message without a queue group. writeMessage' (SUB subject Nothing sid) = byteString "SUB " <> byteString subject <> charUtf8 ' ' <> writeSid sid <> byteString "\r\n" -- SUB message with a queue group. writeMessage' (SUB subject (Just queue) sid) = byteString "SUB " <> byteString subject <> charUtf8 ' ' <> byteString queue <> charUtf8 ' ' <> writeSid sid <> byteString "\r\n" -- UNSUB message without auto-unsubscribe limit. writeMessage' (UNSUB sid Nothing) = byteString "UNSUB " <> writeSid sid <> byteString "\r\n" -- UNSUB message with auto-unsubscribe limit. writeMessage' (UNSUB sid (Just maxMsgs)) = byteString "UNSUB " <> writeSid sid <> charUtf8 ' ' <> intDec maxMsgs <> byteString "\r\n" -- PING message. writeMessage' PING = byteString "PING" <> byteString "\r\n" -- PONG message. writeMessage' PONG = byteString "PONG" <> byteString "\r\n" -- Server acknowledge of a well-formed message. writeMessage' OK = byteString "+OK\r\n" -- | Server indication of a protocol, authorization, or other -- runtime connection error. writeMessage' (ERR pe) = byteString "-ERR " <> writePE pe <> "\r\n" -- | The translate a Field to a Builder and prepend it to the list of -- Builders. writeField :: [Builder] -> (ByteString, Maybe Field) -> [Builder] writeField xs (name, Just (Field value)) = let x = byteString name <> charUtf8 ':' <> write value in x:xs -- There's a Nothing Field. Just return the unmodified Builder list. writeField xs (_, Nothing) = xs -- | Translate a ProtocolError to a Builder. writePE :: ProtocolError -> Builder writePE UnknownProtocolOperation = byteString "\'Unknown Protocol Operation\'" writePE AuthorizationViolation = byteString "\'Authorization Violation\'" writePE AuthorizationTimeout = byteString "\'Authorization Timeout\'" writePE ParserError = byteString "\'Parser Error\'" writePE StaleConnection = byteString "\'Stale Connection\'" writePE SlowConsumer = byteString "\'Slow Consumer\'" writePE MaximumPayloadExceeded = byteString "\'Maximum Payload Exceeded\'" writePE InvalidSubject = byteString "\'Invalid Subject\'" -- | Translate a Sid to a Builder. writeSid :: Sid -> Builder writeSid = int64Dec