1 {-# LANGUAGE DataKinds #-}
2 {-# LANGUAGE DeriveFunctor #-}
3 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
4 {-# LANGUAGE OverloadedStrings #-}
5 {-# LANGUAGE Rank2Types #-}
6 {-# LANGUAGE StrictData #-}
7 {-# LANGUAGE TypeFamilies #-}
8 {-# OPTIONS_GHC -fno-warn-orphans #-}
9 module Language.Symantic.HTTP.Client where
11 import Control.Applicative (Applicative(..), Alternative(..))
12 import Control.Monad (Monad(..), unless)
13 import Control.Monad.Trans.Class (MonadTrans(..))
15 import Data.Default.Class (Default(..))
16 import Data.Either (Either(..))
17 import Data.Eq (Eq(..))
18 import Data.Foldable (null, for_, toList, any)
19 import Data.Function (($), (.), on)
20 import Data.Functor (Functor, (<$>))
21 import Data.Maybe (Maybe(..), maybe)
22 import Data.Ord (Ord(..))
23 import Data.Proxy (Proxy(..))
24 import Data.Semigroup (Semigroup(..))
25 import Data.Sequence (Seq)
26 import Data.String (IsString(..))
27 import Data.Text (Text)
28 import Data.Tuple (fst)
30 import Text.Read (readMaybe)
31 import Text.Show (Show(..))
32 import qualified Control.Concurrent.STM as STM
33 import qualified Control.Exception as Exn
34 import qualified Control.Monad.Classes as MC
35 import qualified Control.Monad.Trans.Except as E
36 import qualified Control.Monad.Trans.Reader as R
37 import qualified Data.ByteString as BS
38 import qualified Data.ByteString.Builder as BSB
39 import qualified Data.ByteString.Lazy as BSL
40 import qualified Data.List as List
41 import qualified Data.Sequence as Seq
42 import qualified Data.Text as T
43 import qualified Data.Time.Clock as Time
44 import qualified Network.HTTP.Client as Client
45 import qualified Network.HTTP.Media as Media
46 import qualified Network.HTTP.Types as HTTP
47 import qualified Network.URI as URI
49 import Language.Symantic.HTTP.Media
50 import Language.Symantic.HTTP.Mime
51 import Language.Symantic.HTTP.URI
54 newtype Client a = Client { unClient :: R.ReaderT ClientEnv (E.ExceptT ClientError IO) a }
55 deriving (Functor, Applicative, Monad)
56 type instance MC.CanDo Client (MC.EffReader ClientEnv) = 'True
57 type instance MC.CanDo Client (MC.EffExcept ClientError) = 'True
58 type instance MC.CanDo Client (MC.EffExec IO) = 'True
59 instance MC.MonadExceptN 'MC.Zero ClientError Client where
60 throwN px = Client . lift . MC.throwN px
61 instance MC.MonadReaderN 'MC.Zero ClientEnv Client where
62 askN px = Client $ MC.askN px
63 instance MC.MonadExecN 'MC.Zero IO Client where
64 execN _px = Client . lift . lift
66 -- | Try clients in order, last error is preserved.
67 instance Alternative Client where
68 empty = MC.throw $ ClientError_EmptyClient
71 MC.exec (runClient env x) >>= \case
73 Left _err -> unClient y
77 MimeUnserialize mt a =>
78 (ClientRequestType mt a -> ClientRequest) -> Client a
80 clientRes <- doClientRequest $ req ClientRequestType
81 mimeUnserializeResponse (Proxy::Proxy mt) clientRes
83 runClient :: ClientEnv -> Client a -> IO (Either ClientError a)
84 runClient env (Client c) = E.runExceptT $ R.runReaderT c env
86 runClientRequest :: ClientEnv -> ClientRequest -> IO (Either ClientError ClientResponse)
87 runClientRequest env req = runClient env (doClientRequest req)
89 -- ** Type 'ClientRequestType'
90 data ClientRequestType mt a = ClientRequestType
92 -- ** Type 'ClientEnv'
93 data ClientEnv = ClientEnv
94 { clientEnv_manager :: Client.Manager
95 , clientEnv_baseURI :: URI
96 , clientEnv_cookieJar :: Maybe (STM.TVar Client.CookieJar)
98 clientEnv :: Client.Manager -> URI -> ClientEnv
99 clientEnv clientEnv_manager clientEnv_baseURI =
101 { clientEnv_cookieJar = Nothing
105 -- ** Type 'ClientError'
107 -- | The server returned an error response
108 = ClientError_FailureResponse ClientResponse
109 -- | The body could not be decoded at the expected type
110 | ClientError_DecodeFailure Text ClientResponse
111 -- | The content-type of the response is not supported
112 | ClientError_UnsupportedContentType MediaType ClientResponse
113 -- | The content-type header is invalid
114 | ClientError_InvalidContentTypeHeader ClientResponse
115 -- | There was a connection error, and no response was received
116 | ClientError_ConnectionError Client.HttpException
117 -- | 'Client' is 'empty'
118 | ClientError_EmptyClient
119 deriving (Eq, Show{-, Generic, Typeable-})
120 instance Exn.Exception ClientError
121 instance Eq Client.HttpException where
122 (==) = (==) `on` show
124 -- * Type 'ClientRequest'
125 data ClientRequest = ClientRequest
126 { clientReqHttpVersion :: HTTP.HttpVersion
127 , clientReqMethod :: HTTP.Method
128 , clientReqPath :: BSB.Builder
129 , clientReqQueryString :: Seq HTTP.QueryItem
130 , clientReqAccept :: Seq Media.MediaType
131 , clientReqHeaders :: Seq HTTP.Header
132 , clientReqBody :: Maybe (Client.RequestBody, Media.MediaType)
134 instance Default ClientRequest where
136 { clientReqHttpVersion = HTTP.http11
137 , clientReqMethod = HTTP.methodGet
139 , clientReqQueryString = Seq.empty
140 , clientReqAccept = Seq.empty
141 , clientReqHeaders = Seq.empty
142 , clientReqBody = Nothing
145 clientRequest :: URI -> ClientRequest -> Client.Request
146 clientRequest baseURI req =
147 Client.defaultRequest
148 { Client.method = clientReqMethod req
149 , Client.host = maybe "" (fromString . URI.uriRegName) $ URI.uriAuthority baseURI
150 , Client.port = case URI.uriPort <$> URI.uriAuthority baseURI of
151 Just (':':p) | Just port <- readMaybe p -> port
153 , Client.path = BSL.toStrict $ fromString (URI.uriPath baseURI) <> BSB.toLazyByteString (clientReqPath req)
154 , Client.queryString = HTTP.renderQuery True . toList $ clientReqQueryString req
155 , Client.requestHeaders = acceptHeader <> contentTypeHeader <> headers
157 , Client.secure = URI.uriScheme baseURI == "https"
160 headers = List.filter (\(h, _) -> h/="Accept" && h/="Content-Type") $
161 toList $ clientReqHeaders req
163 acceptHeader | null hs = []
164 | otherwise = [("Accept", Media.renderHeader hs)]
166 hs = toList $ clientReqAccept req
168 (requestBody, contentTypeHeader) =
169 case clientReqBody req of
170 Nothing -> (Client.RequestBodyLBS "", [])
171 Just (body, typ) -> (body, [(HTTP.hContentType, Media.renderHeader typ)])
173 setClientRequestBodyLBS :: BSL.ByteString -> MediaType -> ClientRequest -> ClientRequest
174 setClientRequestBodyLBS body mt req = req{ clientReqBody = Just (Client.RequestBodyLBS body, mt) }
176 setClientRequestBody :: Client.RequestBody -> MediaType -> ClientRequest -> ClientRequest
177 setClientRequestBody body mt req = req{ clientReqBody = Just (body, mt) }
179 -- ** Type 'ClientResponse'
180 type ClientResponse = ClientResponseWithBody BSL.ByteString
181 data ClientResponseWithBody a = ClientResponse
182 { clientResStatus :: HTTP.Status
183 , clientResHeaders :: Seq HTTP.Header
184 , clientResHttpVersion :: HTTP.HttpVersion
186 } deriving (Eq, Show, Functor)
188 clientResponse :: Client.Response a -> ClientResponseWithBody a
191 { clientResStatus = Client.responseStatus res
192 , clientResBody = Client.responseBody res
193 , clientResHeaders = Seq.fromList $ Client.responseHeaders res
194 , clientResHttpVersion = Client.responseVersion res
197 doClientRequest :: ClientRequest -> Client ClientResponse
198 doClientRequest clientReq = do
199 ClientEnv{..} <- MC.ask
201 let req = clientRequest clientEnv_baseURI clientReq in
202 case clientEnv_cookieJar of
206 now <- Time.getCurrentTime
208 oldCookieJar <- STM.readTVar cj
209 let (newRequest, newCookieJar) =
210 Client.insertCookiesIntoRequest req oldCookieJar now
211 STM.writeTVar cj newCookieJar
214 MC.exec $ catchConnectionError $
215 Client.httpLbs req clientEnv_manager
217 Left err -> MC.throw err
219 for_ clientEnv_cookieJar $ \cj ->
221 now <- Time.getCurrentTime
222 STM.atomically $ STM.modifyTVar' cj (fst . Client.updateCookieJar res req now)
223 let status = HTTP.statusCode $ Client.responseStatus res
224 clientRes = clientResponse res
225 unless (status >= 200 && status < 300) $
226 MC.throw $ ClientError_FailureResponse clientRes
229 catchConnectionError :: IO a -> IO (Either ClientError a)
230 catchConnectionError action =
231 Exn.catch (Right <$> action) $ \err ->
232 return $ Left $ ClientError_ConnectionError err
234 -- ** Type 'ClientResponseStreaming'
235 newtype ClientResponseStreaming = ClientResponseStreaming
236 { runResponseStreaming ::
237 forall a. (ClientResponseWithBody (IO BS.ByteString) -> IO a) -> IO a }
239 doClientRequestStreaming :: ClientRequest -> Client ClientResponseStreaming
240 doClientRequestStreaming clientReq = do
241 ClientEnv{..} <- MC.ask
242 let req = clientRequest clientEnv_baseURI clientReq
243 return $ ClientResponseStreaming $ \k ->
244 Client.withResponse req clientEnv_manager $ \res -> do
245 let status = HTTP.statusCode $ Client.responseStatus res
246 unless (status >= 200 && status < 300) $ do
247 responseBody <- BSL.fromChunks <$> Client.brConsume (Client.responseBody res)
248 Exn.throw $ ClientError_FailureResponse $ clientResponse res{Client.responseBody}
249 k $ clientResponse res
252 MC.MonadExcept ClientError m =>
253 ClientResponse -> m MediaType
254 getContentType clientRes =
255 case List.lookup "Content-Type" $ toList $ clientResHeaders clientRes of
256 Nothing -> return $ "application"Media.//"octet-stream"
258 case Media.parseAccept mt of
259 Nothing -> MC.throw $ ClientError_InvalidContentTypeHeader clientRes
260 Just mt' -> return mt'
262 mimeUnserializeResponse ::
263 MimeUnserialize mt a =>
264 MC.MonadExcept ClientError m =>
265 Proxy mt -> ClientResponse -> m a
266 mimeUnserializeResponse mt clientRes = do
267 mtRes <- getContentType clientRes
268 unless (any (Media.matches mtRes) $ mediaTypes mt) $
269 MC.throw $ ClientError_UnsupportedContentType mtRes clientRes
270 case mimeUnserialize mt $ clientResBody clientRes of
271 Left err -> MC.throw $ ClientError_DecodeFailure (T.pack err) clientRes
272 Right val -> return val