socketio 0.1.2 → 0.1.3
raw patch · 12 files changed
+239/−209 lines, 12 filesdep +attoparsecdep +blaze-builderdep +conduitdep −parsec
Dependencies added: attoparsec, blaze-builder, conduit, conduit-extra
Dependencies removed: parsec
Files
- Web/SocketIO/Connection.hs +25/−10
- Web/SocketIO/Event.hs +3/−2
- Web/SocketIO/Log.hs +6/−1
- Web/SocketIO/Protocol.hs +118/−125
- Web/SocketIO/Request.hs +56/−36
- Web/SocketIO/Server.hs +6/−18
- Web/SocketIO/Session.hs +1/−1
- Web/SocketIO/Types/Base.hs +3/−2
- Web/SocketIO/Types/Event.hs +4/−7
- Web/SocketIO/Types/Request.hs +3/−3
- Web/SocketIO/Types/String.hs +5/−1
- socketio.cabal +9/−3
Web/SocketIO/Connection.hs view
@@ -84,9 +84,9 @@ retrieveSession (Disconnect sessionID) = do result <- lookupSession sessionID return (Disconnect sessionID, split result)-retrieveSession (Emit sessionID e) = do+retrieveSession (Request sessionID e) = do result <- lookupSession sessionID- return (Emit sessionID e, split result)+ return (Request sessionID e, split result) -------------------------------------------------------------------------------- -- | Big time@@ -146,19 +146,34 @@ logWithSessionID Warn sessionID "[Request] Disconnect: Session not found" return MsgNoop -handleConnection (Emit sessionID _, Just (session, Connecting)) = do+handleConnection (Request sessionID _, Just (session, Connecting)) = do extendTimeout session- logWithSessionID Warn sessionID "[Request] Emit: Session still connecting, not ACKed" + logWithSessionID Warn sessionID "[Request] Request: Session still connecting, not ACKed" return $ MsgError NoEndpoint NoData -handleConnection (Emit sessionID event@(Event eventName (Payload payloads)), Just (session, Connected)) = do- logWithSessionID Debug sessionID $ "[Request] Emit: " <> serialize eventName <> " " <> serialize payloads++handleConnection (Request sessionID (MsgEvent _ _ event@(Event eventName (Payload payloads))), Just (session, Connected)) = do+ logWithSessionID Debug sessionID $ "[Request] Request: MsgEvent " <> serialize eventName <> " " <> serialize payloads runSession (SessionEmit event) session -handleConnection (Emit sessionID NoEvent, Just (_, Connected)) = do- logWithSessionID Warn sessionID "[Request] Emit: event malformed"+handleConnection (Request sessionID (MsgEvent _ _ NoEvent), Just (_, Connected)) = do+ logWithSessionID Warn sessionID "[Request] Request: MsgEvent malformed" return $ MsgError NoEndpoint NoData -handleConnection (Emit sessionID _, Nothing) = do- logWithSessionID Warn sessionID "[Request] Emit: Session not found" +handleConnection (Request sessionID message, Just (_, Connected)) = do+ logWithSessionID Debug sessionID $ "[Request] Request: " <> serialize message+ return MsgNoop+ --runSession (SessionEmit event) session+++--handleConnection (Request sessionID event@(Event eventName (Payload payloads)), Just (session, Connected)) = do+-- logWithSessionID Debug sessionID $ "[Request] Request: " <> serialize eventName <> " " <> serialize payloads+-- runSession (SessionEmit event) session++--handleConnection (Request sessionID NoEvent, Just (_, Connected)) = do+-- logWithSessionID Warn sessionID "[Request] Request: malformed"+-- return $ MsgError NoEndpoint NoData++handleConnection (Request sessionID _, Nothing) = do+ logWithSessionID Warn sessionID "[Request] Request: Session not found" return $ MsgError NoEndpoint NoData
Web/SocketIO/Event.hs view
@@ -10,12 +10,13 @@ -------------------------------------------------------------------------------- import Control.Applicative ((<$>)) import Control.Monad.Reader+import qualified Data.Aeson as Aeson import qualified Data.ByteString.Lazy as BL {-# DEPRECATED reply "use msg instead" #-} -------------------------------------------------------------------------------- -- | This function is deprecated; use 'msg' instead-reply :: CallbackM [Text]+reply :: CallbackM [Aeson.Value] reply = do Payload p <- callbackEnvPayload <$> ask return p@@ -29,7 +30,7 @@ -- liftIO $ print payload -- emit "echo" payload -- @-msg :: CallbackM [Text]+msg :: CallbackM [Aeson.Value] msg = do Payload p <- callbackEnvPayload <$> ask return p
Web/SocketIO/Log.hs view
@@ -2,7 +2,7 @@ -- | Exports some logging utilities. {-# LANGUAGE OverloadedStrings #-} -module Web.SocketIO.Log ((<>), logRaw, logWithSession, logWithSessionID) where+module Web.SocketIO.Log ((<>), logRaw, logWithSession, logWithSessionID, showStatusBar) where -------------------------------------------------------------------------------- import Web.SocketIO.Types@@ -36,3 +36,8 @@ logWithSession logType message = do Session sessionID _ _ _ _ <- getSession logRaw logType $ fromByteString (sessionID <> " " <> message)++--------------------------------------------------------------------------------+-- | Show status bar+showStatusBar :: IO ()+showStatusBar = return ()
Web/SocketIO/Protocol.hs view
@@ -2,7 +2,11 @@ -- | Socket.IO Protocol 1.0 {-# LANGUAGE OverloadedStrings #-} -module Web.SocketIO.Protocol (parseFramedMessage, parsePath) where+module Web.SocketIO.Protocol + ( demultiplexMessage+ , parseFramedMessage+ , parsePath+ ) where -------------------------------------------------------------------------------- import Web.SocketIO.Types@@ -10,146 +14,135 @@ -------------------------------------------------------------------------------- import Control.Applicative ((<$>), (<*>)) import Data.Aeson-import qualified Data.ByteString.Lazy as BL-import Text.Parsec-import Text.Parsec.ByteString.Lazy------------------------------------------------------------------------------------- | Parse raw ByteString to Messages-parseFramedMessage :: BL.ByteString -> Framed Message-parseFramedMessage input = if isSingleton- then Framed $ [parseMessage' input]- else Framed $ map parseMessage' splitted- where splitted = split input- parseMessage' x = case parse parseMessage "" x of- Left _ -> MsgNoop- Right a -> a- isSingleton = not (BL.null input) && BL.head input /= 239------------------------------------------------------------------------------------ | Split raw ByteString with U+FFFD as delimiter-split :: BL.ByteString -> [BL.ByteString]-split str = map (BL.drop 2) . skipOddIndexed True . filter isDelimiter . BL.split 239 $ str- where isDelimiter x = not (BL.null x)- && (BL.head x == 191)- && (BL.head (BL.tail x) == 189)- skipOddIndexed _ [] = []- skipOddIndexed True (_:xs) = skipOddIndexed False xs- skipOddIndexed False (x:xs) = x : skipOddIndexed True xs------------------------------------------------------------------------------------- | Message, not framed-parseMessage :: Parser Message-parseMessage = do- n <- digit- case n of- '0' -> (parseID >> parseEndpoint >>= return . MsgDisconnect)- <|> ( return $ MsgDisconnect NoEndpoint)- '1' -> (parseID >> parseEndpoint >>= return . MsgConnect)- <|> ( return $ MsgConnect NoEndpoint)- '2' -> return MsgHeartbeat- '3' -> parseRegularMessage Msg- '4' -> parseRegularMessage MsgJSON- '5' -> MsgEvent <$> parseID - <*> parseEndpoint - <*> parseEvent- '6' -> try (do - string ":::"- n' <- read <$> number- char '+'- d <- fromString <$> text- return $ MsgACK (ID n') (Data d)- ) <|> (do- string ":::"- n' <- read <$> number- return $ MsgACK (ID n') NoData- )- '7' -> colon >> MsgError <$> parseEndpoint <*> parseData- '8' -> return $ MsgNoop- _ -> return $ MsgNoop- where parseRegularMessage ctr = ctr <$> parseID - <*> parseEndpoint - <*> parseData-----------------------------------------------------------------------------------endpoint :: Parser String-endpoint = many1 $ satisfy (/= ':')-----------------------------------------------------------------------------------number :: Parser String-number = many1 digit+import qualified Data.ByteString as B+import Data.Conduit+import Data.Conduit.Attoparsec (conduitParserEither)+import Data.Attoparsec.ByteString.Lazy+import Data.Attoparsec.ByteString.Char8 (digit, decimal)+import Prelude hiding (take, takeWhile) ---------------------------------------------------------------------------------colon :: Parser Char-colon = char ':'+-- | Demultiplexing messages+demultiplexMessage :: Conduit ByteString IO Message+demultiplexMessage = do+ conduitParserEither framedMessageParser =$= awaitForever go+ where go (Left s) = error $ show s+ go (Right (_, p)) = mapM yield p ----------------------------------------------------------------------------------parseID :: Parser ID-parseID = try (colon >> number >>= plus >>= return . IDPlus . read)- <|> try (colon >> number >>= return . ID . read)- <|> (colon >> return NoID)- where plus n = char '+' >> return n +----------------------------------------------------------------------------------+---- | Using U+FFFD as delimiter+frameParser :: Parser a -> Parser a+frameParser parser = do+ string "�"+ len <- decimal+ string "�"+ x <- take len+ case parseOnly parser x of+ Left e -> error e+ Right r -> return r ---------------------------------------------------------------------------------parseEndpoint :: Parser Endpoint-parseEndpoint = try (colon >> fromString <$> endpoint >>= return . Endpoint)- <|> (colon >> return NoEndpoint)+-- | Message, framed with List+framedMessageParser :: Parser [Message]+framedMessageParser = choice [many1 (frameParser messageParser), many' messageParser] ---------------------------------------------------------------------------------parseData :: Parser Data-parseData = try (colon >> text >>= return . Data . fromString)- <|> (colon >> return NoData)+-- | Wrapped for testing+parseFramedMessage :: ByteString -> Framed Message+parseFramedMessage input = case parseOnly framedMessageParser input of+ Left e -> error e+ Right r -> Framed r ---------------------------------------------------------------------------------parseEvent :: Parser Event-parseEvent = try (do- colon- t <- text- case decode (fromString t) of- Just e -> return e- Nothing -> return NoEvent- )- <|> (colon >> return NoEvent)-+-- | Message, not framed+messageParser :: Parser Message+messageParser = do+ n <- digit+ case n of+ '0' -> choice+ [ idParser >> endpointParser >>= return . MsgDisconnect+ , return $ MsgDisconnect NoEndpoint+ ]+ '1' -> choice+ [ idParser >> endpointParser >>= return . MsgConnect+ , return $ MsgConnect NoEndpoint + ]+ '2' -> return MsgHeartbeat+ '3' -> Msg <$> idParser + <*> endpointParser + <*> dataParser+ '4' -> MsgJSON <$> idParser + <*> endpointParser + <*> dataParser+ '5' -> MsgEvent <$> idParser + <*> endpointParser + <*> eventParser+ '6' -> choice+ [ do string ":::"+ d <- decimal+ string "+"+ x <- takeWhile (const True)+ + return $ MsgACK (ID d) (if B.null x then NoData else Data x)+ + , do string ":::"+ d <- decimal+ return $ MsgACK (ID d) NoData+ ]+ '7' -> string ":" >> MsgError <$> endpointParser <*> dataParser+ '8' -> return MsgNoop+ _ -> return MsgNoop ------------------------------------------------------------------------------------ | Slashes as delimiters-textWithoutSlash :: Parser String-textWithoutSlash = many1 $ satisfy (/= '/')+idParser :: Parser ID+idParser = choice+ [ string ":" >> decimal >>= plus >>= return . IDPlus+ , string ":" >> decimal >>= return . ID+ , string ":" >> return NoID+ ]+ where plus n = string "+" >> return n -slash :: Parser Char-slash = char '/'+endpointParser :: Parser Endpoint+endpointParser = do+ string ":"+ option NoEndpoint (takeWhile1 (/= 58) >>= return . Endpoint) ------------------------------------------------------------------------------------ | Non-empty text-text :: Parser String-text = many1 anyChar+dataParser :: Parser Data+dataParser = do+ string ":"+ option NoData (takeWhile1 (/= 58) >>= return . Data) ---------------------------------------------------------------------------------parseTransport :: Parser Transport-parseTransport = try (string "websocket" >> return WebSocket) - <|> (string "xhr-polling" >> return XHRPolling)- <|> (string "unknown" >> return NoTransport)- <|> return NoTransport+eventParser :: Parser Event+eventParser = do+ string ":"+ t <- takeWhile (const True)+ case decode (serialize t) of+ Just e -> return e+ Nothing -> return NoEvent --------------------------------------------------------------------------------- --- | With wrapper+------------------------------------------------------------------------------ +-- | Parse given HTTP request parsePath :: ByteString -> Path-parsePath path = case parse parsePath' "" (fromByteString path) of+parsePath p = case parseOnly pathParser p of Left _ -> WithoutSession "" "" Right x -> x ------------------------------------------------------------------------------------ | Raw Parsec combinator, without wrapper-parsePath' :: Parser Path-parsePath' = do- slash- namespace <- fromString <$> textWithoutSlash- slash- protocol <- fromString <$> textWithoutSlash- slash- try (do- transport <- parseTransport- slash- sessionID <- fromString <$> textWithoutSlash+pathParser :: Parser Path+pathParser = do+ string "/"+ namespace <- takeTill (== 47) -- 0x47: slash+ take 1 -- slip the second slash+ protocol <- takeTill (== 47)+ take 1 -- slip the third slash+ option (WithoutSession namespace protocol) $ do+ transport <- transportParser+ string "/"+ sessionID <- takeTill (== 47) return $ WithSession namespace protocol transport sessionID- ) <|> (return $ WithoutSession namespace protocol)++transportParser :: Parser Transport+transportParser = choice+ [ string "websocket" >> return WebSocket+ , string "xhr-polling" >> return XHRPolling+ , string "unknown" >> return NoTransport+ , skipWhile (/= 47) >> return NoTransport+ ]
Web/SocketIO/Request.hs view
@@ -1,54 +1,74 @@ ----------------------------------------------------------------------------------- | Converts HTTP requests to Socket.IO requests+-- | Converts HTTP requests to Socket.IO requests and run them {-# LANGUAGE OverloadedStrings #-}-module Web.SocketIO.Request (parseHTTPRequest) where +module Web.SocketIO.Request (sourceHTTPRequest, runRequest) where+ -------------------------------------------------------------------------------- import Web.SocketIO.Types import Web.SocketIO.Protocol ---------------------------------------------------------------------------------import Control.Applicative ((<$>)) -import qualified Network.Wai as Wai-import Network.HTTP.Types (Method)+import Blaze.ByteString.Builder (Builder)+import qualified Blaze.ByteString.Builder as Builder+import qualified Data.ByteString as B+import Data.Conduit+import qualified Data.Conduit.List as CL+import qualified Network.Wai as Wai ----------------------------------------------------------------------------------- | Information of a HTTP reqeust we need-type RequestInfo = (Method, Path, Framed Message)+-- | Run!+runRequest :: (Request -> IO Message) -> Conduit Request IO (Flush Builder)+runRequest runner = CL.mapM runner =$= serializeMessage =$= toFlushBuilder ----------------------------------------------------------------------------------- | Extracts from HTTP reqeusts-retrieveRequestInfo :: Wai.Request -> IO RequestInfo-retrieveRequestInfo request = do-- framedMessages <- parseHTTPBody request-+-- | Extracts HTTP requests+sourceHTTPRequest :: Wai.Request -> Source IO Request+sourceHTTPRequest request = do let path = parsePath (Wai.rawPathInfo request)+ let method = Wai.requestMethod request - return - ( Wai.requestMethod request- , path - , framedMessages- )+ case (method, path) of+ ("GET", (WithoutSession _ _)) -> yield Handshake+ ("GET", (WithSession _ _ _ sessionID)) -> yield (Connect sessionID)+ ("POST", (WithSession _ _ _ sessionID)) -> Wai.requestBody request $= demultiplexMessage =$= awaitForever (yield . Request sessionID)+ (_, (WithSession _ _ _ sessionID)) -> yield (Disconnect sessionID)+ _ -> error "error handling http request" ----------------------------------------------------------------------------------- | Converts to SocketIO request-processRequestInfo :: RequestInfo -> [Request]-processRequestInfo ("GET" , (WithoutSession _ _) , _ ) = [Handshake]-processRequestInfo ("GET" , (WithSession _ _ _ sessionID), _ ) = [Connect sessionID]-processRequestInfo ("POST", (WithSession _ _ _ sessionID), Framed messages) = requests- where requests = foldr extractEvent [] messages- extractEvent (MsgEvent _ _ event) acc = Emit sessionID event : acc- extractEvent _ acc = acc-processRequestInfo (_ , (WithSession _ _ _ sessionID), _ ) = [Disconnect sessionID]-processRequestInfo _ = error "error parsing http request"- ------------------------------------------------------------------------------------ | The request part-parseHTTPRequest :: Wai.Request -> IO [Request]-parseHTTPRequest request = fmap processRequestInfo (retrieveRequestInfo request)+-- | Serialize Messages, frame when necessary.+serializeMessage :: Conduit Message IO ByteString+serializeMessage = toByteString 0+ where toByteString :: Int -> Conduit Message IO ByteString+ toByteString i = do+ m <- await+ n <- await+ case (m, n) of+ -- []+ (Nothing, Nothing) -> yield (serialize (Framed [] :: Framed Message))+ -- [m'], singleton+ (Just m', Nothing) -> if i == 0+ then yield (serialize m') -- true singleton+ else yield (frame m') -- just a recursion base case + -- WTF+ (Nothing, Just _ ) -> return ()+ -- [m', n'], frame m', leftover n'+ (Just m', Just n') -> do+ yield (frame m')+ leftover n'+ toByteString (i + 1)+ + frame b = "�" <> serialize size <> "�" <> b'+ where b' = serialize b+ size = B.length b' ----------------------------------------------------------------------------------- | The message part-parseHTTPBody :: Wai.Request -> IO (Framed Message)-parseHTTPBody req = parseFramedMessage <$> Wai.lazyRequestBody req+-- | Convert Framed Message to Flush Builder so that `Wai.responseSource` can consume it+toFlushBuilder :: Conduit ByteString IO (Flush Builder)+toFlushBuilder = do + b <- await+ case b of+ Just b' -> do+ yield $ Chunk (Builder.fromByteString b')+ toFlushBuilder+ Nothing -> yield $ Flush
Web/SocketIO/Server.hs view
@@ -16,7 +16,8 @@ -------------------------------------------------------------------------------- import Control.Monad.Trans (liftIO)-import Network.HTTP.Types (Status, status200, status403)+import Data.Conduit+import Network.HTTP.Types (status200) import Network.HTTP.Types.Header (ResponseHeaders) import qualified Network.Wai as Wai import qualified Network.Wai.Handler.Warp as Warp@@ -45,6 +46,7 @@ -- run it with Warp Warp.run port (httpApp vorspann (runConnection env)) + -------------------------------------------------------------------------------- -- | Wrapped as a HTTP app httpApp :: ResponseHeaders -> (Request -> IO Message) -> Wai.Application@@ -53,9 +55,10 @@ let origin = lookupOrigin httpRequest let headerFields' = insertOrigin headerFields origin - reqs <- parseHTTPRequest httpRequest- mapM runConnection' reqs >>= waiResponse headerFields' + let sourceBody = sourceHTTPRequest httpRequest $= runRequest runConnection' + return $ Wai.responseSource status200 headerFields' sourceBody+ where lookupOrigin req = case lookup "Origin" (Wai.requestHeaders req) of Just origin -> origin Nothing -> "*"@@ -100,18 +103,3 @@ , heartbeatInterval = 25 , pollingDuration = 20 }------------------------------------------------------------------------------------- | Make Wai response-waiResponse :: Monad m => ResponseHeaders -> [Message] -> m Wai.Response-waiResponse vorspann messages = do- return . Wai.responseLBS status vorspann . serialize . Framed $ messages- where status = if status403 `elem` (map toHTTPStatus messages)- then status403- else status200------------------------------------------------------------------------------------- | Maps SocketIO respond message to HTTP status code-toHTTPStatus :: Message -> Status-toHTTPStatus (MsgError _ _) = status403-toHTTPStatus _ = status200
Web/SocketIO/Session.hs view
@@ -30,10 +30,10 @@ heartbeatTimeout' (closeTimeout configuration) (transports configuration)- handleSession SessionConnect = do logWithSession Info $ "Connected"+ triggerEvent (Event "connection" (Payload [])) return $ MsgConnect NoEndpoint handleSession SessionPolling = do
Web/SocketIO/Types/Base.hs view
@@ -19,6 +19,7 @@ import Control.Monad.Reader import Control.Monad.Writer import Control.Monad.Base+import qualified Data.Aeson as Aeson import qualified Data.HashMap.Strict as H import Data.IORef.Lifted import Network.HTTP.Types.Header (ResponseHeaders)@@ -153,7 +154,7 @@ -- `emit` \"launch\" [\"missile\", \"nuke\"] -- @ emit :: EventName -- ^ name of event to trigger- -> [Text] -- ^ payload to carry with+ -> [Aeson.Value] -- ^ payload to carry with -> m () -- | Sends a message to everybody except for the socket that starts it.@@ -162,7 +163,7 @@ -- `broadcast` \"hide\" [\"nukes coming!\"] -- @ broadcast :: EventName -- ^ name of event to trigger- -> [Text] -- ^ payload to carry with+ -> [Aeson.Value] -- ^ payload to carry with -> m () -- |
Web/SocketIO/Types/Event.hs view
@@ -18,10 +18,7 @@ ---------------------------------------------------------------------------------- import Control.Applicative-import Data.Aeson -import Data.Aeson.Encode (encodeToTextBuilder)-import Data.List (intersperse) -import Data.Text.Internal.Builder (toLazyText)+import Data.Aeson as Aeson import qualified Data.Text.Lazy as TL import Data.Vector (toList) --------------------------------------------------------------------------------@@ -30,10 +27,10 @@ -------------------------------------------------------------------------------- -- | Payload carried by an Event-data Payload = Payload [Text] deriving (Eq, Show)+data Payload = Payload [Aeson.Value] deriving (Eq, Show) instance Serializable Payload where- serialize (Payload payload) = serialize $ '[' `TL.cons` (TL.concat $ intersperse "," payload) `TL.snoc` ']'+ serialize (Payload payload) = serialize $ Aeson.encode payload -------------------------------------------------------------------------------- -- | Event@@ -52,7 +49,7 @@ (toArgumentList <$> v .:? "args") where toArgumentList :: Maybe Value -> Payload toArgumentList Nothing = Payload []- toArgumentList (Just (Array a)) = Payload $ filter (/= "null") . map (toLazyText . encodeToTextBuilder) . toList $ a+ toArgumentList (Just (Array a)) = Payload $ filter (/= Aeson.Null) . toList $ a toArgumentList _ = Payload [] parseJSON _ = return NoEvent
Web/SocketIO/Types/Request.hs view
@@ -40,11 +40,11 @@ <> "/" ----------------------------------------------------------------------------------- | Incoming HTTP request+-- | Incoming request data Request = Handshake | Disconnect SessionID | Connect SessionID - | Emit SessionID Event+ | Request SessionID Message deriving (Show) --------------------------------------------------------------------------------@@ -56,7 +56,7 @@ serialize (Framed [message]) = serialize message serialize (Framed messages) = mconcat $ map frame messages where frame message = let serialized = serialize message - in "�" <> serialize size <> "�" <> serialized+ in "�" <> serialize size <> "�" <> serialized where size = B.length (serialize message) --------------------------------------------------------------------------------
Web/SocketIO/Types/String.hs view
@@ -19,6 +19,7 @@ --------------------------------------------------------------------------------+import qualified Data.Aeson as Aeson import qualified Data.String as S import qualified Data.Text as T import qualified Data.Text.Encoding as TE@@ -155,6 +156,9 @@ , Show a) => a -> s serialize = S.fromString . show +instance Serializable Aeson.Value where+ serialize = fromLazyByteString . Aeson.encode+ instance Serializable T.Text where serialize = fromText @@ -163,7 +167,7 @@ instance Serializable ByteString where serialize = fromByteString- + instance Serializable BL.ByteString where serialize = fromLazyByteString
socketio.cabal view
@@ -2,7 +2,7 @@ -- documentation, see http://haskell.org/cabal/users-guide/ name: socketio-version: 0.1.2+version: 0.1.3 synopsis: Socket.IO server description: Socket.IO for Haskell folks.@@ -69,8 +69,11 @@ build-depends: base >=4.6 && <5.0 , text >=1.1 && <2.0 , bytestring >=0.10 && <1.0+ , blaze-builder >=0.3 && <1.0 , aeson >=0.7 && <1.0- , parsec >=3.1 && <4.0+ , conduit >=1.1 && <2.0+ , conduit-extra >=1.1 && <2.0+ , attoparsec >=0.11 && <1.0 , ansi-terminal >=0.6 && <1.0 , unordered-containers >=0.2 && <1.0 , random >=1.0 && <2.0@@ -99,8 +102,11 @@ build-depends: base >=4.6 && <5.0 , text >=1.1 && <2.0 , bytestring >=0.10 && <1.0+ , blaze-builder >=0.3 && <1.0 , aeson >=0.7 && <1.0- , parsec >=3.1 && <4.0+ , conduit >=1.1 && <2.0+ , conduit-extra >=1.1 && <2.0+ , attoparsec >=0.11 && <1.0 , ansi-terminal >=0.6 && <1.0 , unordered-containers >=0.2 && <1.0 , random >=1.0 && <2.0