git.iko.soy/typeable/servant
on master help sign in
extract module for http-client
commit26656935299643d6f566bae8eda74ece26388f24 authorArnaud Bailly <arnaud.oqube@gmail.com> dateMon 28 Aug 2017, 18:36 (9 years ago) parent97aa7db
5 files changed+134−103
servant-client/servant-client.cabal+1 servant-client/src/Servant/Client.hs+1 servant-client/src/Servant/Client/HttpClient.hs+130 servant-client/src/Servant/Common/Req.hs+1−103 servant-client/test/Servant/ClientSpec.hs+1
servant-client/servant-client.cabal+1
@@ -30,6 +30,7 @@ source-repository head
3030library
3131 exposed-modules:
3232 Servant.Client
33+ Servant.Client.HttpClient
3334 Servant.Client.Generic
3435 Servant.Client.Experimental.Auth
3536 Servant.Common.BaseUrl
servant-client/src/Servant/Client.hs+1
@@ -42,6 +42,7 @@ import Prelude ()
4242import Prelude.Compat
4343import Servant.API
4444import Servant.Client.Experimental.Auth
45+import Servant.Client.HttpClient
4546import Servant.Common.BaseUrl
4647import Servant.Common.BasicAuth
4748import Servant.Common.Req
servant-client/src/Servant/Client/HttpClient.hsnew file+130
@@ -0,0 +1,130 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DeriveDataTypeable #-}
3+{-# LANGUAGE DeriveGeneric #-}
4+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
5+{-# LANGUAGE MultiParamTypeClasses #-}
6+{-# LANGUAGE TypeFamilies #-}
7+{-# LANGUAGE OverloadedStrings #-}
8+{-# LANGUAGE ScopedTypeVariables #-}
9+
10+{-| http-client based client requests executor -}
11+module Servant.Client.HttpClient where
12+
13+
14+import Prelude ()
15+import Prelude.Compat
16+
17+import Control.Exception
18+import Control.Monad
19+import Control.Monad.Catch (MonadThrow, MonadCatch)
20+import Data.Foldable (toList)
21+import Data.Functor.Alt (Alt (..))
22+
23+import Control.Monad.Error.Class (MonadError(..))
24+import Control.Monad.Trans.Except
25+
26+import GHC.Generics
27+import Control.Monad.Base (MonadBase (..))
28+import Control.Monad.IO.Class ()
29+import Control.Monad.Reader
30+import Control.Monad.Trans.Control (MonadBaseControl (..))
31+import Data.ByteString.Lazy hiding (pack, filter, map, null, elem, any)
32+import Data.String.Conversions (cs)
33+import Data.Proxy
34+import Network.HTTP.Media
35+import Network.HTTP.Types
36+import Network.HTTP.Client hiding (Proxy, path)
37+import qualified Network.HTTP.Types.Header as HTTP
38+import Servant.API.ContentTypes
39+import Servant.Common.BaseUrl
40+import Servant.Common.Req
41+
42+import qualified Network.HTTP.Client as Client
43+
44+
45+data ClientEnv
46+ = ClientEnv
47+ { manager :: Manager
48+ , baseUrl :: BaseUrl
49+ }
50+
51+
52+-- | @ClientM@ is the monad in which client functions run. Contains the
53+-- 'Manager' and 'BaseUrl' used for requests in the reader environment.
54+
55+newtype ClientM a = ClientM { runClientM' :: ReaderT ClientEnv (ExceptT ServantError IO) a }
56+ deriving ( Functor, Applicative, Monad, MonadIO, Generic
57+ , MonadReader ClientEnv
58+ , MonadError ServantError
59+ , MonadThrow, MonadCatch
60+ )
61+
62+instance MonadBase IO ClientM where
63+ liftBase = ClientM . liftBase
64+
65+instance MonadBaseControl IO ClientM where
66+ type StM ClientM a = Either ServantError a
67+
68+ -- liftBaseWith :: (RunInBase ClientM IO -> IO a) -> ClientM a
69+ liftBaseWith f = ClientM (liftBaseWith (\g -> f (g . runClientM')))
70+
71+ -- restoreM :: StM ClientM a -> ClientM a
72+ restoreM st = ClientM (restoreM st)
73+
74+-- | Try clients in order, last error is preserved.
75+instance Alt ClientM where
76+ a <!> b = a `catchError` \_ -> b
77+
78+runClientM :: ClientM a -> ClientEnv -> IO (Either ServantError a)
79+runClientM cm env = runExceptT $ (flip runReaderT env) $ runClientM' cm
80+
81+
82+performRequest :: Method -> Req
83+ -> ClientM ( Int, ByteString, MediaType
84+ , [HTTP.Header], Response ByteString)
85+performRequest reqMethod req = do
86+ m <- asks manager
87+ reqHost <- asks baseUrl
88+ partialRequest <- liftIO $ reqToRequest req reqHost
89+
90+ let request = partialRequest { Client.method = reqMethod }
91+
92+ eResponse <- liftIO $ catchConnectionError $ Client.httpLbs request m
93+ case eResponse of
94+ Left err ->
95+ throwError . ConnectionError $ SomeException err
96+
97+ Right response -> do
98+ let status = Client.responseStatus response
99+ body = Client.responseBody response
100+ hdrs = Client.responseHeaders response
101+ status_code = statusCode status
102+ ct <- case lookup "Content-Type" $ Client.responseHeaders response of
103+ Nothing -> pure $ "application"//"octet-stream"
104+ Just t -> case parseAccept t of
105+ Nothing -> throwError $ InvalidContentTypeHeader (cs t) body
106+ Just t' -> pure t'
107+ unless (status_code >= 200 && status_code < 300) $
108+ throwError $ FailureResponse (UrlReq reqHost req) status ct body
109+ return (status_code, body, ct, hdrs, response)
110+
111+performRequestCT :: MimeUnrender ct result => Proxy ct -> Method -> Req
112+ -> ClientM ([HTTP.Header], result)
113+performRequestCT ct reqMethod req = do
114+ let acceptCTS = contentTypes ct
115+ (_status, respBody, respCT, hdrs, _response) <-
116+ performRequest reqMethod (req { reqAccept = toList acceptCTS })
117+ unless (any (matches respCT) acceptCTS) $ throwError $ UnsupportedContentType respCT respBody
118+ case mimeUnrender ct respBody of
119+ Left err -> throwError $ DecodeFailure err respCT respBody
120+ Right val -> return (hdrs, val)
121+
122+performRequestNoBody :: Method -> Req -> ClientM [HTTP.Header]
123+performRequestNoBody reqMethod req = do
124+ (_status, _body, _ct, hdrs, _response) <- performRequest reqMethod req
125+ return hdrs
126+
127+catchConnectionError :: IO a -> IO (Either ServantError a)
128+catchConnectionError action =
129+ catch (Right <$> action) $ \e ->
130+ pure . Left . ConnectionError $ SomeException (e :: HttpException)
servant-client/src/Servant/Common/Req.hs+1−103
@@ -13,38 +13,23 @@ import Prelude ()
1313import Prelude.Compat
1414
1515import Control.Exception
16−import Control.Monad
17−import Control.Monad.Catch (MonadThrow, MonadCatch)
18−import Data.Foldable (toList)
19−import Data.Functor.Alt (Alt (..))
16+import Control.Monad.Catch (MonadThrow)
2017import Data.Semigroup ((<>))
2118
22−import Control.Monad.Error.Class (MonadError(..))
23−import Control.Monad.Trans.Except
24−
25−import GHC.Generics
26−import Control.Monad.Base (MonadBase (..))
2719import Control.Monad.IO.Class ()
28−import Control.Monad.Reader
29−import Control.Monad.Trans.Control (MonadBaseControl (..))
3020import qualified Data.ByteString.Builder as BS
3121import Data.ByteString.Lazy hiding (pack, filter, map, null, elem, any)
3222import Data.String
3323import Data.String.Conversions (cs)
34−import Data.Proxy
3524import Data.Text (Text)
3625import Data.Text.Encoding
3726import Data.Typeable
3827import Network.HTTP.Media
3928import Network.HTTP.Types
4029import Network.HTTP.Client hiding (Proxy, path)
41−import qualified Network.HTTP.Types.Header as HTTP
4230import Network.URI hiding (path)
43−import Servant.API.ContentTypes
4431import Servant.Common.BaseUrl
4532
46−import qualified Network.HTTP.Client as Client
47−
4833import Web.HttpApiData
4934
5035data ServantError
@@ -196,90 +181,3 @@ parseRequest url = liftM disableStatusCheck (parseUrl url)
196181
197182displayHttpRequest :: Method -> String
198183displayHttpRequest httpmethod = "HTTP " ++ cs httpmethod ++ " request"
199−
200−data ClientEnv
201− = ClientEnv
202− { manager :: Manager
203− , baseUrl :: BaseUrl
204− }
205−
206−
207−-- | @ClientM@ is the monad in which client functions run. Contains the
208−-- 'Manager' and 'BaseUrl' used for requests in the reader environment.
209−
210−newtype ClientM a = ClientM { runClientM' :: ReaderT ClientEnv (ExceptT ServantError IO) a }
211− deriving ( Functor, Applicative, Monad, MonadIO, Generic
212− , MonadReader ClientEnv
213− , MonadError ServantError
214− , MonadThrow, MonadCatch
215− )
216−
217−instance MonadBase IO ClientM where
218− liftBase = ClientM . liftBase
219−
220−instance MonadBaseControl IO ClientM where
221− type StM ClientM a = Either ServantError a
222−
223− -- liftBaseWith :: (RunInBase ClientM IO -> IO a) -> ClientM a
224− liftBaseWith f = ClientM (liftBaseWith (\g -> f (g . runClientM')))
225−
226− -- restoreM :: StM ClientM a -> ClientM a
227− restoreM st = ClientM (restoreM st)
228−
229−-- | Try clients in order, last error is preserved.
230−instance Alt ClientM where
231− a <!> b = a `catchError` \_ -> b
232−
233−runClientM :: ClientM a -> ClientEnv -> IO (Either ServantError a)
234−runClientM cm env = runExceptT $ (flip runReaderT env) $ runClientM' cm
235−
236−
237−performRequest :: Method -> Req
238− -> ClientM ( Int, ByteString, MediaType
239− , [HTTP.Header], Response ByteString)
240−performRequest reqMethod req = do
241− m <- asks manager
242− reqHost <- asks baseUrl
243− partialRequest <- liftIO $ reqToRequest req reqHost
244−
245− let request = partialRequest { Client.method = reqMethod }
246−
247− eResponse <- liftIO $ catchConnectionError $ Client.httpLbs request m
248− case eResponse of
249− Left err ->
250− throwError . ConnectionError $ SomeException err
251−
252− Right response -> do
253− let status = Client.responseStatus response
254− body = Client.responseBody response
255− hdrs = Client.responseHeaders response
256− status_code = statusCode status
257− ct <- case lookup "Content-Type" $ Client.responseHeaders response of
258− Nothing -> pure $ "application"//"octet-stream"
259− Just t -> case parseAccept t of
260− Nothing -> throwError $ InvalidContentTypeHeader (cs t) body
261− Just t' -> pure t'
262− unless (status_code >= 200 && status_code < 300) $
263− throwError $ FailureResponse (UrlReq reqHost req) status ct body
264− return (status_code, body, ct, hdrs, response)
265−
266−performRequestCT :: MimeUnrender ct result => Proxy ct -> Method -> Req
267− -> ClientM ([HTTP.Header], result)
268−performRequestCT ct reqMethod req = do
269− let acceptCTS = contentTypes ct
270− (_status, respBody, respCT, hdrs, _response) <-
271− performRequest reqMethod (req { reqAccept = toList acceptCTS })
272− unless (any (matches respCT) acceptCTS) $ throwError $ UnsupportedContentType respCT respBody
273− case mimeUnrender ct respBody of
274− Left err -> throwError $ DecodeFailure err respCT respBody
275− Right val -> return (hdrs, val)
276−
277−performRequestNoBody :: Method -> Req -> ClientM [HTTP.Header]
278−performRequestNoBody reqMethod req = do
279− (_status, _body, _ct, hdrs, _response) <- performRequest reqMethod req
280− return hdrs
281−
282−catchConnectionError :: IO a -> IO (Either ServantError a)
283−catchConnectionError action =
284− catch (Right <$> action) $ \e ->
285− pure . Left . ConnectionError $ SomeException (e :: HttpException)
servant-client/test/Servant/ClientSpec.hs+1
@@ -58,6 +58,7 @@ import Servant.API.Internal.Test.ComprehensiveAPI
5858import Servant.Client
5959import Servant.Client.Generic
6060import qualified Servant.Common.Req as SCR
61+import qualified Servant.Client.HttpClient as SCR
6162import Servant.Server
6263import Servant.Server.Experimental.Auth
6364