{-# 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) -- http11 is made up 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) -- | A variation on @Servant.Client.Core.withStreamingRequest@ where the continuation / callback -- passed as the second argument is in the JSM monad as opposed to the IO monad. -- Executes the given request and passes the response data stream to the provided continuation / callback. 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)