git.iko.soy/typeable/servant
on master help sign in
streaming client actually streams
commite75a3cc37be56d3013918ef4330938b1cb0b1fda authorGershom <gershom@awakesecurity.com> dateTue 24 Oct 2017, 17:12 (9 years ago) parent9a2ac6f
7 files changed+83−35
servant-client-core/src/Servant/Client/Core.hs+1 servant-client-core/src/Servant/Client/Core/Internal/HasClient.hs+44−29 servant-client-core/src/Servant/Client/Core/Internal/Request.hs+4 servant-client-core/src/Servant/Client/Core/Internal/RunClient.hs+2 servant-client/src/Servant/Client/Internal/HttpClient.hs+12 servant/src/Servant/API.hs+1 servant/src/Servant/API/Stream.hs+19−6
servant-client-core/src/Servant/Client/Core.hs+1
@@ -43,6 +43,7 @@ module Servant.Client.Core
4343 , Response(..)
4444 , RunClient(..)
4545 , module Servant.Client.Core.Internal.BaseUrl
46+ , StreamingResponse(..)
4647
4748 -- * Writing HasClient instances
4849 -- | These functions need not be re-exported by backend libraries.
servant-client-core/src/Servant/Client/Core/Internal/HasClient.hs+44−29
@@ -4,6 +4,7 @@
44{-# LANGUAGE FlexibleInstances #-}
55{-# LANGUAGE InstanceSigs #-}
66{-# LANGUAGE MultiParamTypeClasses #-}
7+{-# LANGUAGE RankNTypes #-}
78{-# LANGUAGE OverloadedStrings #-}
89{-# LANGUAGE PolyKinds #-}
910{-# LANGUAGE ScopedTypeVariables #-}
@@ -17,9 +18,12 @@ module Servant.Client.Core.Internal.HasClient where
1718import Prelude ()
1819import Prelude.Compat
1920
21+import Control.Concurrent (newMVar, modifyMVar)
22+import Control.Monad (when)
2023import Data.Foldable (toList)
2124import qualified Data.ByteString.Lazy as BL
2225import Data.List (foldl')
26+import Data.Monoid ((<>))
2327import Data.Proxy (Proxy (Proxy))
2428import Data.Sequence (fromList)
2529import Data.String (fromString)
@@ -31,6 +35,7 @@ import Servant.API ((:<|>) ((:<|>)), (:>),
3135 BasicAuthData,
3236 BuildHeadersTo (..),
3337 BuildFromStream (..),
38+ ByteStringParser (..),
3439 Capture, CaptureAll,
3540 Description, EmptyAPI,
3641 FramingUnrender (..),
@@ -248,44 +253,54 @@ instance OVERLAPPING_
248253 , getHeadersHList = buildHeadersTo . toList $ responseHeaders response
249254 }
250255
256+data ResultStream a = ResultStream ((forall b. (IO (Maybe (Either String a)) -> IO b) -> IO b))
257+
251258instance OVERLAPPABLE_
252259 ( RunClient m, MimeUnrender ct a, ReflectMethod method,
253260 FramingUnrender framing a, BuildFromStream a (f a)
254261 ) => HasClient m (Stream method framing ct (f a)) where
255− type Client m (Stream method framing ct (f a)) = m (f a)
256− clientWithRoute _pm Proxy req = do
257− response <- runRequest req
258− { requestAccept = fromList [contentType (Proxy :: Proxy ct)]
259− , requestMethod = reflectMethod (Proxy :: Proxy method)
260− }
261− return $ decodeFramed (Proxy :: Proxy framing) (Proxy :: Proxy ct) (Proxy :: Proxy a) response
262262
263−instance OVERLAPPING_
264− ( RunClient m, BuildHeadersTo ls, MimeUnrender ct a, ReflectMethod method,
265− FramingUnrender framing a, BuildFromStream a (f a)
266− ) => HasClient m (Stream method framing ct (Headers ls (f a))) where
267− type Client m (Stream method framing ct (Headers ls (f a))) = m (Headers ls (f a))
263+ type Client m (Stream method framing ct (f a)) = m (ResultStream a)
264+
268265 clientWithRoute _pm Proxy req = do
269− response <- runRequest req
266+ sresp <- streamingRequest req
270267 { requestAccept = fromList [contentType (Proxy :: Proxy ct)]
271268 , requestMethod = reflectMethod (Proxy :: Proxy method)
272269 }
273− return Headers { getResponse = decodeFramed (Proxy :: Proxy framing) (Proxy :: Proxy ct) (Proxy :: Proxy a) response
274− , getHeadersHList = buildHeadersTo . toList $ responseHeaders response
275− }
276−
277−decodeFramed :: forall ctype strategy a b.
278− (MimeUnrender ctype a, FramingUnrender strategy a, BuildFromStream a b) =>
279− Proxy strategy -> Proxy ctype -> Proxy a -> Response -> b
280−decodeFramed framingproxy ctypeproxy typeproxy response =
281− let (body, uncons) = unrenderFrames framingproxy typeproxy (responseBody response)
282− loop b | BL.null b = []
283− | otherwise = case uncons b of
284− (Right x, r) -> case mimeUnrender ctypeproxy x :: Either String a of
285− Left err -> Left err : loop r
286− Right x' -> Right x' : loop r
287− (Left err, r) -> Left err : loop r
288− in buildFromStream $ loop body
270+ return $ ResultStream $ \k ->
271+ runStreamingResponse sresp $ \(status,_headers,_httpversion,reader) -> do
272+ when (H.statusCode status /= 200) $ error "bad status" --fixme
273+ let unrender = unrenderFrames (Proxy :: Proxy framing) (Proxy :: Proxy a)
274+ loop bs = do
275+ res <- BL.fromStrict <$> reader
276+ if BL.null res
277+ then return $ parseEOF unrender res
278+ else let sofar = (bs <> res)
279+ in case parseIncremental unrender sofar of
280+ Just x -> return x
281+ Nothing -> loop sofar
282+ (frameParser, remainder) <- loop BL.empty
283+ state <- newMVar remainder
284+ let frameLoop bs = do
285+ res <- BL.fromStrict <$> reader
286+ let addIsEmptyInfo (a, r) = (r, (a, BL.null r && BL.null res))
287+ if BL.null res
288+ then return . addIsEmptyInfo $ parseEOF frameParser res
289+ else let sofar = (bs <> res)
290+ in case parseIncremental frameParser res of
291+ Just x -> return $ addIsEmptyInfo x
292+ Nothing -> frameLoop sofar
293+
294+ go = processResult <$> modifyMVar state frameLoop
295+ processResult (Right bs,isDone) =
296+ if BL.null bs && isDone
297+ then Nothing
298+ else Just $ case mimeUnrender (Proxy :: Proxy ct) bs :: Either String a of
299+ Left err -> Left err
300+ Right x -> Right x
301+ processResult (Left err, _) = Just (Left err)
302+ k go
303+
289304
290305-- | If you use a 'Header' in one of your endpoints in your API,
291306-- the corresponding querying function will automatically take
servant-client-core/src/Servant/Client/Core/Internal/Request.hs+4
@@ -4,6 +4,7 @@
44{-# LANGUAGE DeriveGeneric #-}
55{-# LANGUAGE GeneralizedNewtypeDeriving #-}
66{-# LANGUAGE MultiParamTypeClasses #-}
7+{-# LANGUAGE RankNTypes #-}
78{-# LANGUAGE OverloadedStrings #-}
89{-# LANGUAGE ScopedTypeVariables #-}
910{-# LANGUAGE TypeFamilies #-}
@@ -15,6 +16,7 @@ import Prelude.Compat
1516
1617import Control.Monad.Catch (Exception)
1718import qualified Data.ByteString.Builder as Builder
19+import qualified Data.ByteString as BS
1820import qualified Data.ByteString.Lazy as LBS
1921import Data.Semigroup ((<>))
2022import qualified Data.Sequence as Seq
@@ -70,6 +72,8 @@ data Response = Response
7072 , responseHttpVersion :: HttpVersion
7173 } deriving (Eq, Show, Generic, Typeable)
7274
75+data StreamingResponse = StreamingResponse { runStreamingResponse :: forall a. ((Status, Seq.Seq Header, HttpVersion, IO BS.ByteString) -> IO a) -> IO a }
76+
7377-- A GET request to the top-level path
7478defaultRequest :: Request
7579defaultRequest = Request
servant-client-core/src/Servant/Client/Core/Internal/RunClient.hs+2
@@ -19,11 +19,13 @@ import Servant.API (MimeUnrender,
1919 contentTypes,
2020 mimeUnrender)
2121import Servant.Client.Core.Internal.Request (Request, Response (..),
22+ StreamingResponse (..),
2223 ServantError (..))
2324
2425class (Monad m) => RunClient m where
2526 -- | How to make a request.
2627 runRequest :: Request -> m Response
28+ streamingRequest :: Request -> m StreamingResponse
2729 throwServantError :: ServantError -> m a
2830 catchServantError :: m a -> (ServantError -> m a) -> m a
2931
servant-client/src/Servant/Client/Internal/HttpClient.hs+12
@@ -88,6 +88,7 @@ instance Alt ClientM where
8888
8989instance RunClient ClientM where
9090 runRequest = performRequest
91+ streamingRequest = performStreamingRequest
9192 throwServantError = throwError
9293 catchServantError = catchError
9394
@@ -115,6 +116,17 @@ performRequest req = do
115116 throwError $ FailureResponse ourResponse
116117 return ourResponse
117118
119+performStreamingRequest :: Request -> ClientM StreamingResponse
120+performStreamingRequest req = do
121+ m <- asks manager
122+ burl <- asks baseUrl
123+ let request = requestToClientRequest burl req
124+
125+ return $ StreamingResponse $
126+ \k -> Client.withResponse request m $
127+ \r ->
128+ k (Client.responseStatus r, fromList $ Client.responseHeaders r, Client.responseVersion r, Client.responseBody r)
129+
118130clientResponseToReponse :: Client.Response BSL.ByteString -> Response
119131clientResponseToReponse r = Response
120132 { responseStatusCode = Client.responseStatus r
servant/src/Servant/API.hs+1
@@ -85,6 +85,7 @@ import Servant.API.QueryParam (QueryFlag, QueryParam,
8585import Servant.API.Raw (Raw)
8686import Servant.API.Stream (Stream, StreamGenerator (..),
8787 ToStreamGenerator (..), BuildFromStream (..),
88+ ByteStringParser (..),
8889 FramingRender (..), BoundaryStrategy (..),
8990 FramingUnrender (..),
9091 NewlineFraming)
servant/src/Servant/API/Stream.hs+19−6
@@ -11,13 +11,11 @@
1111
1212module Servant.API.Stream where
1313
14−import Control.Arrow ((***), first)
1514import Data.ByteString.Lazy (ByteString, empty)
1615import qualified Data.ByteString.Lazy.Char8 as LB
1716import Data.Proxy (Proxy)
1817import Data.Typeable (Typeable)
1918import GHC.Generics (Generic)
20−import Text.Read (readMaybe)
2119import Network.HTTP.Types.Method (StdMethod (..))
2220
2321-- | A Stream endpoint for a given method emits a stream of encoded values at a given Content-Type, delimited by a framing strategy. Steam endpoints always return response code 200 on success. Type synonyms are provided for standard methods.
@@ -57,10 +55,16 @@ data BoundaryStrategy = BoundaryStrategyBracket (ByteString -> (ByteString,ByteS
5755 | BoundaryStrategyIntersperse ByteString
5856 | BoundaryStrategyGeneral (ByteString -> ByteString)
5957
60−-- | The FramingUnrender class provides the logic for parsing a framing strategy. Given a ByteString, it strips the header, and returns a tuple of the remainder along with a step function that can progressively "uncons" elements from this remainder. The error state is presented per-frame so that protocols that can resume after errors are able to do so. Termination of the unrendering is indicated by return of an empty reminder.
58+-- | A type of parser that can never fail, and has different parsing strategies (incremental, or EOF) depending if more input can be sent. The incremental parser should return `Nothing` if it would like to be sent a longer ByteString. If it returns a value, it also returns the remainder following that value.
59+data ByteStringParser a = ByteStringParser {
60+ parseIncremental :: ByteString -> Maybe (a, ByteString),
61+ parseEOF :: ByteString -> (a, ByteString)
62+}
6163
64+-- | The FramingUnrender class provides the logic for parsing a framing strategy. The outer @ByteStringParser@ strips the header from a stream of bytes, and yields a parser that can handle the remainder, stepwise. Each frame may be a ByteString, or a String indicating the error state for that frame. Such states are per-frame, so that protocols that can resume after errors are able to do so. Eventually this returns an empty ByteString to indicate termination.
6265class FramingUnrender strategy a where
63− unrenderFrames :: Proxy strategy -> Proxy a -> ByteString -> (ByteString, ByteString -> (Either String ByteString, ByteString))
66+ unrenderFrames :: Proxy strategy -> Proxy a -> ByteStringParser (ByteStringParser (Either String ByteString))
67+
6468
6569-- | A simple framing strategy that has no header or termination, and inserts a newline character between each frame.
6670-- This assumes that it is used with a Content-Type that encodes without newlines (e.g. JSON).
@@ -72,8 +76,14 @@ instance FramingRender NewlineFraming a where
7276 trailer _ _ = empty
7377
7478instance FramingUnrender NewlineFraming a where
75− unrenderFrames _ _ = (, (Right *** LB.drop 1) . LB.break (== '\n'))
76−
79+ unrenderFrames _ _ = ByteStringParser (Just . (go,)) (go,)
80+ where go = ByteStringParser
81+ (\x -> case LB.break (== '\n') x of
82+ (h,r) -> if not (LB.null r) then Just (Right h, LB.drop 1 r) else Nothing
83+ )
84+ (\x -> case LB.break (== '\n') x of
85+ (h,r) -> (Right h, LB.drop 1 r)
86+ )
7787-- | The netstring framing strategy as defined by djb: <http://cr.yp.to/proto/netstrings.txt>
7888data NetstringFraming
7989
@@ -82,9 +92,12 @@ instance FramingRender NetstringFraming a where
8292 boundary _ _ = BoundaryStrategyBracket $ \b -> (LB.pack . show . LB.length $ b, "")
8393 trailer _ _ = empty
8494
95+{-
96+
8597instance FramingUnrender NetstringFraming a where
8698 unrenderFrames _ _ = (, \b -> let (i,r) = LB.break (==':') b
8799 in case readMaybe (LB.unpack i) of
88100 Just len -> first Right $ LB.splitAt len . LB.drop 1 $ r
89101 Nothing -> (Left ("Bad netstring frame, couldn't parse value as integer value: " ++ LB.unpack i), LB.drop 1 . LB.dropWhile (/= ',') $ r)
90102 )
103+-}