hats-0.1.0.0: src/Network/Nats/Message/Writer.hs
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
-- |
-- Module: Network.Nats.Message.Writer
-- Copyright: (c) 2016 Patrik Sandahl
-- License: MIT
-- Maintainer: Patrik Sandahl <patrik.sandahl@gmail.com>
-- 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