servant-haxl-client-0.2.0.0: src-ghcjs/Servant/Haxl/Client/Internal.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE JavaScriptFFI #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
module Servant.Haxl.Client.Internal
( ServantResponse(..)
, ServantRequest(..)
, initServantClientState
) where
import Control.Concurrent.Async
import Control.Concurrent.QSem
import Control.Exception
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Either
import qualified Data.ByteString.Char8 as BS
import Data.ByteString.Lazy hiding (elem, filter, map,
null, pack)
import qualified Data.CaseInsensitive as CI
import Data.Hashable
import qualified Data.JSString as JSString
import Data.JSString.Text
import Data.Maybe
import Data.String.Conversions
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Text.Encoding
import Haxl.Core hiding (Request, catch)
import qualified JavaScript.Web.XMLHttpRequest as XHR
import Network.HTTP.Media
import Network.HTTP.Types
import qualified Network.HTTP.Types.Header as HTTP
import Network.URI
import Servant.Haxl.Client.BaseUrl
import Servant.Haxl.Client.Internal.Error
import Servant.Haxl.Client.Types
-- foreign import javascript safe "new DataView($3,$1,$2)" js_dataView :: Int -> Int -> JSVal -> JSVal
data ServantResponse = ServantResponse (XHR.Response BS.ByteString)
instance Show ServantResponse where
show (ServantResponse xhrResponse) = show $ XHR.contents xhrResponse
xhrMethodConversion :: Method -> XHR.Method
xhrMethodConversion method
| method == methodGet = XHR.GET
| method == methodPost = XHR.POST
| method == methodPut = XHR.PUT
| method == methodDelete = XHR.DELETE
| otherwise = error "No such XHR method"
xhrRequestConversion :: XHR.Method -> Req -> BaseUrl -> XHR.Request
xhrRequestConversion method req (BaseUrl reqScheme reqHost reqPort) =
setData XHR.Request
{ XHR.reqMethod = method
, XHR.reqURI = JSString.pack . show $ nullURI
{ uriScheme = case reqScheme of
Http -> "http:"
Https -> "https:"
, uriAuthority = Just
URIAuth { uriUserInfo = ""
, uriRegName = reqHost
, uriPort = ":" ++ show reqPort
}
, uriPath = reqPath req
, uriQuery = BS.unpack $ renderQuery True $ queryTextToQuery (qs req)
}
, XHR.reqLogin = Nothing
, XHR.reqHeaders = (\(str, text) -> (JSString.pack str, textToJSString text))
<$> headers req
, XHR.reqWithCredentials = False
, XHR.reqData = XHR.NoData -- will be set by setData
}
where
setData r = case reqBody req of
Nothing -> r
Just (bs, media) -> do
-- let (b, _, _) = fromByteString $ toStrict bs
r { XHR.reqHeaders = XHR.reqHeaders r ++ [("Content-Type", JSString.pack . show $ media)]
-- , XHR.reqData = XHR.TypedArrayData $ getUint8Array b
-- Temporary workaround until I can solve this:
-- http://stackoverflow.com/questions/35930517/sending-binary-data-with-ghcjs-xhr
, XHR.reqData = XHR.StringData $ textToJSString $ decodeUtf8 $ toStrict bs
}
performRequest_ :: Method -> Req -> WantedStatusCodes -> BaseUrl
-> EitherT ServantError IO ( Int, ByteString, MediaType
, [HTTP.Header], ServantResponse)
performRequest_ method req wantedStatus reqHost = do
let xhrMethod = xhrMethodConversion method
let xhrReq = xhrRequestConversion xhrMethod req reqHost
eResponse <- liftIO $ catchXHRError $ XHR.xhrByteString xhrReq
case eResponse of
Left err ->
left $ ConnectionError $ ServantConnectionError err
Right response -> do
let body = fromStrict $ fromMaybe "" (XHR.contents response)
status_code = XHR.status response
hrds <- liftIO $
breakHeaders . textFromJSString <$> XHR.getAllResponseHeaders response
ct <- case lookup "Content-Type" hrds of
Nothing -> pure $ "application"//"octet-stream"
Just t -> case parseAccept t of
Nothing -> left $ InvalidContentTypeHeader (cs t) body
Just t' -> pure t'
unless (wantedStatus `wants` status_code) $
left $ FailureResponse (Status status_code "") ct body
return (status_code, body, ct, hrds, ServantResponse response)
where
wants AllCodes _ = True
wants (SelectCodes codes) status_code = status_code `elem` codes
breakHeaders :: Text -> [HTTP.Header]
breakHeaders allHeaders = Text.splitOn "\r\n" allHeaders >>= breakHeader
where
breakHeader :: Text -> [HTTP.Header]
breakHeader txHeader = header
where
(txName, txDataWithColon) = Text.breakOn ": " txHeader
txData = Text.stripPrefix ": " txDataWithColon
ciName = CI.mk $ encodeUtf8 txName
bsData = encodeUtf8 <$> txData
header = maybe [] (\d -> [(ciName, d)]) bsData
catchXHRError :: IO a -> IO (Either XHR.XHRError a)
catchXHRError action = catch (Right <$> action) (pure . Left)
data ServantRequest a where
ServantRequest :: Method -> Req -> WantedStatusCodes -> BaseUrl ->
ServantRequest (Int, ByteString, MediaType, [HTTP.Header], ServantResponse)
deriving instance Show (ServantRequest a)
deriving instance Eq (ServantRequest a)
instance Show1 ServantRequest where
show1 = show
instance Hashable (ServantRequest a) where
hashWithSalt s (ServantRequest m r w h) = hashWithSalt s (m, r, w, h)
instance DataSourceName ServantRequest where
dataSourceName _ = "ServantRequest"
instance StateKey ServantRequest where
data State ServantRequest = ServantRequestState Int
instance DataSource () ServantRequest where
fetch (ServantRequestState numThreads) _ () requests =
AsyncFetch $ \inner -> do
sem <- newQSem numThreads
asyncs <- mapM (handler sem) requests
inner
mapM_ wait asyncs
where
handler :: QSem -> BlockedFetch ServantRequest -> IO (Async ())
handler sem (BlockedFetch (ServantRequest met req wantedStatus reqHost) rvar) =
async $ bracket_ (waitQSem sem) (signalQSem sem) $ do
e <- runEitherT $ performRequest_ met req wantedStatus reqHost
case e of
Left err -> putFailure rvar err
Right a -> putSuccess rvar a
return ()
initServantClientState :: Int -> IO (State ServantRequest)
initServantClientState = return . ServantRequestState