{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ExtendedDefaultRules #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PackageImports #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -Wunused-imports #-}
{-# OPTIONS_GHC -Wincomplete-patterns #-}
module Servant.Client.JS
( module Servant.Client.Core.Reexport
, ClientEnv (..)
, ClientM (..)
, runClientM
, client
, withStreamingRequestJSM
) where
import Control.Concurrent
import Control.Monad (forM_)
import Control.Monad.Base
import Control.Monad.Catch
import Control.Monad.Error.Class
import Control.Monad.Reader
import Control.Monad.Trans.Control
import Control.Monad.Trans.Except
import Data.Binary.Builder (toLazyByteString)
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as BL
import Data.CaseInsensitive
import Data.Maybe (fromMaybe)
import Data.Proxy
import Data.Functor.Alt
import qualified Data.Sequence as Seq
import Data.Text
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import GHC.Conc
import GHC.Generics
import GHCJS.Buffer
import GHCJS.Marshal.Internal
#ifdef ghcjs_HOST_OS
import GHCJS.Prim hiding (getProp, fromJSString)
import Language.Javascript.JSaddle (fromJSString)
#else
import "jsaddle" GHCJS.Prim hiding (fromJSString)
#endif
import Language.Javascript.JSaddle (
#ifndef ghcjs_HOST_OS
MonadJSM,
#endif
JSM (..), liftJSM, jsg, toJSVal, obj, (#), (<#), fun, fromJSVal, (!), JSString (..), makeObject, isTruthy, ghcjsPure )
import Network.HTTP.Media (renderHeader)
import Network.HTTP.Types
import Servant.Client.Core
import Servant.Client.Core.Reexport
import qualified Servant.Types.SourceT as S
default (Text)
newtype ClientEnv = ClientEnv { baseUrl :: BaseUrl }
deriving (Eq, Show)
newtype ClientM a = ClientM
{ runClientM' :: ReaderT ClientEnv (ExceptT ClientError JSM) a }
deriving ( Functor, Applicative, Monad, MonadIO
#ifndef ghcjs_HOST_OS
, MonadJSM
#endif
, Generic, MonadReader ClientEnv, MonadError ClientError
, MonadThrow, MonadCatch )
client :: HasClient ClientM api => Proxy api -> Client ClientM api
client api = api `clientIn` (Proxy :: Proxy ClientM)
runClientM :: ClientM a -> ClientEnv -> JSM (Either ClientError a)
runClientM m env = runExceptT $ runReaderT (runClientM' m) env
#ifndef ghcjs_HOST_OS
deriving instance MonadBase IO JSM
deriving instance MonadBaseControl IO JSM
#endif
instance MonadBase IO ClientM where
liftBase = ClientM . liftBase
instance MonadBaseControl IO ClientM where
type StM ClientM a = Either ClientError a
liftBaseWith f = ClientM (liftBaseWith (\g -> f (g . runClientM')))
restoreM st = ClientM (restoreM st)
instance Alt ClientM where
a <!> b = a `catchError` const b
instance RunClient ClientM where
runRequest = fetch
throwClientError = throwError
instance RunStreamingClient ClientM where
withStreamingRequest req handler = withStreamingRequestJSM req (liftIO . handler)
#ifdef ghcjs_HOST_OS
unJSString :: JSString -> Text
unJSString = fromJSString
#else
unJSString :: JSString -> Text
unJSString (JSString s) = s
#endif
getFetchArgs :: ClientEnv -> Request -> JSM [JSVal]
getFetchArgs (ClientEnv (BaseUrl urlScheme host port basePath))
(Request reqPath reqQs reqBody reqAccept reqHdrs _reqVer reqMethod) = do
self <- jsg "self"
let schemeStr :: Text
schemeStr = case urlScheme of
Http -> "http://"
Https -> "https://"
url <- toJSVal $ schemeStr <> pack host <> ":" <> pack (show port) <> pack basePath
<> decodeUtf8 (BL.toStrict (toLazyByteString reqPath))
<> (if Prelude.null reqQs then "" else "?" ) <> (intercalate "&"
$ (\(k,v) -> decodeUtf8 k <> "="
<> maybe "" decodeUtf8 v)
<$> Prelude.foldr (:) [] reqQs)
init <- obj
methodStr <- toJSVal $ decodeUtf8 reqMethod
init <# "method" $ methodStr
headers <- obj
forM_ reqHdrs $ \(original -> k, v) -> do
v' <- toJSVal (decodeUtf8 v)
headers <# decodeUtf8 k $ v'
forM_ reqAccept $ \mt -> do
mt' <- toJSVal (decodeUtf8 (renderHeader mt))
headers <# "Accept" $ mt'
init <# "headers" $ headers
case reqBody of
Just (RequestBodyLBS x, mt) -> do
v <- toJSVal (decodeUtf8 (BL.toStrict x))
init <# "body" $ v
mt' <- toJSVal (decodeUtf8 (renderHeader mt))
headers <# "Content-Type" $ mt'
Just (RequestBodyBS x, mt) -> do
v <- toJSVal (decodeUtf8 x)
init <# "body" $ v
mt' <- toJSVal (decodeUtf8 (renderHeader mt))
headers <# "Content-Type" $ mt'
Just (RequestBodySource _, _) -> error "Servant.Client.JS.withStreamingRequest(JSM) does not (yet) support RequestBodySource"
Nothing -> return ()
init' <- toJSVal init
return [url, init']
getResponseMeta :: JSVal -> JSM (Status, Seq.Seq Header, HttpVersion)
getResponseMeta res = do
status <- toEnum . fromMaybe 200
<$> (fromJSVal =<< res ! ("status" :: Text))
resHeadersObj <- makeObject =<< res ! ("headers" :: Text)
resHeaderNames <- (resHeadersObj # ("keys" :: Text) $ ([] :: [JSVal]))
>>= fix (\go names ->
do x <- names # ("next" :: Text) $ ([] :: [JSVal])
isDone <- fromJSVal =<< (x ! ("done" :: Text))
if isDone == Just True || isDone == Nothing
then return []
else do
rest <- go names
v <- fromJSVal =<< x ! "value"
case v of
Just k -> return (k : rest)
Nothing -> return rest)
resHeaders <- fmap (Prelude.foldr (Seq.:<|) Seq.Empty)
. forM resHeaderNames $ \headerName -> do
headerValue <- fmap (fromMaybe "") . fromJSVal
=<< (resHeadersObj # ("get" :: Text) $ [headerName])
return (mk (encodeUtf8 (unJSString headerName)), encodeUtf8 headerValue)
return (status, resHeaders, http11)
uint8arrayToByteString :: JSVal -> JSM BS.ByteString
uint8arrayToByteString val = do
abuf <- val ! "buffer"
buf <- ghcjsPure (createFromArrayBuffer (pFromJSVal abuf)) >>= freeze
len <- ghcjsPure (byteLength buf)
ghcjsPure $ toByteString 0 (Just len) buf
parseChunk :: JSVal -> JSM (Maybe BS.ByteString)
parseChunk chunk = do
isDone <- ghcjsPure =<< isTruthy
<$> (chunk ! ("done" :: Text))
case isDone of
True -> return Nothing
False -> Just <$> (uint8arrayToByteString =<< chunk ! ("value" :: Text))
fetch :: Request -> ClientM Response
fetch req = ClientM . ReaderT $ \env -> do
self <- liftJSM $ jsg ("self" :: Text)
args <- liftJSM $ getFetchArgs env req
promise <- liftJSM $ self # ("fetch" :: Text) $ args
contents <- liftIO $ newTVarIO (mempty :: BS.ByteString)
result <- liftIO newEmptyMVar
promiseHandler <- liftJSM . toJSVal . fun $ \_ _ args -> do
case args of
[res] -> do
meta <- getResponseMeta res
stream <- res ! ("body" :: Text)
rdr <- stream # ("getReader" :: Text) $ ([] :: [JSVal])
_ <- fix $ \go -> do
rdrPromise <- rdr # ("read" :: Text) $ ([] :: [JSVal])
rdrHandler <- toJSVal . fun $ \_ _ args -> do
case args of
[chunk] -> do
next <- parseChunk chunk
case next of
Nothing -> liftIO $ putMVar result . (meta,) =<< readTVarIO contents
Just x -> do
liftIO . atomically $ writeTVar contents . (<> x) =<< readTVar contents
go
_ -> do
error "fetch read promise handler received wrong number of arguments"
_ <- rdrPromise # ("then" :: Text) $ [rdrHandler]
return ()
return ()
_ -> error "fetch promise handler received wrong number of arguments"
liftJSM $ promise # ("then" :: Text) $ [promiseHandler]
((status, hdrs, ver), body) <- liftIO $ takeMVar result
return $ Response status hdrs ver (BL.fromStrict body)
withStreamingRequestJSM :: Request -> (StreamingResponse -> JSM a) -> ClientM a
withStreamingRequestJSM req handler =
ClientM . ReaderT $ \env -> do
self <- liftJSM $ jsg "self"
fetchArgs <- liftJSM $ getFetchArgs env req
fetchPromise <- liftJSM $ self # "fetch" $ fetchArgs
push <- liftIO newEmptyMVar
result <- liftIO newEmptyMVar
fetchPromiseHandler <- liftJSM . toJSVal . fun $ \_ _ args ->
case args of
[res] -> do
(status, hdrs, ver) <- getResponseMeta res
stream <- res ! ("body" :: Text)
rdr <- stream # ("getReader" :: Text) $ ([] :: [JSVal])
_ <- fix $ \go -> do
rdrPromise <- rdr # ("read" :: Text) $ ([] :: [JSVal])
rdrHandler <- toJSVal . fun $ \_ _ args ->
case args of
[chunk] -> do
next <- parseChunk chunk
case next of
Just bs -> do
liftIO $ putMVar push (Just bs)
go
Nothing -> liftIO $ putMVar push Nothing
_ -> error "wrong number of arguments to rdrHandler"
_ <- rdrPromise # ("then" :: Text) $ [rdrHandler]
return ()
let out :: forall b. (S.StepT IO BS.ByteString -> IO b) -> IO b
out handler' = handler' . S.Effect . fix $ \go -> do
next <- takeMVar push
case next of
Nothing -> return S.Stop
Just x -> return $ S.Yield x (S.Effect go)
liftIO . putMVar result . Response status hdrs ver $ S.SourceT @IO out
_ -> error "wrong number of arguments to Promise.then() callback"
liftJSM $ fetchPromise # "then" $ [fetchPromiseHandler]
liftJSM . handler =<< liftIO (takeMVar result)