git.iko.soy/typeable/servant
on master help sign in
Separate Servant.Client.Streaming
8feda81Oleg Grenrus
doc/cookbook/basic-streaming/Streaming.lhs+1−1
@@ -38,7 +38,7 @@ import Text.Read
38 (readMaybe)
38 (readMaybe)
39
39
40import Servant
40import Servant
41−import Servant.Client
41+import Servant.Client.Streaming
42import qualified Servant.Types.SourceT as S
42import qualified Servant.Types.SourceT as S
43
43
44import qualified Network.Wai.Handler.Warp as Warp
44import qualified Network.Wai.Handler.Warp as Warp
doc/tutorial/Client.lhs+12−12
@@ -21,7 +21,8 @@ import Network.HTTP.Client (newManager, defaultManagerSettings)
21import Servant.API
21import Servant.API
22import Servant.Client
22import Servant.Client
23import Servant.Types.SourceT (foreach)
23import Servant.Types.SourceT (foreach)
24−import Control.Monad.Codensity (Codensity)
24+
25+import qualified Servant.Client.Streaming as S
25```
26```
26
27
27Also, we need examples for some domain specific data types:
28Also, we need examples for some domain specific data types:
@@ -224,6 +225,8 @@ type StreamAPI = "positionStream" :> StreamGet NewlineFraming JSON (SourceIO Pos
224
225
225Note that we use the same `SourceIO` type as on the server-side
226Note that we use the same `SourceIO` type as on the server-side
226(this is different from `servant-0.14`).
227(this is different from `servant-0.14`).
228+However, we have to use different client, `Servant.Client.Streaming`,
229+which can stream (but has different API).
227
230
228In any case, here's how we write a function to query our API:
231In any case, here's how we write a function to query our API:
229
232
@@ -231,21 +234,18 @@ In any case, here's how we write a function to query our API:
231streamAPI :: Proxy StreamAPI
234streamAPI :: Proxy StreamAPI
232streamAPI = Proxy
235streamAPI = Proxy
233
236
234−posStream :: ClientM (Codensity IO (SourceIO Position))
237+posStream :: S.ClientM (SourceIO Position)
235−posStream = client streamAPI
238+posStream = S.client streamAPI
236```
239```
237
240
238−We'll get back a `Codensity IO (SourceIO Position)`. The wrapping in
241+We'll get back a `SourceIO Position`. Instead of `runClientM`, the streaming
239−`Codensity` is generally necessary, as `Codensity` lets us `bracket` things
240−properly. This is best explained by an example. To consume `ClientM (Codentity
241−IO ...)` we can use `withClientM` helper: the underlying HTTP connection is
242+client provides `withClientM`: the underlying HTTP connection is open only
242−open only until the inner functions exits. Inside the block we can e.g just
243+until the inner functions exits. Inside the block we can e.g just print out
243−print out all elements from a `SourceIO`, to give some idea of how to work with
244+all elements from a `SourceIO`, to give some idea of how to work with them.
244−them.
245
245
246``` haskell
246``` haskell
247−printSourceIO :: Show a => ClientEnv -> ClientM (Codensity IO (SourceIO a)) -> IO ()
247+printSourceIO :: Show a => ClientEnv -> S.ClientM (SourceIO a) -> IO ()
248−printSourceIO env c = withClientM c env $ \e -> case e of
248+printSourceIO env c = S.withClientM c env $ \e -> case e of
249 Left err -> putStrLn $ "Error: " ++ show err
249 Left err -> putStrLn $ "Error: " ++ show err
250 Right rs -> foreach fail print rs
250 Right rs -> foreach fail print rs
251```
251```
doc/tutorial/tutorial.cabal−1
@@ -46,7 +46,6 @@ library
46 , http-client
46 , http-client
47 , http-media
47 , http-media
48 , http-types
48 , http-types
49− , kan-extensions
50 , mtl
49 , mtl
51 , string-conversions
50 , string-conversions
52 , text
51 , text
servant-client-core/servant-client-core.cabal−1
@@ -70,7 +70,6 @@ library
70 , http-api-data >= 0.3.8.1 && < 0.4
70 , http-api-data >= 0.3.8.1 && < 0.4
71 , http-media >= 0.7.1.2 && < 0.8
71 , http-media >= 0.7.1.2 && < 0.8
72 , http-types >= 0.12.1 && < 0.13
72 , http-types >= 0.12.1 && < 0.13
73− , kan-extensions >= 5.2 && < 5.3
74 , network-uri >= 2.6.1.0 && < 2.7
73 , network-uri >= 2.6.1.0 && < 2.7
75 , safe >= 0.3.17 && < 0.4
74 , safe >= 0.3.17 && < 0.4
76
75
servant-client-core/src/Servant/Client/Core.hs+2
@@ -44,6 +44,8 @@ module Servant.Client.Core
44 , GenResponse (..)
44 , GenResponse (..)
45 , RunClient(..)
45 , RunClient(..)
46 , module Servant.Client.Core.Internal.BaseUrl
46 , module Servant.Client.Core.Internal.BaseUrl
47+ -- ** Streaming
48+ , RunStreamingClient(..)
47 , StreamingResponse
49 , StreamingResponse
48
50
49 -- * Writing HasClient instances
51 -- * Writing HasClient instances
servant-client-core/src/Servant/Client/Core/Internal/HasClient.hs+18−23
@@ -36,20 +36,17 @@ import qualified Network.HTTP.Types as H
36import Servant.API
36import Servant.API
37 ((:<|>) ((:<|>)), (:>), AuthProtect, BasicAuth, BasicAuthData,
37 ((:<|>) ((:<|>)), (:>), AuthProtect, BasicAuth, BasicAuthData,
38 BuildHeadersTo (..), Capture', CaptureAll, Description,
38 BuildHeadersTo (..), Capture', CaptureAll, Description,
39− EmptyAPI, FramingUnrender (..), FromSourceIO (..),
39+ EmptyAPI, FramingUnrender (..), FromSourceIO (..), Header',
40− Header', Headers (..), HttpVersion, IsSecure,
40+ Headers (..), HttpVersion, IsSecure, MimeRender (mimeRender),
41− MimeRender (mimeRender), MimeUnrender (mimeUnrender),
42− NoContent (NoContent), QueryFlag, QueryParam', QueryParams,
41+ MimeUnrender (mimeUnrender), NoContent (NoContent), QueryFlag,
43− Raw, ReflectMethod (..), RemoteHost, ReqBody', SBoolI, Stream,
42+ QueryParam', QueryParams, Raw, ReflectMethod (..), RemoteHost,
44− StreamBody, Summary, ToHttpApiData, Vault, Verb,
43+ ReqBody', SBoolI, Stream, StreamBody, Summary, ToHttpApiData,
45− WithNamedContext, contentType, getHeadersHList, getResponse,
44+ Vault, Verb, WithNamedContext, contentType, getHeadersHList,
46− toQueryParam, toUrlPiece)
45+ getResponse, toQueryParam, toUrlPiece)
47import Servant.API.ContentTypes
46import Servant.API.ContentTypes
48 (contentTypes)
47 (contentTypes)
49import Servant.API.Modifiers
48import Servant.API.Modifiers
50 (FoldRequired, RequiredArgument, foldRequiredArgument)
49 (FoldRequired, RequiredArgument, foldRequiredArgument)
51−import Control.Monad.Codensity
52− (Codensity (..))
53import qualified Servant.Types.SourceT as S
50import qualified Servant.Types.SourceT as S
54
51
55import Servant.Client.Core.Internal.Auth
52import Servant.Client.Core.Internal.Auth
@@ -272,25 +269,23 @@ instance {-# OVERLAPPING #-}
272 hoistClientMonad _ _ f ma = f ma
269 hoistClientMonad _ _ f ma = f ma
273
270
274instance {-# OVERLAPPABLE #-}
271instance {-# OVERLAPPABLE #-}
275− ( RunClient m, MimeUnrender ct chunk, ReflectMethod method,
272+ ( RunStreamingClient m, MimeUnrender ct chunk, ReflectMethod method,
276 FramingUnrender framing, FromSourceIO chunk a
273 FramingUnrender framing, FromSourceIO chunk a
277 ) => HasClient m (Stream method status framing ct a) where
274 ) => HasClient m (Stream method status framing ct a) where
278
275
279− type Client m (Stream method status framing ct a) = m (Codensity IO a)
276+ type Client m (Stream method status framing ct a) = m a
280
277
281 hoistClientMonad _ _ f ma = f ma
278 hoistClientMonad _ _ f ma = f ma
282
279
283− clientWithRoute _pm Proxy req = do
280+ clientWithRoute _pm Proxy req = withStreamingRequest req' $ \gres -> do
284− sresp <- streamingRequest req
285− { requestAccept = fromList [contentType (Proxy :: Proxy ct)]
286− , requestMethod = reflectMethod (Proxy :: Proxy method)
287− }
288− return $ do
289− let mimeUnrender' = mimeUnrender (Proxy :: Proxy ct) :: BL.ByteString -> Either String chunk
281+ let mimeUnrender' = mimeUnrender (Proxy :: Proxy ct) :: BL.ByteString -> Either String chunk
290− framingUnrender' = framingUnrender (Proxy :: Proxy framing) mimeUnrender'
282+ framingUnrender' = framingUnrender (Proxy :: Proxy framing) mimeUnrender'
291− gres <- sresp
292− return $ fromSourceIO $ framingUnrender' $ S.fromAction BS.null (responseBody gres)
283+ return $ fromSourceIO $ framingUnrender' $ S.fromAction BS.null (responseBody gres)
293−
284+ where
285+ req' = req
286+ { requestAccept = fromList [contentType (Proxy :: Proxy ct)]
287+ , requestMethod = reflectMethod (Proxy :: Proxy method)
288+ }
294
289
295-- | If you use a 'Header' in one of your endpoints in your API,
290-- | If you use a 'Header' in one of your endpoints in your API,
296-- the corresponding querying function will automatically take
291-- the corresponding querying function will automatically take
servant-client-core/src/Servant/Client/Core/Internal/Request.hs+1−3
@@ -38,8 +38,6 @@ import Network.HTTP.Media
38import Network.HTTP.Types
38import Network.HTTP.Types
39 (Header, HeaderName, HttpVersion, Method, QueryItem, Status,
39 (Header, HeaderName, HttpVersion, Method, QueryItem, Status,
40 http11, methodGet)
40 http11, methodGet)
41−import Control.Monad.Codensity
42− (Codensity (..))
43import Web.HttpApiData
41import Web.HttpApiData
44 (ToHttpApiData, toEncodedUrlPiece, toHeader)
42 (ToHttpApiData, toEncodedUrlPiece, toHeader)
45
43
@@ -91,7 +89,7 @@ data GenResponse a = Response
91 } deriving (Eq, Show, Generic, Typeable, Functor, Foldable, Traversable)
89 } deriving (Eq, Show, Generic, Typeable, Functor, Foldable, Traversable)
92
90
93type Response = GenResponse LBS.ByteString
91type Response = GenResponse LBS.ByteString
94−type StreamingResponse = Codensity IO (GenResponse (IO BS.ByteString))
92+type StreamingResponse = GenResponse (IO BS.ByteString)
95
93
96-- A GET request to the top-level path
94-- A GET request to the top-level path
97defaultRequest :: Request
95defaultRequest :: Request
servant-client-core/src/Servant/Client/Core/Internal/RunClient.hs+9−2
@@ -31,9 +31,11 @@ import Servant.Client.Core.Internal.Request
31class Monad m => RunClient m where
31class Monad m => RunClient m where
32 -- | How to make a request.
32 -- | How to make a request.
33 runRequest :: Request -> m Response
33 runRequest :: Request -> m Response
34− streamingRequest :: Request -> m StreamingResponse
35 throwServantError :: ServantError -> m a
34 throwServantError :: ServantError -> m a
36
35
36+class RunClient m => RunStreamingClient m where
37+ withStreamingRequest :: Request -> (StreamingResponse -> IO a) -> m a
38+
37checkContentTypeHeader :: RunClient m => Response -> m MediaType
39checkContentTypeHeader :: RunClient m => Response -> m MediaType
38checkContentTypeHeader response =
40checkContentTypeHeader response =
39 case lookup "Content-Type" $ toList $ responseHeaders response of
41 case lookup "Content-Type" $ toList $ responseHeaders response of
@@ -56,5 +58,10 @@ decodedAs response contentType = do
56
58
57instance ClientF ~ f => RunClient (Free f) where
59instance ClientF ~ f => RunClient (Free f) where
58 runRequest req = liftF (RunRequest req id)
60 runRequest req = liftF (RunRequest req id)
59− streamingRequest req = liftF (StreamingRequest req id)
60 throwServantError = liftF . Throw
61 throwServantError = liftF . Throw
62+
63+{-
64+Free and streaming?
65+instance ClientF ~ f => RunStreamingClient (Free f) where
66+ streamingRequest req = liftF (StreamingRequest req id)
67+-}
servant-client/servant-client.cabal+2
@@ -33,7 +33,9 @@ source-repository head
33library
33library
34 exposed-modules:
34 exposed-modules:
35 Servant.Client
35 Servant.Client
36+ Servant.Client.Streaming
36 Servant.Client.Internal.HttpClient
37 Servant.Client.Internal.HttpClient
38+ Servant.Client.Internal.HttpClient.Streaming
37
39
38 -- Bundled with GHC: Lower bound to not force re-installs
40 -- Bundled with GHC: Lower bound to not force re-installs
39 -- text and mtl are bundled starting with GHC-8.4
41 -- text and mtl are bundled starting with GHC-8.4
servant-client/src/Servant/Client.hs−1
@@ -7,7 +7,6 @@ module Servant.Client
7 ( client
7 ( client
8 , ClientM
8 , ClientM
9 , runClientM
9 , runClientM
10− , withClientM
11 , ClientEnv(..)
10 , ClientEnv(..)
12 , mkClientEnv
11 , mkClientEnv
13 , hoistClient
12 , hoistClient
servant-client/src/Servant/Client/Internal/HttpClient.hs−29
@@ -21,8 +21,6 @@ import Control.Monad.Base
21 (MonadBase (..))
21 (MonadBase (..))
22import Control.Monad.Catch
22import Control.Monad.Catch
23 (MonadCatch, MonadThrow)
23 (MonadCatch, MonadThrow)
24−import Control.Monad.Codensity
25− (Codensity (..))
26import Control.Monad.Error.Class
24import Control.Monad.Error.Class
27 (MonadError (..))
25 (MonadError (..))
28import Control.Monad.Reader
26import Control.Monad.Reader
@@ -134,7 +132,6 @@ instance Alt ClientM where
134
132
135instance RunClient ClientM where
133instance RunClient ClientM where
136 runRequest = performRequest
134 runRequest = performRequest
137− streamingRequest = performStreamingRequest
138 throwServantError = throwError
135 throwServantError = throwError
139
136
140instance ClientLike (ClientM a) (ClientM a) where
137instance ClientLike (ClientM a) (ClientM a) where
@@ -143,17 +140,6 @@ instance ClientLike (ClientM a) (ClientM a) where
143runClientM :: ClientM a -> ClientEnv -> IO (Either ServantError a)
140runClientM :: ClientM a -> ClientEnv -> IO (Either ServantError a)
144runClientM cm env = runExceptT $ flip runReaderT env $ unClientM cm
141runClientM cm env = runExceptT $ flip runReaderT env $ unClientM cm
145
142
146−withClientM
147− :: ClientM (Codensity IO a) -- ^ client with codensity result
148− -> ClientEnv -- ^ environment
149− -> (Either ServantError a -> IO b) -- ^ continuation
150− -> IO b
151−withClientM cm env k = do
152− e <- runExceptT (runReaderT (unClientM cm) env)
153− case e of
154− Left err -> k (Left err)
155− Right cod -> runCodensity cod (k . Right)
156−
157performRequest :: Request -> ClientM Response
143performRequest :: Request -> ClientM Response
158performRequest req = do
144performRequest req = do
159 ClientEnv m burl cookieJar' <- ask
145 ClientEnv m burl cookieJar' <- ask
@@ -186,21 +172,6 @@ performRequest req = do
186 throwError $ FailureResponse ourResponse
172 throwError $ FailureResponse ourResponse
187 return ourResponse
173 return ourResponse
188
174
189−performStreamingRequest :: Request -> ClientM StreamingResponse
190−performStreamingRequest req = do
191− m <- asks manager
192− burl <- asks baseUrl
193− let request = requestToClientRequest burl req
194− return $ Codensity $
195− \k -> Client.withResponse request m $
196− \r -> do
197− let status = Client.responseStatus r
198− status_code = statusCode status
199− unless (status_code >= 200 && status_code < 300) $ do
200− b <- BSL.fromChunks <$> Client.brConsume (Client.responseBody r)
201− throw $ FailureResponse $ clientResponseToResponse r { Client.responseBody = b }
202− k (clientResponseToResponse r)
203−
204clientResponseToResponse :: Client.Response a -> GenResponse a
175clientResponseToResponse :: Client.Response a -> GenResponse a
205clientResponseToResponse r = Response
176clientResponseToResponse r = Response
206 { responseStatusCode = Client.responseStatus r
177 { responseStatusCode = Client.responseStatus r
servant-client/src/Servant/Client/Internal/HttpClient/Streaming.hsnew file+172
@@ -0,0 +1,172 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DeriveGeneric #-}
3+{-# LANGUAGE FlexibleContexts #-}
4+{-# LANGUAGE FlexibleInstances #-}
5+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
6+{-# LANGUAGE MultiParamTypeClasses #-}
7+{-# LANGUAGE RankNTypes #-}
8+{-# LANGUAGE ScopedTypeVariables #-}
9+{-# LANGUAGE TypeFamilies #-}
10+module Servant.Client.Internal.HttpClient.Streaming (
11+ module Servant.Client.Internal.HttpClient.Streaming,
12+ ClientEnv (..),
13+ mkClientEnv,
14+ clientResponseToResponse,
15+ requestToClientRequest,
16+ catchConnectionError,
17+ ) where
18+
19+import Prelude ()
20+import Prelude.Compat
21+
22+import Control.Concurrent.STM.TVar
23+import Control.Exception
24+import Control.Monad
25+import Control.Monad.Base
26+ (MonadBase (..))
27+import Control.Monad.Codensity
28+ (Codensity (..))
29+import Control.Monad.Error.Class
30+ (MonadError (..))
31+import Control.Monad.Reader
32+import Control.Monad.STM
33+ (atomically)
34+import Control.Monad.Trans.Except
35+import qualified Data.ByteString.Lazy as BSL
36+import Data.Foldable
37+ (for_)
38+import Data.Functor.Alt
39+ (Alt (..))
40+import Data.Proxy
41+ (Proxy (..))
42+import Data.Time.Clock
43+ (getCurrentTime)
44+import GHC.Generics
45+import Network.HTTP.Types
46+ (statusCode)
47+
48+import qualified Network.HTTP.Client as Client
49+
50+import Servant.Client.Core
51+import Servant.Client.Internal.HttpClient
52+ (ClientEnv (..), catchConnectionError,
53+ clientResponseToResponse, mkClientEnv, requestToClientRequest)
54+
55+
56+-- | Generates a set of client functions for an API.
57+--
58+-- Example:
59+--
60+-- > type API = Capture "no" Int :> Get '[JSON] Int
61+-- > :<|> Get '[JSON] [Bool]
62+-- >
63+-- > api :: Proxy API
64+-- > api = Proxy
65+-- >
66+-- > getInt :: Int -> ClientM Int
67+-- > getBools :: ClientM [Bool]
68+-- > getInt :<|> getBools = client api
69+client :: HasClient ClientM api => Proxy api -> Client ClientM api
70+client api = api `clientIn` (Proxy :: Proxy ClientM)
71+
72+-- | Change the monad the client functions live in, by
73+-- supplying a conversion function
74+-- (a natural transformation to be precise).
75+--
76+-- For example, assuming you have some @manager :: 'Manager'@ and
77+-- @baseurl :: 'BaseUrl'@ around:
78+--
79+-- > type API = Get '[JSON] Int :<|> Capture "n" Int :> Post '[JSON] Int
80+-- > api :: Proxy API
81+-- > api = Proxy
82+-- > getInt :: IO Int
83+-- > postInt :: Int -> IO Int
84+-- > getInt :<|> postInt = hoistClient api (flip runClientM cenv) (client api)
85+-- > where cenv = mkClientEnv manager baseurl
86+hoistClient
87+ :: HasClient ClientM api
88+ => Proxy api
89+ -> (forall a. m a -> n a)
90+ -> Client m api
91+ -> Client n api
92+hoistClient = hoistClientMonad (Proxy :: Proxy ClientM)
93+
94+-- | @ClientM@ is the monad in which client functions run. Contains the
95+-- 'Client.Manager' and 'BaseUrl' used for requests in the reader environment.
96+newtype ClientM a = ClientM
97+ { unClientM :: ReaderT ClientEnv (ExceptT ServantError (Codensity IO)) a }
98+ deriving ( Functor, Applicative, Monad, MonadIO, Generic
99+ , MonadReader ClientEnv, MonadError ServantError)
100+
101+instance MonadBase IO ClientM where
102+ liftBase = ClientM . liftIO
103+
104+-- | Try clients in order, last error is preserved.
105+instance Alt ClientM where
106+ a <!> b = a `catchError` \_ -> b
107+
108+instance RunClient ClientM where
109+ runRequest = performRequest
110+ throwServantError = throwError
111+
112+instance RunStreamingClient ClientM where
113+ withStreamingRequest = performWithStreamingRequest
114+
115+instance ClientLike (ClientM a) (ClientM a) where
116+ mkClient = id
117+
118+withClientM :: ClientM a -> ClientEnv -> (Either ServantError a -> IO b) -> IO b
119+withClientM cm env k =
120+ let Codensity f = runExceptT $ flip runReaderT env $ unClientM cm
121+ in f k
122+
123+performRequest :: Request -> ClientM Response
124+performRequest req = do
125+ -- TODO: should use Client.withResponse here too
126+ ClientEnv m burl cookieJar' <- ask
127+ let clientRequest = requestToClientRequest burl req
128+ request <- case cookieJar' of
129+ Nothing -> pure clientRequest
130+ Just cj -> liftIO $ do
131+ now <- getCurrentTime
132+ atomically $ do
133+ oldCookieJar <- readTVar cj
134+ let (newRequest, newCookieJar) =
135+ Client.insertCookiesIntoRequest
136+ (requestToClientRequest burl req)
137+ oldCookieJar
138+ now
139+ writeTVar cj newCookieJar
140+ pure newRequest
141+
142+ eResponse <- liftIO $ catchConnectionError $ Client.httpLbs request m
143+ case eResponse of
144+ Left err -> throwError err
145+ Right response -> do
146+ for_ cookieJar' $ \cj -> liftIO $ do
147+ now' <- getCurrentTime
148+ atomically $ modifyTVar' cj (fst . Client.updateCookieJar response request now')
149+ let status = Client.responseStatus response
150+ status_code = statusCode status
151+ ourResponse = clientResponseToResponse response
152+ unless (status_code >= 200 && status_code < 300) $
153+ throwError $ FailureResponse ourResponse
154+ return ourResponse
155+
156+performWithStreamingRequest :: Request -> (StreamingResponse -> IO a) -> ClientM a
157+performWithStreamingRequest req k = do
158+ m <- asks manager
159+ burl <- asks baseUrl
160+ let request = requestToClientRequest burl req
161+ ClientM $ lift $ lift $ Codensity $ \k1 ->
162+ Client.withResponse request m $ \res -> do
163+ let status = Client.responseStatus res
164+ status_code = statusCode status
165+
166+ -- we throw FailureResponse in IO :(
167+ unless (status_code >= 200 && status_code < 300) $ do
168+ b <- BSL.fromChunks <$> Client.brConsume (Client.responseBody res)
169+ throwIO $ FailureResponse $ clientResponseToResponse res { Client.responseBody = b }
170+
171+ x <- k (clientResponseToResponse res)
172+ k1 x
servant-client/src/Servant/Client/Streaming.hsnew file+17
@@ -0,0 +1,17 @@
1+-- | This module provides 'client' which can automatically generate
2+-- querying functions for each endpoint just from the type representing your
3+-- API.
4+--
5+-- This client supports streaming operations.
6+module Servant.Client.Streaming
7+ ( client
8+ , ClientM
9+ , withClientM
10+ , ClientEnv(..)
11+ , mkClientEnv
12+ , hoistClient
13+ , module Servant.Client.Core.Reexport
14+ ) where
15+
16+import Servant.Client.Core.Reexport
17+import Servant.Client.Internal.HttpClient.Streaming
servant-client/test/Servant/ClientSpec.hs+1−1
@@ -72,7 +72,7 @@ import Servant.Server
72import Servant.Server.Experimental.Auth
72import Servant.Server.Experimental.Auth
73
73
74-- This declaration simply checks that all instances are in place.
74-- This declaration simply checks that all instances are in place.
75−_ = client comprehensiveAPI
75+_ = client comprehensiveAPIWithoutStreaming
76
76
77spec :: Spec
77spec :: Spec
78spec = describe "Servant.Client" $ do
78spec = describe "Servant.Client" $ do
servant-client/test/Servant/StreamSpec.hs+21−22
@@ -28,20 +28,21 @@ import Control.Monad.Codensity
28import Control.Monad.IO.Class
28import Control.Monad.IO.Class
29 (MonadIO (..))
29 (MonadIO (..))
30import Control.Monad.Trans.Except
30import Control.Monad.Trans.Except
31−import qualified Data.ByteString as BS
31+import qualified Data.ByteString as BS
32import Data.Proxy
32import Data.Proxy
33−import qualified Data.TDigest as TD
33+import qualified Data.TDigest as TD
34−import qualified Network.HTTP.Client as C
34+import qualified Network.HTTP.Client as C
35import Prelude ()
35import Prelude ()
36import Prelude.Compat
36import Prelude.Compat
37import Servant.API
37import Servant.API
38 ((:<|>) ((:<|>)), (:>), JSON, NetstringFraming,
38 ((:<|>) ((:<|>)), (:>), JSON, NetstringFraming,
39 NewlineFraming, NoFraming, OctetStream, SourceIO, StreamGet)
39 NewlineFraming, NoFraming, OctetStream, SourceIO, StreamGet)
40−import Servant.Client
40+import Servant.Client.Streaming
41import Servant.ClientSpec
41import Servant.ClientSpec
42 (Person (..))
42 (Person (..))
43−import qualified Servant.ClientSpec as CS
43+import qualified Servant.ClientSpec as CS
44import Servant.Server
44import Servant.Server
45+import Servant.Test.ComprehensiveAPI
45import Servant.Types.SourceT
46import Servant.Types.SourceT
46import System.Entropy
47import System.Entropy
47 (getEntropy, getHardwareEntropy)
48 (getEntropy, getHardwareEntropy)
@@ -59,8 +60,12 @@ import GHC.Stats
59 (currentBytesUsed, getGCStats)
60 (currentBytesUsed, getGCStats)
60#endif
61#endif
61
62
63+-- This declaration simply checks that all instances are in place.
64+-- Note: this is streaming client
65+_ = client comprehensiveAPI
66+
62spec :: Spec
67spec :: Spec
63−spec = describe "Servant.Stream" $ do
68+spec = describe "Servant.Client.Streaming" $ do
64 streamSpec
69 streamSpec
65
70
66type StreamApi =
71type StreamApi =
@@ -71,8 +76,8 @@ type StreamApi =
71api :: Proxy StreamApi
76api :: Proxy StreamApi
72api = Proxy
77api = Proxy
73
78
74−getGetNL, getGetNS :: ClientM (Codensity IO (SourceIO Person))
79+getGetNL, getGetNS :: ClientM (SourceIO Person)
75−getGetALot :: ClientM (Codensity IO (SourceIO BS.ByteString))
80+getGetALot :: ClientM (SourceIO BS.ByteString)
76getGetNL :<|> getGetNS :<|> getGetALot = client api
81getGetNL :<|> getGetNS :<|> getGetALot = client api
77
82
78alice :: Person
83alice :: Person
@@ -104,28 +109,22 @@ powerOfTwo = (2 ^)
104manager' :: C.Manager
109manager' :: C.Manager
105manager' = unsafePerformIO $ C.newManager C.defaultManagerSettings
110manager' = unsafePerformIO $ C.newManager C.defaultManagerSettings
106
111
107−runClient :: ClientM a -> BaseUrl -> IO (Either ServantError a)
112+withClient :: ClientM a -> BaseUrl -> (Either ServantError a -> IO r) -> IO r
108−runClient x baseUrl' = runClientM x (mkClientEnv manager' baseUrl')
113+withClient x baseUrl' = withClientM x (mkClientEnv manager' baseUrl')
109
114
110−testRunSourceIO :: Codensity IO (SourceIO a)
115+testRunSourceIO :: SourceIO a
111 -> IO (Either String [a])
116 -> IO (Either String [a])
112−testRunSourceIO = runExceptT . runSourceT . joinCodensitySourceT
117+testRunSourceIO = runExceptT . runSourceT
113−
114−joinCodensitySourceT :: Codensity m (SourceT m a) -> SourceT m a
115−joinCodensitySourceT cod =
116− SourceT $ \r ->
117− runCodensity cod $ \src ->
118− unSourceT src r
119
118
120streamSpec :: Spec
119streamSpec :: Spec
121streamSpec = beforeAll (CS.startWaiApp server) $ afterAll CS.endWaiApp $ do
120streamSpec = beforeAll (CS.startWaiApp server) $ afterAll CS.endWaiApp $ do
122 it "works with Servant.API.StreamGet.Newline" $ \(_, baseUrl) -> do
121 it "works with Servant.API.StreamGet.Newline" $ \(_, baseUrl) -> do
123− Right res <- runClient getGetNL baseUrl
122+ withClient getGetNL baseUrl $ \(Right res) ->
124− testRunSourceIO res `shouldReturn` Right [alice, bob, alice]
123+ testRunSourceIO res `shouldReturn` Right [alice, bob, alice]
125
124
126 it "works with Servant.API.StreamGet.Netstring" $ \(_, baseUrl) -> do
125 it "works with Servant.API.StreamGet.Netstring" $ \(_, baseUrl) -> do
127− Right res <- runClient getGetNS baseUrl
126+ withClient getGetNS baseUrl $ \(Right res) ->
128− testRunSourceIO res `shouldReturn` Right [alice, bob, alice]
127+ testRunSourceIO res `shouldReturn` Right [alice, bob, alice]
129
128
130{-
129{-
131 it "streams in constant memory" $ \(_, baseUrl) -> do
130 it "streams in constant memory" $ \(_, baseUrl) -> do
servant-conduit/example/Main.hs+1−1
@@ -27,7 +27,7 @@ import Text.Read
27import Data.Conduit
27import Data.Conduit
28import qualified Data.Conduit.Combinators as C
28import qualified Data.Conduit.Combinators as C
29import Servant
29import Servant
30−import Servant.Client
30+import Servant.Client.Streaming
31import Servant.Conduit ()
31import Servant.Conduit ()
32
32
33import qualified Network.Wai.Handler.Warp as Warp
33import qualified Network.Wai.Handler.Warp as Warp
servant-docs/golden/comprehensive.md+36
@@ -16,6 +16,42 @@
16
16
17```
17```
18
18
19+## GET /alternative/left
20+
21+### Response:
22+
23+- Status code 200
24+- Headers: []
25+
26+- Supported content types are:
27+
28+ - `application/json;charset=utf-8`
29+ - `application/json`
30+
31+- Example (`application/json;charset=utf-8`, `application/json`):
32+
33+```javascript
34+
35+```
36+
37+## GET /alternative/right
38+
39+### Response:
40+
41+- Status code 200
42+- Headers: []
43+
44+- Supported content types are:
45+
46+ - `application/json;charset=utf-8`
47+ - `application/json`
48+
49+- Example (`application/json;charset=utf-8`, `application/json`):
50+
51+```javascript
52+
53+```
54+
19## GET /capture/:foo
55## GET /capture/:foo
20
56
21### Captures:
57### Captures:
servant-machines/example/Main.hs+1−1
@@ -26,7 +26,7 @@ import Text.Read
26
26
27import Data.Machine
27import Data.Machine
28import Servant
28import Servant
29−import Servant.Client
29+import Servant.Client.Streaming
30import Servant.Machines ()
30import Servant.Machines ()
31
31
32import qualified Network.Wai.Handler.Warp as Warp
32import qualified Network.Wai.Handler.Warp as Warp
servant-pipes/example/Main.hs+4−3
@@ -19,18 +19,19 @@ import Network.Wai
19 (Application)
19 (Application)
20import System.Environment
20import System.Environment
21 (getArgs, lookupEnv)
21 (getArgs, lookupEnv)
22−import System.IO (IOMode (..))
22+import System.IO
23+ (IOMode (..))
23import Text.Read
24import Text.Read
24 (readMaybe)
25 (readMaybe)
25
26
26import qualified Pipes as P
27import qualified Pipes as P
28+import Pipes.ByteString as PBS
27import qualified Pipes.Prelude as P
29import qualified Pipes.Prelude as P
28import Pipes.Safe
30import Pipes.Safe
29 (SafeT)
31 (SafeT)
30import qualified Pipes.Safe.Prelude as P
32import qualified Pipes.Safe.Prelude as P
31import Servant
33import Servant
32−import Pipes.ByteString as PBS
33−import Servant.Client
34+import Servant.Client.Streaming
34import Servant.Pipes ()
35import Servant.Pipes ()
35
36
36import qualified Network.Wai.Handler.Warp as Warp
37import qualified Network.Wai.Handler.Warp as Warp
servant/src/Servant/Test/ComprehensiveAPI.hs+53−27
@@ -15,38 +15,64 @@ import Servant.Types.SourceT
15type GET = Get '[JSON] NoContent
15type GET = Get '[JSON] NoContent
16
16
17type ComprehensiveAPI =
17type ComprehensiveAPI =
18− ComprehensiveAPIWithoutRaw :<|>
18+ ComprehensiveAPIWithoutStreamingOrRaw'
19+ (EmptyEndpoint :<|> StreamingEndpoint :<|> RawEndpoint)
20+
21+type RawEndpoint =
19− "raw" :> Raw
22+ "raw" :> Raw
23+
24+type StreamingEndpoint =
25+ "streaming" :> StreamBody NetstringFraming JSON (SourceT IO Int) :> Stream 'GET 200 NetstringFraming JSON (SourceT IO Int)
26+
27+type EmptyEndpoint =
28+ "empty-api" :> EmptyAPI
20
29
21comprehensiveAPI :: Proxy ComprehensiveAPI
30comprehensiveAPI :: Proxy ComprehensiveAPI
22comprehensiveAPI = Proxy
31comprehensiveAPI = Proxy
23
32
24type ComprehensiveAPIWithoutRaw =
33type ComprehensiveAPIWithoutRaw =
25− GET :<|>
34+ ComprehensiveAPIWithoutStreamingOrRaw'
26− "get-int" :> Get '[JSON] Int :<|>
35+ (EmptyEndpoint :<|> StreamingEndpoint)
27− "capture" :> Capture' '[Description "example description"] "foo" Int :> GET :<|>
28− "header" :> Header "foo" Int :> GET :<|>
29− "header-lenient" :> Header' '[Required, Lenient] "bar" Int :> GET :<|>
30− "http-version" :> HttpVersion :> GET :<|>
31− "is-secure" :> IsSecure :> GET :<|>
32− "param" :> QueryParam "foo" Int :> GET :<|>
33− "param-lenient" :> QueryParam' '[Required, Lenient] "bar" Int :> GET :<|>
34− "params" :> QueryParams "foo" Int :> GET :<|>
35− "flag" :> QueryFlag "foo" :> GET :<|>
36− "remote-host" :> RemoteHost :> GET :<|>
37− "req-body" :> ReqBody '[JSON] Int :> GET :<|>
38− "req-body-lenient" :> ReqBody' '[Lenient] '[JSON] Int :> GET :<|>
39− "res-headers" :> Get '[JSON] (Headers '[Header "foo" Int] NoContent) :<|>
40− "foo" :> GET :<|>
41− "vault" :> Vault :> GET :<|>
42− "post-no-content" :> Verb 'POST 204 '[JSON] NoContent :<|>
43− "post-int" :> Verb 'POST 204 '[JSON] Int :<|>
44− "streaming" :> StreamBody NetstringFraming JSON (SourceT IO Int) :> Stream 'GET 200 NetstringFraming JSON (SourceT IO Int) :<|>
45− "named-context" :> WithNamedContext "foo" '[] GET :<|>
46− "capture-all" :> CaptureAll "foo" Int :> GET :<|>
47− "summary" :> Summary "foo" :> GET :<|>
48− "description" :> Description "foo" :> GET :<|>
49− "empty-api" :> EmptyAPI
50
36
51comprehensiveAPIWithoutRaw :: Proxy ComprehensiveAPIWithoutRaw
37comprehensiveAPIWithoutRaw :: Proxy ComprehensiveAPIWithoutRaw
52comprehensiveAPIWithoutRaw = Proxy
38comprehensiveAPIWithoutRaw = Proxy
39+
40+type ComprehensiveAPIWithoutStreaming =
41+ ComprehensiveAPIWithoutStreamingOrRaw'
42+ (EmptyEndpoint :<|> RawEndpoint)
43+
44+comprehensiveAPIWithoutStreaming :: Proxy ComprehensiveAPIWithoutStreaming
45+comprehensiveAPIWithoutStreaming = Proxy
46+
47+-- | @:: API -> API@, so we have linear structure of the API.
48+type ComprehensiveAPIWithoutStreamingOrRaw' endpoint =
49+ GET
50+ :<|> "get-int" :> Get '[JSON] Int
51+ :<|> "capture" :> Capture' '[Description "example description"] "foo" Int :> GET
52+ :<|> "header" :> Header "foo" Int :> GET
53+ :<|> "header-lenient" :> Header' '[Required, Lenient] "bar" Int :> GET
54+ :<|> "http-version" :> HttpVersion :> GET
55+ :<|> "is-secure" :> IsSecure :> GET
56+ :<|> "param" :> QueryParam "foo" Int :> GET
57+ :<|> "param-lenient" :> QueryParam' '[Required, Lenient] "bar" Int :> GET
58+ :<|> "params" :> QueryParams "foo" Int :> GET
59+ :<|> "flag" :> QueryFlag "foo" :> GET
60+ :<|> "remote-host" :> RemoteHost :> GET
61+ :<|> "req-body" :> ReqBody '[JSON] Int :> GET
62+ :<|> "req-body-lenient" :> ReqBody' '[Lenient] '[JSON] Int :> GET
63+ :<|> "res-headers" :> Get '[JSON] (Headers '[Header "foo" Int] NoContent)
64+ :<|> "foo" :> GET
65+ :<|> "vault" :> Vault :> GET
66+ :<|> "post-no-content" :> Verb 'POST 204 '[JSON] NoContent
67+ :<|> "post-int" :> Verb 'POST 204 '[JSON] Int
68+ :<|> "named-context" :> WithNamedContext "foo" '[] GET
69+ :<|> "capture-all" :> CaptureAll "foo" Int :> GET
70+ :<|> "summary" :> Summary "foo" :> GET
71+ :<|> "description" :> Description "foo" :> GET
72+ :<|> "alternative" :> ("left" :> GET :<|> "right" :> GET)
73+ :<|> endpoint
74+
75+type ComprehensiveAPIWithoutStreamingOrRaw = ComprehensiveAPIWithoutStreamingOrRaw' EmptyEndpoint
76+
77+comprehensiveAPIWithoutStreamingOrRaw :: Proxy ComprehensiveAPIWithoutStreamingOrRaw
78+comprehensiveAPIWithoutStreamingOrRaw = Proxy
servant/test/Servant/LinksSpec.hs+1−1
@@ -89,7 +89,7 @@ spec = describe "Servant.Links" $ do
89 allNames ["Seneca", "Aurelius"] `shouldBeLink` "all/Seneca/Aurelius"
89 allNames ["Seneca", "Aurelius"] `shouldBeLink` "all/Seneca/Aurelius"
90
90
91 it "can generate all links for ComprehensiveAPIWithoutRaw" $ do
91 it "can generate all links for ComprehensiveAPIWithoutRaw" $ do
92− let (firstLink :<|> _) = allLinks comprehensiveAPIWithoutRaw
92+ let firstLink :<|> _ = allLinks comprehensiveAPIWithoutRaw
93 firstLink `shouldBeLink` ""
93 firstLink `shouldBeLink` ""
94
94
95-- |
95-- |