net-mqtt 0.7.1.1 → 0.8.0.0
raw patch · 7 files changed
+109/−39 lines, 7 filesdep +checkersPVP ok
version bump matches the API change (PVP)
Dependencies added: checkers
API changes (from Hackage documentation)
- Network.MQTT.Arbitrary: data ATopic
- Network.MQTT.Arbitrary: instance GHC.Classes.Eq Network.MQTT.Arbitrary.ATopic
- Network.MQTT.Arbitrary: instance GHC.Show.Show Network.MQTT.Arbitrary.ATopic
- Network.MQTT.Arbitrary: instance Test.QuickCheck.Arbitrary.Arbitrary Network.MQTT.Arbitrary.ATopic
- Network.MQTT.Arbitrary: unTopic :: ATopic -> Topic
- Network.MQTT.Client: type Topic = Text
- Network.MQTT.Topic: type Filter = Text
- Network.MQTT.Topic: type Topic = Text
+ Network.MQTT.Arbitrary: instance Test.QuickCheck.Arbitrary.Arbitrary Network.MQTT.Topic.Topic
+ Network.MQTT.Client: data Topic
+ Network.MQTT.Topic: data Filter
+ Network.MQTT.Topic: data Topic
+ Network.MQTT.Topic: instance Data.String.IsString Network.MQTT.Topic.Filter
+ Network.MQTT.Topic: instance Data.String.IsString Network.MQTT.Topic.Topic
+ Network.MQTT.Topic: instance GHC.Base.Semigroup Network.MQTT.Topic.Filter
+ Network.MQTT.Topic: instance GHC.Base.Semigroup Network.MQTT.Topic.Topic
+ Network.MQTT.Topic: instance GHC.Classes.Eq Network.MQTT.Topic.Filter
+ Network.MQTT.Topic: instance GHC.Classes.Eq Network.MQTT.Topic.Topic
+ Network.MQTT.Topic: instance GHC.Classes.Ord Network.MQTT.Topic.Filter
+ Network.MQTT.Topic: instance GHC.Classes.Ord Network.MQTT.Topic.Topic
+ Network.MQTT.Topic: instance GHC.Show.Show Network.MQTT.Topic.Filter
+ Network.MQTT.Topic: instance GHC.Show.Show Network.MQTT.Topic.Topic
+ Network.MQTT.Topic: instance Network.MQTT.Topic.Splittable Network.MQTT.Topic.Filter
+ Network.MQTT.Topic: instance Network.MQTT.Topic.Splittable Network.MQTT.Topic.Topic
+ Network.MQTT.Topic: mkFilter :: Text -> Maybe Filter
+ Network.MQTT.Topic: mkTopic :: Text -> Maybe Topic
+ Network.MQTT.Topic: split :: Splittable a => a -> [a]
+ Network.MQTT.Topic: unFilter :: Filter -> Text
+ Network.MQTT.Topic: unTopic :: Topic -> Text
- Network.MQTT.Arbitrary: arbitraryTopic :: [Char] -> (Int, Int) -> (Int, Int) -> Gen ATopic
+ Network.MQTT.Arbitrary: arbitraryTopic :: [Char] -> (Int, Int) -> (Int, Int) -> Gen Topic
Files
- Changelog.md +12/−0
- app/mqtt-watch/Main.hs +5/−3
- net-mqtt.cabal +3/−2
- src/Network/MQTT/Arbitrary.hs +13/−18
- src/Network/MQTT/Client.hs +16/−10
- src/Network/MQTT/Topic.hs +45/−6
- test/Spec.hs +15/−0
Changelog.md view
@@ -1,5 +1,17 @@ # Changelog for net-mqtt +## 0.8.0.0++The `Topic` and `Filter` types are now `newtype` wrappers around Text+instead of being aliases for text. This is a breaking change, but+makes a lot of bugs harder to express. There's a `split` function+available that will split a `Topic` or `Filter` into components, and+both types are `Semigroup`s joining on `/`.++Because these are `newtype`s, we the `Arbitrary` instances don't need+special wrappers, so `ATopic` is gone in favor of just `Topic`'s+`Arbitrary` instance.+ ## 0.7.1.0 More Arbitrary topic helpers.
app/mqtt-watch/Main.hs view
@@ -13,9 +13,11 @@ import qualified Data.ByteString.Lazy.Char8 as BCS import qualified Data.IORef as R import Data.Maybe (fromJust)+import qualified Data.Text as T import qualified Data.Text.IO as TIO import Data.Word (Word32) import Network.MQTT.Client+import Network.MQTT.Topic (Filter, Topic, mkFilter, unTopic) import Network.MQTT.Types (ConnACKFlags (..), SessionReuse (..), qosFromInt) import Network.URI import Options.Applicative (Parser, argument, auto, eitherReader, execParser, fullDesc, help, helper,@@ -33,7 +35,7 @@ , optVerbose :: Bool , optQoS :: QoS , optSubResume :: Bool- , optTopics :: [Topic]+ , optTopics :: [Filter] } options :: Parser Options@@ -44,7 +46,7 @@ <*> switch (short 'v' <> long "verbose" <> help "enable debug logging") <*> option (eitherReader pQoS) (long "qos" <> short 'q' <> showDefault <> value QoS0 <> help "QoS level (0-2)") <*> switch (long "always-subscribe" <> help "subscribe even when resuming a connection")- <*> some (argument str (metavar "topics..."))+ <*> some (argument (maybeReader (mkFilter . T.pack)) (metavar "topics...")) where pQoS = maybe (Left "Only QoS 0, 1, and 2 are supported") Right . (qosFromInt <=< readMaybe)@@ -52,7 +54,7 @@ printer :: TChan Msg -> Bool -> Bool -> IO () printer ch showProps verbose = forever $ do (Msg t m props) <- atomically $ readTChan ch- TIO.putStr $ mconcat [t, " → "]+ TIO.putStr $ mconcat [unTopic t, " → "] BL.hPut stdout m putStrLn "" mapM_ (putStrLn . (" " <>) . drop 4 . show) (filter (viewableProp showProps verbose) props)
net-mqtt.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 61a8c0f6c844ddab092db68ee8410eaaa215e5ca65232f4b17cec8d5cc842ecc+-- hash: b407b40c8336bc1b3e8490c3097a2bf78450622686c0e5c0e621e33675b4829d name: net-mqtt-version: 0.7.1.1+version: 0.8.0.0 synopsis: An MQTT Protocol Implementation. description: Please see the README on GitHub at <https://github.com/dustin/mqtt-hs#readme> category: Network@@ -132,6 +132,7 @@ , base >=4.7 && <5 , binary >=0.8.5 && <0.9 , bytestring >=0.10.8 && <0.12+ , checkers , conduit >=1.3.1 && <1.4 , conduit-extra >=1.3.0 && <1.4 , connection >=0.2.0 && <0.4
src/Network/MQTT/Arbitrary.hs view
@@ -12,11 +12,12 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TupleSections #-}+{-# LANGUAGE ViewPatterns #-} {-# OPTIONS_GHC -Wno-orphans #-} module Network.MQTT.Arbitrary ( SizeT(..),- ATopic, unTopic, MatchingTopic(..),+ MatchingTopic(..), arbitraryTopicSegment, arbitraryTopic, arbitraryMatchingTopic, v311mask ) where@@ -25,9 +26,10 @@ import qualified Data.ByteString.Char8 as BC import qualified Data.ByteString.Lazy as L import Data.Function ((&))+import Data.Maybe (mapMaybe) import Data.Text (Text) import qualified Data.Text as Text-import Network.MQTT.Topic (Filter, Topic)+import Network.MQTT.Topic (Filter, Topic, mkFilter, mkTopic, unTopic) import Network.MQTT.Types as MT import Test.QuickCheck as QC @@ -216,19 +218,12 @@ v311mask (PubCOMPPkt (PubCOMP x _ _)) = PubCOMPPkt (PubCOMP x 0 mempty) v311mask x = x --- | An arbitrary topic.-newtype ATopic = ATopic [Text] deriving (Show, Eq)--instance Arbitrary ATopic where+instance Arbitrary Topic where arbitrary = arbitraryTopic ['a'..'z'] (1,6) (1,6) - shrink (ATopic x) = fmap ATopic . shrinkList shrinkWord $ x+ shrink (unTopic -> x) = mapMaybe (mkTopic . Text.intercalate "/") . shrinkList shrinkWord $ Text.splitOn "/" x where shrinkWord = fmap Text.pack . shrink . Text.unpack --- | Retrieve the Topic from ATopic-unTopic :: ATopic -> Topic-unTopic (ATopic t) = Text.intercalate "/" t- -- | An arbitrary Topic and an arbitrary Filter that should match it. newtype MatchingTopic = MatchingTopic (Topic, [Filter]) deriving (Eq, Show) @@ -241,11 +236,10 @@ arbitraryTopicSegment :: [Char] -> Int -> Gen Text arbitraryTopicSegment alphabet n = Text.pack <$> vectorOf n (elements alphabet) --- | Generate an arbitrary ATopic from the given alphabet with lengths+-- | Generate an arbitrary Topic from the given alphabet with lengths -- of segments and the segment count specified by the given ranges.-arbitraryTopic :: [Char] -> (Int,Int) -> (Int,Int) -> Gen ATopic-arbitraryTopic alphabet seglen nsegs = do- ATopic <$> someSegs+arbitraryTopic :: [Char] -> (Int,Int) -> (Int,Int) -> Gen Topic+arbitraryTopic alphabet seglen nsegs = someSegs `suchThatMap` (mkTopic . Text.intercalate "/") where someSegs = choose nsegs >>= flip vectorOf aSeg aSeg = choose seglen >>= arbitraryTopicSegment alphabet @@ -253,11 +247,12 @@ -- as some arbitrary filters that should match that topic. arbitraryMatchingTopic :: [Char] -> (Int,Int) -> (Int,Int) -> (Int,Int) -> Gen (Topic, [Filter]) arbitraryMatchingTopic alphabet seglen nsegs nfilts = do- t@(ATopic tsegs) <- arbitraryTopic alphabet seglen nsegs+ t <- arbitraryTopic alphabet seglen nsegs+ let tsegs = Text.splitOn "/" (unTopic t) fn <- choose nfilts reps <- vectorOf fn $ vectorOf (length tsegs) (elements [id, const "+", const "#"])- let m = map (unTopic . ATopic . clean . zipWith (&) tsegs) reps- pure $ (unTopic t, m)+ let m = mapMaybe (mkFilter . Text.intercalate "/" . clean . zipWith (&) tsegs) reps+ pure (t, m) where clean [] = [] clean ("#":_) = ["#"]
src/Network/MQTT/Client.hs view
@@ -55,7 +55,7 @@ import Data.Conduit.Network.TLS (runTLSClient, tlsClientConfig, tlsClientTLSSettings) import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map-import Data.Maybe (fromMaybe)+import Data.Maybe (fromJust, fromMaybe) import Data.Text (Text) import qualified Data.Text.Encoding as TE import Data.Word (Word16)@@ -68,7 +68,7 @@ import System.IO.Error (catchIOError, isEOFError) import System.Timeout (timeout) -import Network.MQTT.Topic (Filter, Topic)+import Network.MQTT.Topic (Filter, Topic, mkFilter, mkTopic, unFilter, unTopic) import Network.MQTT.Types as T data ConnState = Starting@@ -426,7 +426,7 @@ corrs <- readTVarIO _corr E.evaluate . force =<< case maybe _cb (\cd -> Map.findWithDefault _cb cd corrs) cdata of NoCallback -> pure ()- SimpleCallback f -> call (f c (blToText _pubTopic) _pubBody _pubProps)+ SimpleCallback f -> call (f c (blToTopic _pubTopic) _pubBody _pubProps) LowLevelCallback f -> call (f c p) where@@ -438,15 +438,15 @@ resolve p@PublishRequest{..} = do topic <- resolveTopic (foldr aliasID Nothing _pubProps)- pure p{_pubTopic=textToBL topic}+ pure p{_pubTopic=textToBL (unTopic topic)} where aliasID (PropTopicAlias x) _ = Just x aliasID _ o = o - resolveTopic Nothing = pure (blToText _pubTopic)+ resolveTopic Nothing = pure (blToTopic _pubTopic) resolveTopic (Just x) = do- when (_pubTopic /= "") $ modifyTVar' _inA (Map.insert x (blToText _pubTopic))+ when (_pubTopic /= "") $ modifyTVar' _inA (Map.insert x (blToTopic _pubTopic)) m <- readTVar _inA case Map.lookup x m of Nothing -> mqttFail ("failed to lookup topic alias " <> show x)@@ -496,6 +496,12 @@ blToText :: BL.ByteString -> Text blToText = TE.decodeUtf8 . BL.toStrict +blToTopic :: BL.ByteString -> Topic+blToTopic = fromJust . mkTopic . blToText++blToFilter :: BL.ByteString -> Filter+blToFilter = fromJust . mkFilter . blToText+ reservePktID :: MQTTClient -> [DispatchType] -> STM (TChan MQTTPkt, Word16) reservePktID c@MQTTClient{..} dts = do checkConnected c@@ -534,7 +540,7 @@ let (SubACKPkt (SubscribeResponse _ rs aprops)) = r pure (rs, aprops) - where ls' = map (first textToBL) ls+ where ls' = map (first (textToBL . unFilter)) ls -- | Unsubscribe from a list of topic filters. --@@ -545,7 +551,7 @@ -- should know about. unsubscribe :: MQTTClient -> [Filter] -> [Property] -> IO ([UnsubStatus], [Property]) unsubscribe c@MQTTClient{..} ls props = do- (UnsubACKPkt (UnsubscribeResponse _ rsn rprop)) <- sendAndWait c DUnsubACK (\pid -> UnsubscribePkt $ UnsubscribeRequest pid (map textToBL ls) props)+ (UnsubACKPkt (UnsubscribeResponse _ rsn rprop)) <- sendAndWait c DUnsubACK (\pid -> UnsubscribePkt $ UnsubscribeRequest pid (map (textToBL . unFilter) ls) props) pure (rprop, rsn) -- | Publish a message (QoS 0).@@ -578,7 +584,7 @@ _pubQoS = q, _pubPktID = pid, _pubRetain = r,- _pubTopic = textToBL t,+ _pubTopic = textToBL (unTopic t), _pubBody = m, _pubProps = props} @@ -624,7 +630,7 @@ mkLWT t m r = T.LastWill{ T._willRetain=r, T._willQoS=QoS0,- T._willTopic = textToBL t,+ T._willTopic = textToBL (unTopic t), T._willMsg=m, T._willProps=mempty }
src/Network/MQTT/Topic.hs view
@@ -9,25 +9,64 @@ Topic and topic related utiilities. -} -{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-} module Network.MQTT.Topic (- Filter, Topic, match+ Filter, unFilter, Topic, unTopic, match,+ mkFilter, mkTopic, split ) where -import Data.Text (Text, isPrefixOf, splitOn)+import Data.String (IsString (..))+import Data.Text (Text, isPrefixOf, splitOn) +class Splittable a where+ -- | split separates a `Filter` or `Topic` into its `/`-separated components.+ split :: a -> [a]+ -- | An MQTT topic.-type Topic = Text+newtype Topic = Topic { unTopic :: Text } deriving (Show, Ord, Eq, IsString) +instance Splittable Topic where+ split (Topic t) = Topic <$> splitOn "/" t++instance Semigroup Topic where+ (Topic a) <> (Topic b) = Topic (a <> "/" <> b)++-- | mkTopic creates a topic from a text representation of a valid filter.+mkTopic :: Text -> Maybe Topic+mkTopic "" = Nothing+mkTopic t = Topic <$> validate (splitOn "/" t)+ where+ validate ("#":_) = Nothing+ validate ("+":_) = Nothing+ validate [] = Just t+ validate (_:xs) = validate xs+ -- | An MQTT topic filter.-type Filter = Text+newtype Filter = Filter { unFilter :: Text } deriving (Show, Ord, Eq, IsString) +instance Splittable Filter where+ split (Filter f) = Filter <$> splitOn "/" f++instance Semigroup Filter where+ (Filter a) <> (Filter b) = Filter (a <> "/" <> b)++-- | mkFilter creates a filter from a text representation of a valid filter.+mkFilter :: Text -> Maybe Filter+mkFilter "" = Nothing+mkFilter t = Filter <$> validate (splitOn "/" t)+ where+ validate ["#"] = Just t+ validate ("#":_) = Nothing+ validate [] = Just t+ validate (_:xs) = validate xs+ -- | match returns true iff the given pattern can be matched by the -- specified Topic as defined in the -- <http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718107 MQTT 3.1.1 specification>. match :: Filter -> Topic -> Bool-match pat top = cmp (splitOn "/" pat) (splitOn "/" top)+match (Filter pat) (Topic top) = cmp (splitOn "/" pat) (splitOn "/" top) where cmp [] [] = True
test/Spec.hs view
@@ -3,11 +3,16 @@ import Control.Monad (mapM_) import qualified Data.Attoparsec.ByteString.Lazy as A import qualified Data.ByteString.Lazy as L+import Data.String (fromString)+import qualified Data.Text as T import Data.Word (Word8) import Network.MQTT.Arbitrary import Network.MQTT.Topic import Network.MQTT.Types as MT+ import Test.QuickCheck as QC+import Test.QuickCheck.Checkers+import Test.QuickCheck.Classes import Test.Tasty import Test.Tasty.HUnit import Test.Tasty.QuickCheck as QC@@ -98,6 +103,13 @@ mapM_ (\q -> assertEqual (show q) (Just q) (qosFromInt (fromEnum q))) [QoS0 ..] assertEqual "invalid QoS" Nothing (qosFromInt 1939) +instance Arbitrary Filter where+ arbitrary = ttof <$> arbitrary+ where ttof = fromString . T.unpack . unTopic++instance EqProp Filter where (=-=) = eq+instance EqProp Topic where (=-=) = eq+ tests :: [TestTree] tests = [ localOption (QC.QuickCheckTests 10000) $ testProperty "header length rt (parser)" prop_rtLengthParser,@@ -112,6 +124,9 @@ testProperty "conn reasons" (byteRT :: ConnACKRC -> Bool), testProperty "disco reasons" (byteRT :: DiscoReason -> Bool),++ testProperties "topic semigroup" (unbatch $ semigroup (undefined :: Topic, undefined :: Int)),+ testProperties "filter semigroup" (unbatch $ semigroup (undefined :: Filter, undefined :: Int)), testGroup "topic matching" testTopicMatching, testProperty "arbitrary topic matching" prop_TopicMatching