git.iko.soy/typeable/servant
on master help sign in
Separate Servant.Client.Streaming

- as a bonus only `servant-client` depends on `kan-extensions`

commit8feda81fcd3ad2735df70ff25e2755e552b3dcbe authorOleg Grenrus <oleg.grenrus@iki.fi> dateThu 1 Nov 2018, 19:42 (8 years ago) parent05d0f7e
21 files changed+352−129
doc/cookbook/basic-streaming/Streaming.lhs+1−1 doc/tutorial/Client.lhs+12−12 doc/tutorial/tutorial.cabal−1 servant-client-core/servant-client-core.cabal−1 servant-client-core/src/Servant/Client/Core.hs+2 servant-client-core/src/Servant/Client/Core/Internal/HasClient.hs+18−23 servant-client-core/src/Servant/Client/Core/Internal/Request.hs+1−3 servant-client-core/src/Servant/Client/Core/Internal/RunClient.hs+9−2 servant-client/servant-client.cabal+2 servant-client/src/Servant/Client.hs−1 servant-client/src/Servant/Client/Internal/HttpClient.hs−29 servant-client/src/Servant/Client/Internal/HttpClient/Streaming.hs+172 servant-client/src/Servant/Client/Streaming.hs+17 servant-client/test/Servant/ClientSpec.hs+1−1 servant-client/test/Servant/StreamSpec.hs+21−22 servant-conduit/example/Main.hs+1−1 servant-docs/golden/comprehensive.md+36 servant-machines/example/Main.hs+1−1 servant-pipes/example/Main.hs+4−3 servant/src/Servant/Test/ComprehensiveAPI.hs+53−27 servant/test/Servant/LinksSpec.hs+1−1
doc/cookbook/basic-streaming/Streaming.lhs+1−1
@@ -38,7 +38,7 @@ import Text.Read
3838 (readMaybe)
3939
4040import Servant
41−import Servant.Client
41+import Servant.Client.Streaming
4242import qualified Servant.Types.SourceT as S
4343
4444import qualified Network.Wai.Handler.Warp as Warp
doc/tutorial/Client.lhs+12−12
@@ -21,7 +21,8 @@ import Network.HTTP.Client (newManager, defaultManagerSettings)
2121import Servant.API
2222import Servant.Client
2323import Servant.Types.SourceT (foreach)
24−import Control.Monad.Codensity (Codensity)
24+
25+import qualified Servant.Client.Streaming as S
2526```
2627
2728Also, we need examples for some domain specific data types:
@@ -224,6 +225,8 @@ type StreamAPI = "positionStream" :> StreamGet NewlineFraming JSON (SourceIO Pos
224225
225226Note that we use the same `SourceIO` type as on the server-side
226227(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).
227230
228231In any case, here's how we write a function to query our API:
229232
@@ -231,21 +234,18 @@ In any case, here's how we write a function to query our API:
231234streamAPI :: Proxy StreamAPI
232235streamAPI = Proxy
233236
234−posStream :: ClientM (Codensity IO (SourceIO Position))
235−posStream = client streamAPI
237+posStream :: S.ClientM (SourceIO Position)
238+posStream = S.client streamAPI
236239```
237240
238−We'll get back a `Codensity IO (SourceIO Position)`. The wrapping in
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−open only until the inner functions exits. Inside the block we can e.g just
243−print out all elements from a `SourceIO`, to give some idea of how to work with
244−them.
241+We'll get back a `SourceIO Position`. Instead of `runClientM`, the streaming
242+client provides `withClientM`: the underlying HTTP connection is open only
243+until the inner functions exits. Inside the block we can e.g just print out
244+all elements from a `SourceIO`, to give some idea of how to work with them.
245245
246246``` haskell
247−printSourceIO :: Show a => ClientEnv -> ClientM (Codensity IO (SourceIO a)) -> IO ()
248−printSourceIO env c = withClientM c env $ \e -> case e of
247+printSourceIO :: Show a => ClientEnv -> S.ClientM (SourceIO a) -> IO ()
248+printSourceIO env c = S.withClientM c env $ \e -> case e of
249249 Left err -> putStrLn $ "Error: " ++ show err
250250 Right rs -> foreach fail print rs
251251```
doc/tutorial/tutorial.cabal−1
@@ -46,7 +46,6 @@ library
4646 , http-client
4747 , http-media
4848 , http-types
49− , kan-extensions
5049 , mtl
5150 , string-conversions
5251 , text
servant-client-core/servant-client-core.cabal−1
@@ -70,7 +70,6 @@ library
7070 , http-api-data >= 0.3.8.1 && < 0.4
7171 , http-media >= 0.7.1.2 && < 0.8
7272 , http-types >= 0.12.1 && < 0.13
73− , kan-extensions >= 5.2 && < 5.3
7473 , network-uri >= 2.6.1.0 && < 2.7
7574 , safe >= 0.3.17 && < 0.4
7675
servant-client-core/src/Servant/Client/Core.hs+2
@@ -44,6 +44,8 @@ module Servant.Client.Core
4444 , GenResponse (..)
4545 , RunClient(..)
4646 , module Servant.Client.Core.Internal.BaseUrl
47+ -- ** Streaming
48+ , RunStreamingClient(..)
4749 , StreamingResponse
4850
4951 -- * 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
3636import Servant.API
3737 ((:<|>) ((:<|>)), (:>), AuthProtect, BasicAuth, BasicAuthData,
3838 BuildHeadersTo (..), Capture', CaptureAll, Description,
39− EmptyAPI, FramingUnrender (..), FromSourceIO (..),
40− Header', Headers (..), HttpVersion, IsSecure,
41− MimeRender (mimeRender), MimeUnrender (mimeUnrender),
42− NoContent (NoContent), QueryFlag, QueryParam', QueryParams,
43− Raw, ReflectMethod (..), RemoteHost, ReqBody', SBoolI, Stream,
44− StreamBody, Summary, ToHttpApiData, Vault, Verb,
45− WithNamedContext, contentType, getHeadersHList, getResponse,
46− toQueryParam, toUrlPiece)
39+ EmptyAPI, FramingUnrender (..), FromSourceIO (..), Header',
40+ Headers (..), HttpVersion, IsSecure, MimeRender (mimeRender),
41+ MimeUnrender (mimeUnrender), NoContent (NoContent), QueryFlag,
42+ QueryParam', QueryParams, Raw, ReflectMethod (..), RemoteHost,
43+ ReqBody', SBoolI, Stream, StreamBody, Summary, ToHttpApiData,
44+ Vault, Verb, WithNamedContext, contentType, getHeadersHList,
45+ getResponse, toQueryParam, toUrlPiece)
4746import Servant.API.ContentTypes
4847 (contentTypes)
4948import Servant.API.Modifiers
5049 (FoldRequired, RequiredArgument, foldRequiredArgument)
51−import Control.Monad.Codensity
52− (Codensity (..))
5350import qualified Servant.Types.SourceT as S
5451
5552import Servant.Client.Core.Internal.Auth
@@ -272,25 +269,23 @@ instance {-# OVERLAPPING #-}
272269 hoistClientMonad _ _ f ma = f ma
273270
274271instance {-# OVERLAPPABLE #-}
275− ( RunClient m, MimeUnrender ct chunk, ReflectMethod method,
272+ ( RunStreamingClient m, MimeUnrender ct chunk, ReflectMethod method,
276273 FramingUnrender framing, FromSourceIO chunk a
277274 ) => HasClient m (Stream method status framing ct a) where
278275
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
280277
281278 hoistClientMonad _ _ f ma = f ma
282279
283− clientWithRoute _pm Proxy req = 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
290− framingUnrender' = framingUnrender (Proxy :: Proxy framing) mimeUnrender'
291− gres <- sresp
292− return $ fromSourceIO $ framingUnrender' $ S.fromAction BS.null (responseBody gres)
293−
280+ clientWithRoute _pm Proxy req = withStreamingRequest req' $ \gres -> do
281+ let mimeUnrender' = mimeUnrender (Proxy :: Proxy ct) :: BL.ByteString -> Either String chunk
282+ framingUnrender' = framingUnrender (Proxy :: Proxy framing) mimeUnrender'
283+ return $ fromSourceIO $ framingUnrender' $ S.fromAction BS.null (responseBody gres)
284+ where
285+ req' = req
286+ { requestAccept = fromList [contentType (Proxy :: Proxy ct)]
287+ , requestMethod = reflectMethod (Proxy :: Proxy method)
288+ }
294289
295290-- | If you use a 'Header' in one of your endpoints in your API,
296291-- 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
3838import Network.HTTP.Types
3939 (Header, HeaderName, HttpVersion, Method, QueryItem, Status,
4040 http11, methodGet)
41−import Control.Monad.Codensity
42− (Codensity (..))
4341import Web.HttpApiData
4442 (ToHttpApiData, toEncodedUrlPiece, toHeader)
4543
@@ -91,7 +89,7 @@ data GenResponse a = Response
9189 } deriving (Eq, Show, Generic, Typeable, Functor, Foldable, Traversable)
9290
9391type Response = GenResponse LBS.ByteString
94−type StreamingResponse = Codensity IO (GenResponse (IO BS.ByteString))
92+type StreamingResponse = GenResponse (IO BS.ByteString)
9593
9694-- A GET request to the top-level path
9795defaultRequest :: Request
servant-client-core/src/Servant/Client/Core/Internal/RunClient.hs+9−2
@@ -31,9 +31,11 @@ import Servant.Client.Core.Internal.Request
3131class Monad m => RunClient m where
3232 -- | How to make a request.
3333 runRequest :: Request -> m Response
34− streamingRequest :: Request -> m StreamingResponse
3534 throwServantError :: ServantError -> m a
3635
36+class RunClient m => RunStreamingClient m where
37+ withStreamingRequest :: Request -> (StreamingResponse -> IO a) -> m a
38+
3739checkContentTypeHeader :: RunClient m => Response -> m MediaType
3840checkContentTypeHeader response =
3941 case lookup "Content-Type" $ toList $ responseHeaders response of
@@ -56,5 +58,10 @@ decodedAs response contentType = do
5658
5759instance ClientF ~ f => RunClient (Free f) where
5860 runRequest req = liftF (RunRequest req id)
59− streamingRequest req = liftF (StreamingRequest req id)
6061 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
3333library
3434 exposed-modules:
3535 Servant.Client
36+ Servant.Client.Streaming
3637 Servant.Client.Internal.HttpClient
38+ Servant.Client.Internal.HttpClient.Streaming
3739
3840 -- Bundled with GHC: Lower bound to not force re-installs
3941 -- text and mtl are bundled starting with GHC-8.4
servant-client/src/Servant/Client.hs−1
@@ -7,7 +7,6 @@ module Servant.Client
77 ( client
88 , ClientM
99 , runClientM
10− , withClientM
1110 , ClientEnv(..)
1211 , mkClientEnv
1312 , hoistClient
servant-client/src/Servant/Client/Internal/HttpClient.hs−29
@@ -21,8 +21,6 @@ import Control.Monad.Base
2121 (MonadBase (..))
2222import Control.Monad.Catch
2323 (MonadCatch, MonadThrow)
24−import Control.Monad.Codensity
25− (Codensity (..))
2624import Control.Monad.Error.Class
2725 (MonadError (..))
2826import Control.Monad.Reader
@@ -134,7 +132,6 @@ instance Alt ClientM where
134132
135133instance RunClient ClientM where
136134 runRequest = performRequest
137− streamingRequest = performStreamingRequest
138135 throwServantError = throwError
139136
140137instance ClientLike (ClientM a) (ClientM a) where
@@ -143,17 +140,6 @@ instance ClientLike (ClientM a) (ClientM a) where
143140runClientM :: ClientM a -> ClientEnv -> IO (Either ServantError a)
144141runClientM cm env = runExceptT $ flip runReaderT env $ unClientM cm
145142
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−
157143performRequest :: Request -> ClientM Response
158144performRequest req = do
159145 ClientEnv m burl cookieJar' <- ask
@@ -186,21 +172,6 @@ performRequest req = do
186172 throwError $ FailureResponse ourResponse
187173 return ourResponse
188174
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−
204175clientResponseToResponse :: Client.Response a -> GenResponse a
205176clientResponseToResponse r = Response
206177 { 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
7272import Servant.Server.Experimental.Auth
7373
7474-- This declaration simply checks that all instances are in place.
75−_ = client comprehensiveAPI
75+_ = client comprehensiveAPIWithoutStreaming
7676
7777spec :: Spec
7878spec = describe "Servant.Client" $ do
servant-client/test/Servant/StreamSpec.hs+21−22
@@ -28,20 +28,21 @@ import Control.Monad.Codensity
2828import Control.Monad.IO.Class
2929 (MonadIO (..))
3030import Control.Monad.Trans.Except
31−import qualified Data.ByteString as BS
31+import qualified Data.ByteString as BS
3232import Data.Proxy
33−import qualified Data.TDigest as TD
34−import qualified Network.HTTP.Client as C
33+import qualified Data.TDigest as TD
34+import qualified Network.HTTP.Client as C
3535import Prelude ()
3636import Prelude.Compat
3737import Servant.API
3838 ((:<|>) ((:<|>)), (:>), JSON, NetstringFraming,
3939 NewlineFraming, NoFraming, OctetStream, SourceIO, StreamGet)
40−import Servant.Client
40+import Servant.Client.Streaming
4141import Servant.ClientSpec
4242 (Person (..))
43−import qualified Servant.ClientSpec as CS
43+import qualified Servant.ClientSpec as CS
4444import Servant.Server
45+import Servant.Test.ComprehensiveAPI
4546import Servant.Types.SourceT
4647import System.Entropy
4748 (getEntropy, getHardwareEntropy)
@@ -59,8 +60,12 @@ import GHC.Stats
5960 (currentBytesUsed, getGCStats)
6061#endif
6162
63+-- This declaration simply checks that all instances are in place.
64+-- Note: this is streaming client
65+_ = client comprehensiveAPI
66+
6267spec :: Spec
63−spec = describe "Servant.Stream" $ do
68+spec = describe "Servant.Client.Streaming" $ do
6469 streamSpec
6570
6671type StreamApi =
@@ -71,8 +76,8 @@ type StreamApi =
7176api :: Proxy StreamApi
7277api = Proxy
7378
74−getGetNL, getGetNS :: ClientM (Codensity IO (SourceIO Person))
75−getGetALot :: ClientM (Codensity IO (SourceIO BS.ByteString))
79+getGetNL, getGetNS :: ClientM (SourceIO Person)
80+getGetALot :: ClientM (SourceIO BS.ByteString)
7681getGetNL :<|> getGetNS :<|> getGetALot = client api
7782
7883alice :: Person
@@ -104,28 +109,22 @@ powerOfTwo = (2 ^)
104109manager' :: C.Manager
105110manager' = unsafePerformIO $ C.newManager C.defaultManagerSettings
106111
107−runClient :: ClientM a -> BaseUrl -> IO (Either ServantError a)
108−runClient x baseUrl' = runClientM x (mkClientEnv manager' baseUrl')
112+withClient :: ClientM a -> BaseUrl -> (Either ServantError a -> IO r) -> IO r
113+withClient x baseUrl' = withClientM x (mkClientEnv manager' baseUrl')
109114
110−testRunSourceIO :: Codensity IO (SourceIO a)
115+testRunSourceIO :: SourceIO a
111116 -> IO (Either String [a])
112−testRunSourceIO = runExceptT . runSourceT . joinCodensitySourceT
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
117+testRunSourceIO = runExceptT . runSourceT
119118
120119streamSpec :: Spec
121120streamSpec = beforeAll (CS.startWaiApp server) $ afterAll CS.endWaiApp $ do
122121 it "works with Servant.API.StreamGet.Newline" $ \(_, baseUrl) -> do
123− Right res <- runClient getGetNL baseUrl
124− testRunSourceIO res `shouldReturn` Right [alice, bob, alice]
122+ withClient getGetNL baseUrl $ \(Right res) ->
123+ testRunSourceIO res `shouldReturn` Right [alice, bob, alice]
125124
126125 it "works with Servant.API.StreamGet.Netstring" $ \(_, baseUrl) -> do
127− Right res <- runClient getGetNS baseUrl
128− testRunSourceIO res `shouldReturn` Right [alice, bob, alice]
126+ withClient getGetNS baseUrl $ \(Right res) ->
127+ testRunSourceIO res `shouldReturn` Right [alice, bob, alice]
129128
130129{-
131130 it "streams in constant memory" $ \(_, baseUrl) -> do
servant-conduit/example/Main.hs+1−1
@@ -27,7 +27,7 @@ import Text.Read
2727import Data.Conduit
2828import qualified Data.Conduit.Combinators as C
2929import Servant
30−import Servant.Client
30+import Servant.Client.Streaming
3131import Servant.Conduit ()
3232
3333import qualified Network.Wai.Handler.Warp as Warp
servant-docs/golden/comprehensive.md+36
@@ -16,6 +16,42 @@
1616
1717```
1818
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+
1955## GET /capture/:foo
2056
2157### Captures:
servant-machines/example/Main.hs+1−1
@@ -26,7 +26,7 @@ import Text.Read
2626
2727import Data.Machine
2828import Servant
29−import Servant.Client
29+import Servant.Client.Streaming
3030import Servant.Machines ()
3131
3232import qualified Network.Wai.Handler.Warp as Warp
servant-pipes/example/Main.hs+4−3
@@ -19,18 +19,19 @@ import Network.Wai
1919 (Application)
2020import System.Environment
2121 (getArgs, lookupEnv)
22−import System.IO (IOMode (..))
22+import System.IO
23+ (IOMode (..))
2324import Text.Read
2425 (readMaybe)
2526
2627import qualified Pipes as P
28+import Pipes.ByteString as PBS
2729import qualified Pipes.Prelude as P
2830import Pipes.Safe
2931 (SafeT)
3032import qualified Pipes.Safe.Prelude as P
3133import Servant
32−import Pipes.ByteString as PBS
33−import Servant.Client
34+import Servant.Client.Streaming
3435import Servant.Pipes ()
3536
3637import qualified Network.Wai.Handler.Warp as Warp
servant/src/Servant/Test/ComprehensiveAPI.hs+53−27
@@ -15,38 +15,64 @@ import Servant.Types.SourceT
1515type GET = Get '[JSON] NoContent
1616
1717type ComprehensiveAPI =
18− ComprehensiveAPIWithoutRaw :<|>
19− "raw" :> Raw
18+ ComprehensiveAPIWithoutStreamingOrRaw'
19+ (EmptyEndpoint :<|> StreamingEndpoint :<|> RawEndpoint)
20+
21+type RawEndpoint =
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
2029
2130comprehensiveAPI :: Proxy ComprehensiveAPI
2231comprehensiveAPI = Proxy
2332
2433type ComprehensiveAPIWithoutRaw =
25− GET :<|>
26− "get-int" :> Get '[JSON] Int :<|>
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
34+ ComprehensiveAPIWithoutStreamingOrRaw'
35+ (EmptyEndpoint :<|> StreamingEndpoint)
5036
5137comprehensiveAPIWithoutRaw :: Proxy ComprehensiveAPIWithoutRaw
5238comprehensiveAPIWithoutRaw = 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
8989 allNames ["Seneca", "Aurelius"] `shouldBeLink` "all/Seneca/Aurelius"
9090
9191 it "can generate all links for ComprehensiveAPIWithoutRaw" $ do
92− let (firstLink :<|> _) = allLinks comprehensiveAPIWithoutRaw
92+ let firstLink :<|> _ = allLinks comprehensiveAPIWithoutRaw
9393 firstLink `shouldBeLink` ""
9494
9595-- |