mqtt-hs-0.2.0: Network/MQTT/Types.hs
{-# Language GeneralizedNewtypeDeriving,
PatternSynonyms,
OverloadedStrings,
DataKinds,
KindSignatures,
GADTs,
TypeFamilies,
ScopedTypeVariables,
TemplateHaskell
#-}
{-|
Module: MQTT.Types
Copyright: Lukas Braun 2014
License: GPL-3
Maintainer: koomi+mqtt@hackerspace-bamberg.de
Types representing MQTT messages.
-}
module Network.MQTT.Types
( -- * Messages
Message(..)
, SomeMessage(..)
, MqttHeader(..)
-- * Message bodies
, MessageBody(..)
, Connect(..)
, ConnAck(..)
, Publish(..)
, Subscribe(..)
, SubAck(..)
, Unsubscribe(..)
, SimpleMsg(..)
-- * Miscellaneous
, Will(..)
, QoS(..)
, MsgID
, getMsgID
, Topic
, fromTopic
, toTopic
, matches
, MqttText(..)
-- * Message types
, MsgType(..)
, toMsgType
, toMsgType'
-- ** Singletons
-- | Singletons are used to build a bridge between the type and value level.
-- See the @singletons@ package for more information.
--
-- You do not have to use or understand these in order to use this
-- library, they are mostly used internally to get better guarantees
-- about the flow of 'Message's.
, toSMsgType
, SMsgType
, Sing( SCONNECT
, SCONNACK
, SPUBLISH
, SPUBACK
, SPUBREC
, SPUBREL
, SPUBCOMP
, SSUBSCRIBE
, SSUBACK
, SUNSUBSCRIBE
, SUNSUBACK
, SPINGREQ
, SPINGRESP
, SDISCONNECT)
) where
import Data.ByteString (ByteString)
import Data.Singletons.TH
import Data.String (IsString(..))
import Data.Text (Text)
import qualified Data.Text as T
import Data.Word
-- | A MQTT message, indexed by the type of the message ('MsgType').
data Message (t :: MsgType)
= Message
{ header :: MqttHeader
, body :: MessageBody t
}
-- | Any message, hiding the index.
data SomeMessage where
SomeMessage :: Message t -> SomeMessage
-- | Fixed header required in every message.
data MqttHeader
= Header
{ -- msgType :: MsgType -- ^ Type of the message
dup :: Bool -- ^ Has this message been sent before?
, qos :: QoS -- ^ Quality of Service-level
, retain :: Bool -- ^ Should the broker retain the message for
-- future subscribers?
}
deriving (Eq, Ord, Show)
-- | The body of a MQTT message, indexed by the type of the message ('MsgType').
data MessageBody (t :: MsgType) where
MConnect :: Connect -> MessageBody CONNECT
MConnAck :: ConnAck -> MessageBody CONNACK
MPublish :: Publish -> MessageBody PUBLISH
MPubAck :: SimpleMsg -> MessageBody PUBACK
MPubRec :: SimpleMsg -> MessageBody PUBREC
MPubRel :: SimpleMsg -> MessageBody PUBREL
MPubComp :: SimpleMsg -> MessageBody PUBCOMP
MSubscribe :: Subscribe -> MessageBody SUBSCRIBE
MSubAck :: SubAck -> MessageBody SUBACK
MUnsubscribe :: Unsubscribe -> MessageBody UNSUBSCRIBE
MUnsubAck :: SimpleMsg -> MessageBody UNSUBACK
MPingReq :: MessageBody PINGREQ
MPingResp :: MessageBody PINGRESP
MDisconnect :: MessageBody DISCONNECT
-- | The fields of a CONNECT message.
data Connect
= Connect
{ cleanSession :: Bool
-- ^ Should the server forget subscriptions and other state on
-- disconnects?
, will :: Maybe Will
-- ^ Optional 'Will' message.
, clientID :: MqttText
-- ^ Client ID used by the server to identify clients.
, username :: Maybe MqttText
-- ^ Optional username used for authentication.
, password :: Maybe MqttText
-- ^ Optional password used for authentication.
, keepAlive :: Word16
-- ^ Maximum interval (in seconds) in which a message must be sent.
-- 0 means no limit.
} deriving (Show, Eq)
-- | The response to a CONNECT. Anything other than 0 means the broker
-- refused the connection
-- (<http://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/mqtt-v3r1.html#connack details>).
newtype ConnAck = ConnAck { returnCode :: Word8 }
deriving (Show, Eq)
-- | The fields of a PUBLISH message.
data Publish
= Publish
{ topic :: Topic
-- ^ The 'Topic' to which the message should be published.
, pubMsgID :: Maybe MsgID
-- ^ 'MsgID' of the message if 'QoS' > 'NoConfirm'.
, payload :: ByteString
-- ^ The content that will be published.
} deriving (Show, Eq)
-- | The fields of a SUBSCRIBE message.
data Subscribe
= Subscribe
{ subscribeMsgID :: MsgID
, subTopics :: [(Topic, QoS)]
-- ^ The 'Topic's and corresponding requested 'QoS'.
} deriving (Show, Eq)
-- | The fields of a SUBACK message.
data SubAck
= SubAck
{ subAckMsgID :: MsgID
, granted :: [QoS]
-- ^ The 'QoS' granted for each 'Topic' in the order they were sent
-- in the SUBSCRIBE.
} deriving (Show, Eq)
-- | The fields of a UNSUBSCRIBE message.
data Unsubscribe
= Unsubscribe
{ unsubMsgID :: MsgID
, unsubTopics :: [Topic]
-- ^ The 'Topic's from which the client should be unsubscribed.
} deriving (Show, Eq)
-- | Any message body that consists only of a 'MsgID'.
newtype SimpleMsg = SimpleMsg { msgID :: MsgID }
deriving (Show, Eq)
-- | The different levels of QoS
data QoS
= NoConfirm -- ^ Fire and forget
| Confirm -- ^ Acknowledged delivery (repeat until ack)
| Handshake -- ^ Assured delivery (four-step handshake)
deriving (Eq, Ord, Enum, Show)
-- | A Will message is published by the broker if a client disconnects
-- without sending a DISCONNECT.
data Will
= Will
{ wRetain :: Bool
, wQoS :: QoS
, wTopic :: Topic
, wMsg :: MqttText
}
deriving (Eq, Show)
-- | MQTT uses length-prefixed UTF-8 as text encoding.
newtype MqttText = MqttText { text :: Text }
deriving (Eq, Show, IsString)
-- | A topic is a "hierarchical name space that defines a taxonomy of
-- information sources for which subscribers can register an interest."
--
-- See
-- <http://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/mqtt-v3r1.html#appendix-a here>
-- for more information on topics.
data Topic = Topic { levels :: [Text], orig :: Text }
-- levels and orig should always refer to the same topic, this way no text
-- has to be copied when converting from/to text
type MsgID = Word16
-- | Get the message ID of any message, if it exists.
getMsgID :: MessageBody t -> Maybe MsgID
getMsgID (MConnect _) = Nothing
getMsgID (MConnAck _) = Nothing
getMsgID (MPublish pub) = pubMsgID pub
getMsgID (MPubAck simple) = Just (msgID simple)
getMsgID (MPubRec simple) = Just (msgID simple)
getMsgID (MPubRel simple) = Just (msgID simple)
getMsgID (MPubComp simple) = Just (msgID simple)
getMsgID (MSubscribe sub) = Just (subscribeMsgID sub)
getMsgID (MSubAck subA) = Just (subAckMsgID subA)
getMsgID (MUnsubscribe unsub) = Just (unsubMsgID unsub)
getMsgID (MUnsubAck simple) = Just (msgID simple)
getMsgID MPingReq = Nothing
getMsgID MPingResp = Nothing
getMsgID MDisconnect = Nothing
instance Show Topic where
show (Topic _ t) = show t
instance Eq Topic where
Topic _ t1 == Topic _ t2 = t1 == t2
-- | Check if one of the 'Topic's matches the other.
matches :: Topic -> Topic -> Bool
matches (Topic t1 _) (Topic t2 _) = go t1 t2
where
go [] [] = True
go [] (l:_) = l == "#"
go (l:_) [] = l == "#"
go (l1:ls1) (l2:ls2) = l1 == "#" || l2 == "#"
|| ((l1 == "+" || l2 == "+" || l1 == l2)
&& go ls1 ls2)
toTopic :: MqttText -> Topic
toTopic (MqttText txt) = Topic (T.split (== '/') txt) txt
fromTopic :: Topic -> MqttText
fromTopic = MqttText . orig
instance IsString Topic where
fromString str = let txt = T.pack str in
Topic (T.split (== '/') txt) txt
-- | The various types of messages.
data MsgType
= CONNECT
| CONNACK
| PUBLISH
| PUBACK
| PUBREC
| PUBREL
| PUBCOMP
| SUBSCRIBE
| SUBACK
| UNSUBSCRIBE
| UNSUBACK
| PINGREQ
| PINGRESP
| DISCONNECT
deriving (Eq, Enum, Ord, Show)
genSingletons [''MsgType]
singDecideInstance ''MsgType
-- | Determine the 'MsgType' of a 'Message'.
toMsgType :: Message t -> MsgType
toMsgType msg =
case body msg of
MConnect _ -> CONNECT
MConnAck _ -> CONNACK
MPublish _ -> PUBLISH
MPubAck _ -> PUBACK
MPubRec _ -> PUBREC
MPubRel _ -> PUBREL
MPubComp _ -> PUBCOMP
MSubscribe _ -> SUBSCRIBE
MSubAck _ -> SUBACK
MUnsubscribe _ -> UNSUBSCRIBE
MUnsubAck _ -> UNSUBACK
MPingReq -> PINGREQ
MPingResp -> PINGRESP
MDisconnect -> DISCONNECT
-- | Determine the 'MsgType' of a 'SomeMessage'.
toMsgType' :: SomeMessage -> MsgType
toMsgType' (SomeMessage msg) = toMsgType msg
-- | Determine the singleton 'SMsgType' of a 'Message'.
toSMsgType :: Message t -> SMsgType t
toSMsgType msg =
case body msg of
MConnect _ -> SCONNECT
MConnAck _ -> SCONNACK
MPublish _ -> SPUBLISH
MPubAck _ -> SPUBACK
MPubRec _ -> SPUBREC
MPubRel _ -> SPUBREL
MPubComp _ -> SPUBCOMP
MSubscribe _ -> SSUBSCRIBE
MSubAck _ -> SSUBACK
MUnsubscribe _ -> SUNSUBSCRIBE
MUnsubAck _ -> SUNSUBACK
MPingReq -> SPINGREQ
MPingResp -> SPINGRESP
MDisconnect -> SDISCONNECT