on master help sign in
Merge pull request #988 from phadej/list-is-pure-stream
ae05ef5Oleg Grenrus
servant-client-core/servant-client-core.cabal+1
@@ -52,6 +52,7 @@ library
52 , bytestring >= 0.10.4.0 && < 0.11
52 , bytestring >= 0.10.4.0 && < 0.11
53 , containers >= 0.5.5.1 && < 0.6
53 , containers >= 0.5.5.1 && < 0.6
54 , text >= 1.2.3.0 && < 1.3
54 , text >= 1.2.3.0 && < 1.3
55+ , transformers >= 0.3.0.0 && < 0.6
55
56
56 if !impl(ghc >= 8.0)
57 if !impl(ghc >= 8.0)
57 build-depends:
58 build-depends:
servant-client-core/src/Servant/Client/Core/Internal/HasClient.hs+7−6
@@ -21,6 +21,7 @@ import Prelude.Compat
21import Control.Concurrent (newMVar, modifyMVar)
21import Control.Concurrent (newMVar, modifyMVar)
22import Data.Foldable (toList)
22import Data.Foldable (toList)
23import qualified Data.ByteString.Lazy as BL
23import qualified Data.ByteString.Lazy as BL
24+import Control.Monad.IO.Class (MonadIO (..))
24import Data.List (foldl')
25import Data.List (foldl')
25import Data.Proxy (Proxy (Proxy))
26import Data.Proxy (Proxy (Proxy))
26import Data.Semigroup ((<>))
27import Data.Semigroup ((<>))
@@ -33,7 +34,7 @@ import Servant.API ((:<|>) ((:<|>)), (:>),
33 AuthProtect, BasicAuth,
34 AuthProtect, BasicAuth,
34 BasicAuthData,
35 BasicAuthData,
35 BuildHeadersTo (..),
36 BuildHeadersTo (..),
36− BuildFromStream (..),
37+ FromResultStream (..),
37 ByteStringParser (..),
38 ByteStringParser (..),
38 Capture', CaptureAll,
39 Capture', CaptureAll,
39 Description, EmptyAPI,
40 Description, EmptyAPI,
@@ -283,18 +284,18 @@ instance OVERLAPPING_
283 hoistClientMonad _ _ f ma = f ma
284 hoistClientMonad _ _ f ma = f ma
284
285
285instance OVERLAPPABLE_
286instance OVERLAPPABLE_
286− ( RunClient m, MimeUnrender ct a, ReflectMethod method,
287+ ( RunClient m, MonadIO m, MimeUnrender ct a, ReflectMethod method,
287− FramingUnrender framing a, BuildFromStream a (f a)
288+ FramingUnrender framing a, FromResultStream a b
288− ) => HasClient m (Stream method status framing ct (f a)) where
289+ ) => HasClient m (Stream method status framing ct b) where
289
290
290− type Client m (Stream method status framing ct (f a)) = m (f a)
291+ type Client m (Stream method status framing ct b) = m b
291
292
292 clientWithRoute _pm Proxy req = do
293 clientWithRoute _pm Proxy req = do
293 sresp <- streamingRequest req
294 sresp <- streamingRequest req
294 { requestAccept = fromList [contentType (Proxy :: Proxy ct)]
295 { requestAccept = fromList [contentType (Proxy :: Proxy ct)]
295 , requestMethod = reflectMethod (Proxy :: Proxy method)
296 , requestMethod = reflectMethod (Proxy :: Proxy method)
296 }
297 }
297− return . buildFromStream $ ResultStream $ \k ->
298+ liftIO $ fromResultStream $ ResultStream $ \k ->
298 runStreamingResponse sresp $ \gres -> do
299 runStreamingResponse sresp $ \gres -> do
299 let reader = responseBody gres
300 let reader = responseBody gres
300 let unrender = unrenderFrames (Proxy :: Proxy framing) (Proxy :: Proxy a)
301 let unrender = unrenderFrames (Proxy :: Proxy framing) (Proxy :: Proxy a)
servant-client/test/Servant/StreamSpec.hs+4−4
@@ -107,12 +107,12 @@ manager' = unsafePerformIO $ C.newManager C.defaultManagerSettings
107runClient :: ClientM a -> BaseUrl -> IO (Either ServantError a)
107runClient :: ClientM a -> BaseUrl -> IO (Either ServantError a)
108runClient x baseUrl' = runClientM x (mkClientEnv manager' baseUrl')
108runClient x baseUrl' = runClientM x (mkClientEnv manager' baseUrl')
109
109
110−runResultStream :: ResultStream a
110+testRunResultStream :: ResultStream a
111 -> IO ( Maybe (Either String a)
111 -> IO ( Maybe (Either String a)
112 , Maybe (Either String a)
112 , Maybe (Either String a)
113 , Maybe (Either String a)
113 , Maybe (Either String a)
114 , Maybe (Either String a))
114 , Maybe (Either String a))
115−runResultStream (ResultStream k)
115+testRunResultStream (ResultStream k)
116 = k $ \act -> (,,,) <$> act <*> act <*> act <*> act
116 = k $ \act -> (,,,) <$> act <*> act <*> act <*> act
117
117
118streamSpec :: Spec
118streamSpec :: Spec
@@ -122,13 +122,13 @@ streamSpec = beforeAll (CS.startWaiApp server) $ afterAll CS.endWaiApp $ do
122 Right res <- runClient getGetNL baseUrl
122 Right res <- runClient getGetNL baseUrl
123 let jra = Just (Right alice)
123 let jra = Just (Right alice)
124 jrb = Just (Right bob)
124 jrb = Just (Right bob)
125− runResultStream res `shouldReturn` (jra, jrb, jra, Nothing)
125+ testRunResultStream res `shouldReturn` (jra, jrb, jra, Nothing)
126
126
127 it "works with Servant.API.StreamGet.Netstring" $ \(_, baseUrl) -> do
127 it "works with Servant.API.StreamGet.Netstring" $ \(_, baseUrl) -> do
128 Right res <- runClient getGetNS baseUrl
128 Right res <- runClient getGetNS baseUrl
129 let jra = Just (Right alice)
129 let jra = Just (Right alice)
130 jrb = Just (Right bob)
130 jrb = Just (Right bob)
131− runResultStream res `shouldReturn` (jra, jrb, jra, Nothing)
131+ testRunResultStream res `shouldReturn` (jra, jrb, jra, Nothing)
132
132
133 it "streams in constant memory" $ \(_, baseUrl) -> do
133 it "streams in constant memory" $ \(_, baseUrl) -> do
134 Right (ResultStream res) <- runClient getGetALot baseUrl
134 Right (ResultStream res) <- runClient getGetALot baseUrl
servant-docs/src/Servant/Docs/Internal.hs+18
@@ -840,6 +840,24 @@ instance OVERLAPPABLE_
840 status = fromInteger $ natVal (Proxy :: Proxy status)
840 status = fromInteger $ natVal (Proxy :: Proxy status)
841 p = Proxy :: Proxy a
841 p = Proxy :: Proxy a
842
842
843+-- | TODO: mention the endpoint is streaming, its framing strategy
844+--
845+-- Also there are no samples.
846+instance OVERLAPPABLE_
847+ (MimeRender ct a, KnownNat status
848+ , ReflectMethod method)
849+ => HasDocs (Stream method status framing ct a) where
850+ docsFor Proxy (endpoint, action) DocOptions{..} =
851+ single endpoint' action'
852+
853+ where endpoint' = endpoint & method .~ method'
854+ action' = action & response.respTypes .~ allMime t
855+ & response.respStatus .~ status
856+ t = Proxy :: Proxy '[ct]
857+ method' = reflectMethod (Proxy :: Proxy method)
858+ status = fromInteger $ natVal (Proxy :: Proxy status)
859+ p = Proxy :: Proxy a
860+
843instance OVERLAPPING_
861instance OVERLAPPING_
844 (ToSample a, AllMimeRender (ct ': cts) a, KnownNat status
862 (ToSample a, AllMimeRender (ct ': cts) a, KnownNat status
845 , ReflectMethod method, AllHeaderSamples ls, GetHeaders (HList ls))
863 , ReflectMethod method, AllHeaderSamples ls, GetHeaders (HList ls))
servant-foreign/src/Servant/Foreign/Internal.hs+14
@@ -238,6 +238,20 @@ instance (Elem JSON list, HasForeignType lang ftype a, ReflectMethod method)
238 method = reflectMethod (Proxy :: Proxy method)
238 method = reflectMethod (Proxy :: Proxy method)
239 methodLC = toLower $ decodeUtf8 method
239 methodLC = toLower $ decodeUtf8 method
240
240
241+-- | TODO: doesn't taking framing into account.
242+instance (ct ~ JSON, HasForeignType lang ftype a, ReflectMethod method)
243+ => HasForeign lang ftype (Stream method status framing ct a) where
244+ type Foreign ftype (Stream method status framing ct a) = Req ftype
245+
246+ foreignFor lang Proxy Proxy req =
247+ req & reqFuncName . _FunctionName %~ (methodLC :)
248+ & reqMethod .~ method
249+ & reqReturnType .~ Just retType
250+ where
251+ retType = typeFor lang (Proxy :: Proxy ftype) (Proxy :: Proxy a)
252+ method = reflectMethod (Proxy :: Proxy method)
253+ methodLC = toLower $ decodeUtf8 method
254+
241instance (KnownSymbol sym, HasForeignType lang ftype (RequiredArgument mods a), HasForeign lang ftype api)
255instance (KnownSymbol sym, HasForeignType lang ftype (RequiredArgument mods a), HasForeign lang ftype api)
242 => HasForeign lang ftype (Header' mods sym a :> api) where
256 => HasForeign lang ftype (Header' mods sym a :> api) where
243 type Foreign ftype (Header' mods sym a :> api) = Foreign ftype api
257 type Foreign ftype (Header' mods sym a :> api) = Foreign ftype api
servant/src/Servant/API.hs+1−1
@@ -114,7 +114,7 @@ import Servant.API.ResponseHeaders
114 ResponseHeader (..), addHeader, getHeadersHList, getResponse,
114 ResponseHeader (..), addHeader, getHeadersHList, getResponse,
115 noHeader)
115 noHeader)
116import Servant.API.Stream
116import Servant.API.Stream
117− (BoundaryStrategy (..), BuildFromStream (..),
117+ (BoundaryStrategy (..), FromResultStream (..),
118 ByteStringParser (..), FramingRender (..),
118 ByteStringParser (..), FramingRender (..),
119 FramingUnrender (..), NetstringFraming, NewlineFraming,
119 FramingUnrender (..), NetstringFraming, NewlineFraming,
120 NoFraming, ResultStream (..), Stream, StreamGenerator (..),
120 NoFraming, ResultStream (..), Stream, StreamGenerator (..),
servant/src/Servant/API/Internal/Test/ComprehensiveAPI.hs+2
@@ -7,6 +7,7 @@
7module Servant.API.Internal.Test.ComprehensiveAPI where
7module Servant.API.Internal.Test.ComprehensiveAPI where
8
8
9import Data.Proxy
9import Data.Proxy
10+ (Proxy (..))
10import Servant.API
11import Servant.API
11
12
12type GET = Get '[JSON] NoContent
13type GET = Get '[JSON] NoContent
@@ -38,6 +39,7 @@ type ComprehensiveAPIWithoutRaw =
38 Vault :> GET :<|>
39 Vault :> GET :<|>
39 Verb 'POST 204 '[JSON] NoContent :<|>
40 Verb 'POST 204 '[JSON] NoContent :<|>
40 Verb 'POST 204 '[JSON] Int :<|>
41 Verb 'POST 204 '[JSON] Int :<|>
42+ Stream 'GET 200 NetstringFraming JSON [Int] :<|>
41 WithNamedContext "foo" '[] GET :<|>
43 WithNamedContext "foo" '[] GET :<|>
42 CaptureAll "foo" Int :> GET :<|>
44 CaptureAll "foo" Int :> GET :<|>
43 Summary "foo" :> GET :<|>
45 Summary "foo" :> GET :<|>
servant/src/Servant/API/Stream.hs+104−32
@@ -1,23 +1,48 @@
1−{-# LANGUAGE DataKinds #-}
1+{-# LANGUAGE DataKinds #-}
2−{-# LANGUAGE DeriveDataTypeable #-}
2+{-# LANGUAGE DeriveDataTypeable #-}
3−{-# LANGUAGE DeriveGeneric #-}
3+{-# LANGUAGE DeriveGeneric #-}
4−{-# LANGUAGE FlexibleInstances #-}
4+{-# LANGUAGE FlexibleInstances #-}
5{-# LANGUAGE FunctionalDependencies #-}
5{-# LANGUAGE FunctionalDependencies #-}
6−{-# LANGUAGE KindSignatures #-}
6+{-# LANGUAGE KindSignatures #-}
7−{-# LANGUAGE MultiParamTypeClasses #-}
7+{-# LANGUAGE MultiParamTypeClasses #-}
8−{-# LANGUAGE OverloadedStrings #-}
8+{-# LANGUAGE OverloadedStrings #-}
9−{-# LANGUAGE PolyKinds #-}
9+{-# LANGUAGE PolyKinds #-}
10−{-# LANGUAGE RankNTypes #-}
10+{-# LANGUAGE RankNTypes #-}
11−{-# LANGUAGE TupleSections #-}
11+{-# LANGUAGE TupleSections #-}
12{-# OPTIONS_HADDOCK not-home #-}
12{-# OPTIONS_HADDOCK not-home #-}
13
13
14−module Servant.API.Stream where
14+module Servant.API.Stream (
15+ Stream,
16+ StreamGet,
17+ StreamPost,
18+ -- * Sources
19+ --
20+ -- | Both 'StreamGenerator' and 'ResultStream' are equivalent
21+ -- to some *source* in streaming libraries.
22+ StreamGenerator (..),
23+ ToStreamGenerator (..),
24+ ResultStream (..),
25+ FromResultStream (..),
26+ -- * Framing
27+ FramingRender (..),
28+ FramingUnrender (..),
29+ BoundaryStrategy (..),
30+ ByteStringParser (..),
31+ -- ** Strategies
32+ NoFraming,
33+ NewlineFraming,
34+ NetstringFraming,
35+ ) where
15
36
16import Control.Arrow
37import Control.Arrow
17 (first)
38 (first)
18import Data.ByteString.Lazy
39import Data.ByteString.Lazy
19 (ByteString, empty)
40 (ByteString, empty)
20import qualified Data.ByteString.Lazy.Char8 as LB
41import qualified Data.ByteString.Lazy.Char8 as LB
42+import Data.Foldable
43+ (traverse_)
44+import Data.List.NonEmpty
45+ (NonEmpty (..))
21import Data.Monoid
46import Data.Monoid
22 ((<>))
47 ((<>))
23import Data.Proxy
48import Data.Proxy
@@ -30,35 +55,82 @@ import GHC.TypeLits
30 (Nat)
55 (Nat)
31import Network.HTTP.Types.Method
56import Network.HTTP.Types.Method
32 (StdMethod (..))
57 (StdMethod (..))
58+import System.IO.Unsafe
59+ (unsafeInterleaveIO)
33import Text.Read
60import Text.Read
34 (readMaybe)
61 (readMaybe)
35
62
63+-- | A Stream endpoint for a given method emits a stream of encoded values at a
36−-- | A Stream endpoint for a given method emits a stream of encoded values at a given Content-Type, delimited by a framing strategy. Stream endpoints always return response code 200 on success. Type synonyms are provided for standard methods.
64+-- given Content-Type, delimited by a framing strategy. Stream endpoints always
65+-- return response code 200 on success. Type synonyms are provided for standard
66+-- methods.
37data Stream (method :: k1) (status :: Nat) (framing :: *) (contentType :: *) (a :: *)
67data Stream (method :: k1) (status :: Nat) (framing :: *) (contentType :: *) (a :: *)
38 deriving (Typeable, Generic)
68 deriving (Typeable, Generic)
39
69
40type StreamGet = Stream 'GET 200
70type StreamGet = Stream 'GET 200
41type StreamPost = Stream 'POST 200
71type StreamPost = Stream 'POST 200
42
72
73+-- | Stream endpoints may be implemented as producing a @StreamGenerator@ a
74+-- function that itself takes two emit functions the first to be used on the
75+-- first value the stream emits, and the second to be used on all subsequent
43−-- | Stream endpoints may be implemented as producing a @StreamGenerator@ -- a function that itself takes two emit functions -- the first to be used on the first value the stream emits, and the second to be used on all subsequent values (to allow interspersed framing strategies such as comma separation).
76+-- values (to allow interspersed framing strategies such as comma separation).
44−newtype StreamGenerator a = StreamGenerator {getStreamGenerator :: (a -> IO ()) -> (a -> IO ()) -> IO ()}
77+newtype StreamGenerator a = StreamGenerator { getStreamGenerator :: (a -> IO ()) -> (a -> IO ()) -> IO () }
45
78
46-- | ToStreamGenerator is intended to be implemented for types such as Conduit, Pipe, etc. By implementing this class, all such streaming abstractions can be used directly as endpoints.
79-- | ToStreamGenerator is intended to be implemented for types such as Conduit, Pipe, etc. By implementing this class, all such streaming abstractions can be used directly as endpoints.
47class ToStreamGenerator a b | a -> b where
80class ToStreamGenerator a b | a -> b where
48− toStreamGenerator :: a -> StreamGenerator b
81+ toStreamGenerator :: a -> StreamGenerator b
49−
82+
50−instance ToStreamGenerator (StreamGenerator a) a
83+instance ToStreamGenerator (StreamGenerator a) a where
84+ toStreamGenerator x = x
85+
86+instance ToStreamGenerator (NonEmpty a) a where
87+ toStreamGenerator (x :| xs) = StreamGenerator $ \f g -> f x >> traverse_ g xs
88+
89+instance ToStreamGenerator [a] a where
90+ toStreamGenerator [] = StreamGenerator $ \_ _ -> return ()
51− where toStreamGenerator x = x
91+ toStreamGenerator (x : xs) = StreamGenerator $ \f g -> f x >> traverse_ g xs
52−
92+
53−-- | Clients reading from streaming endpoints can be implemented as producing a @ResultStream@ that captures the setup, takedown, and incremental logic for a read, being an IO continuation that takes a producer of Just either values or errors that terminates with a Nothing.
93+-- | Clients reading from streaming endpoints can be implemented as producing a
94+-- @ResultStream@ that captures the setup, takedown, and incremental logic for
95+-- a read, being an IO continuation that takes a producer of Just either values
96+-- or errors that terminates with a Nothing.
54−newtype ResultStream a = ResultStream (forall b. (IO (Maybe (Either String a)) -> IO b) -> IO b)
97+newtype ResultStream a = ResultStream { runResultStream :: forall b. (IO (Maybe (Either String a)) -> IO b) -> IO b }
55−
98+
56−-- | BuildFromStream is intended to be implemented for types such as Conduit, Pipe, etc. By implementing this class, all such streaming abstractions can be used directly on the client side for talking to streaming endpoints.
99+-- | FromResultStream is intended to be implemented for types such as Conduit, Pipe, etc. By implementing this class, all such streaming abstractions can be used directly on the client side for talking to streaming endpoints.
57−class BuildFromStream a b where
100+class FromResultStream a b | b -> a where
58− buildFromStream :: ResultStream a -> b
101+ fromResultStream :: ResultStream a -> IO b
59−
102+
60−instance BuildFromStream a (ResultStream a)
103+instance FromResultStream a (ResultStream a) where
61− where buildFromStream x = x
104+ fromResultStream = return
105+
106+-- | Uses 'unsafeInterleaveIO'
107+instance FromResultStream a [a] where
108+ fromResultStream x = runResultStream x lazyRead
109+
110+-- | Uses 'unsafeInterleaveIO'
111+instance FromResultStream a (NonEmpty a) where
112+ fromResultStream x = runResultStream x $ \r -> do
113+ e <- r
114+ case e of
115+ Nothing -> fail "Empty stream"
116+ Just (Left er) -> fail er
117+ Just (Right y) -> do
118+ ys <- lazyRead r
119+ return (y :| ys)
120+
121+lazyRead :: IO (Maybe (Either String a)) -> IO [a]
122+lazyRead r = go
123+ where
124+ go = unsafeInterleaveIO loop
125+
126+ loop = do
127+ e <- r
128+ case e of
129+ Nothing -> return []
130+ Just (Left er) -> fail er
131+ Just (Right y) -> do
132+ ys <- go
133+ return (y : ys)
62
134
63-- | The FramingRender class provides the logic for emitting a framing strategy. The strategy emits a header, followed by boundary-delimited data, and finally a termination character. For many strategies, some of these will just be empty bytestrings.
135-- | The FramingRender class provides the logic for emitting a framing strategy. The strategy emits a header, followed by boundary-delimited data, and finally a termination character. For many strategies, some of these will just be empty bytestrings.
64class FramingRender strategy a where
136class FramingRender strategy a where
@@ -74,10 +146,10 @@ data BoundaryStrategy = BoundaryStrategyBracket (ByteString -> (ByteString,ByteS
74 | BoundaryStrategyGeneral (ByteString -> ByteString)
146 | BoundaryStrategyGeneral (ByteString -> ByteString)
75
147
76-- | 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.
148-- | 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.
77−data ByteStringParser a = ByteStringParser {
149+data ByteStringParser a = ByteStringParser
78− parseIncremental :: ByteString -> Maybe (a, ByteString),
150+ { parseIncremental :: ByteString -> Maybe (a, ByteString)
79− parseEOF :: ByteString -> (a, ByteString)
151+ , parseEOF :: ByteString -> (a, ByteString)
80−}
152+ }
81
153
82-- | 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.
154-- | 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.
83class FramingUnrender strategy a where
155class FramingUnrender strategy a where
servant/src/Servant/Utils/Links.hs+2−2
@@ -460,8 +460,8 @@ instance HasLink Raw where
460 type MkLink Raw a = a
460 type MkLink Raw a = a
461 toLink toA _ = toA
461 toLink toA _ = toA
462
462
463−instance HasLink (Stream m fr ct a) where
463+instance HasLink (Stream m status fr ct a) where
464− type MkLink (Stream m fr ct a) r = r
464+ type MkLink (Stream m status fr ct a) r = r
465 toLink toA _ = toA
465 toLink toA _ = toA
466
466
467-- AuthProtext instances
467-- AuthProtext instances