servant/src/Servant/Common/Req.hs

161 lines
5.9 KiB
Haskell
Raw Normal View History

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Servant.Common.Req where
import Control.Applicative
import Control.Exception
import Control.Monad
import Control.Monad.Catch (MonadThrow)
import Control.Monad.IO.Class
import Control.Monad.Trans.Either
import Data.ByteString.Lazy hiding (pack, filter, map, null)
import Data.IORef
2014-12-08 12:52:30 +01:00
import Data.String
import Data.String.Conversions
import Data.Proxy
2015-02-17 01:51:59 +01:00
import Data.Text (Text)
2014-12-08 12:52:30 +01:00
import Data.Text.Encoding
import Network.HTTP.Client hiding (Proxy)
2015-01-22 23:18:13 +01:00
import Network.HTTP.Client.TLS
import Network.HTTP.Media
import Network.HTTP.Types
import Network.URI
import Servant.API.ContentTypes
import Servant.Common.BaseUrl
2014-12-08 12:52:30 +01:00
import Servant.Common.Text
import System.IO.Unsafe
import qualified Network.HTTP.Client as Client
data Req = Req
2015-02-17 01:51:59 +01:00
{ reqPath :: String
, qs :: QueryText
, reqBody :: Maybe (ByteString, MediaType)
, reqAccept :: [MediaType]
, headers :: [(String, Text)]
}
defReq :: Req
2015-02-17 01:51:59 +01:00
defReq = Req "" [] Nothing [] []
appendToPath :: String -> Req -> Req
appendToPath p req =
req { reqPath = reqPath req ++ "/" ++ p }
2015-01-01 23:43:29 +01:00
appendToMatrixParams :: String
-> Maybe String
-> Req
-> Req
appendToMatrixParams pname pvalue req =
req { reqPath = reqPath req ++ ";" ++ pname ++ maybe "" ("=" ++) pvalue }
appendToQueryString :: Text -- ^ param name
-> Maybe Text -- ^ param value
-> Req
-> Req
appendToQueryString pname pvalue req =
req { qs = qs req ++ [(pname, pvalue)]
}
2014-12-08 12:52:30 +01:00
addHeader :: ToText a => String -> a -> Req -> Req
addHeader name val req = req { headers = headers req
++ [(name, toText val)]
}
setRQBody :: ByteString -> MediaType -> Req -> Req
setRQBody b t req = req { reqBody = Just (b, t) }
reqToRequest :: (Functor m, MonadThrow m) => Req -> BaseUrl -> m Request
2014-12-08 12:52:30 +01:00
reqToRequest req (BaseUrl reqScheme reqHost reqPort) =
2015-02-17 01:51:59 +01:00
fmap (setheaders . setAccept . setrqb . setQS ) $ parseUrl url
where url = show $ nullURI { uriScheme = case reqScheme of
Http -> "http:"
Https -> "https:"
, uriAuthority = Just $
URIAuth { uriUserInfo = ""
, uriRegName = reqHost
, uriPort = ":" ++ show reqPort
}
, uriPath = reqPath req
}
2015-02-17 03:50:50 +01:00
setrqb r = case reqBody req of
Nothing -> r
Just (b,t) -> r { requestBody = RequestBodyLBS b
2015-02-17 01:51:59 +01:00
, requestHeaders = requestHeaders r
++ [(hContentType, cs . show $ t)] }
setQS = setQueryString $ queryTextToQuery (qs req)
setheaders r = r { requestHeaders = requestHeaders r
2015-02-17 03:50:50 +01:00
<> fmap toProperHeader (headers req) }
2015-02-17 01:51:59 +01:00
setAccept r = r { requestHeaders = filter ((/= "Accept") . fst) (requestHeaders r)
2015-02-17 03:50:50 +01:00
<> [("Accept", renderHeader $ reqAccept req)
| not . null . reqAccept $ req] }
2014-12-08 12:52:30 +01:00
toProperHeader (name, val) =
2015-02-06 09:34:59 +01:00
(fromString name, encodeUtf8 val)
-- * performing requests
{-# NOINLINE __manager #-}
__manager :: IORef Manager
__manager = unsafePerformIO (newManager tlsManagerSettings >>= newIORef)
__withGlobalManager :: (Manager -> IO a) -> IO a
__withGlobalManager action = readIORef __manager >>= action
displayHttpRequest :: Method -> String
displayHttpRequest httpmethod = "HTTP " ++ cs httpmethod ++ " request"
2015-02-17 00:32:15 +01:00
performRequest :: Method -> Req -> (Int -> Bool) -> BaseUrl -> EitherT String IO (Int, ByteString, MediaType)
performRequest reqMethod req isWantedStatus reqHost = do
partialRequest <- liftIO $ reqToRequest req reqHost
let request = partialRequest { Client.method = reqMethod
, checkStatus = \ _status _headers _cookies -> Nothing
}
eResponse <- liftIO $ __withGlobalManager $ \ manager ->
catchStatusCodeException $
Client.httpLbs request manager
case eResponse of
Left status ->
left (displayHttpRequest reqMethod ++ " failed with status: " ++ showStatus status)
Right response -> do
let status = Client.responseStatus response
unless (isWantedStatus (statusCode status)) $
left (displayHttpRequest reqMethod ++ " failed with status: " ++ showStatus status)
2015-02-17 00:32:15 +01:00
ct <- case lookup "Content-Type" $ Client.responseHeaders response of
Nothing -> pure $ "application"//"octet-stream"
Just t -> case parseAccept t of
Nothing -> left $ "invalid Content-Type header: " <> cs t
Just t' -> pure t'
2015-02-17 03:50:50 +01:00
return (statusCode status, Client.responseBody response, ct)
where
showStatus (Status code message) =
show code ++ " - " ++ cs message
performRequestCT :: MimeUnrender ct result =>
Proxy ct -> Method -> Req -> Int -> BaseUrl -> EitherT String IO result
performRequestCT ct reqMethod req wantedStatus reqHost = do
let acceptCT = contentType ct
(_status, respBody, respCT) <-
performRequest reqMethod (req { reqAccept = [acceptCT] }) (== wantedStatus) reqHost
unless (matches respCT (acceptCT)) $
left $ "requested Content-Type " <> show acceptCT <> ", but got " <> show respCT
either
(left . ((displayHttpRequest reqMethod ++ " returned invalid response of type" ++ show respCT) ++))
return
(fromByteString ct respBody)
catchStatusCodeException :: IO a -> IO (Either Status a)
catchStatusCodeException action =
catch (Right <$> action) $ \e ->
case e of
Client.StatusCodeException status _ _ -> return $ Left status
exc -> throwIO exc