{-# LANGUAGE RankNTypes, OverloadedStrings, DeriveDataTypeable, ForeignFunctionInterface, JavaScriptFFI, EmptyDataDecls, TypeFamilies, DataKinds, ScopedTypeVariables, FlexibleContexts, FlexibleInstances, TypeSynonymInstances, LambdaCase, MultiParamTypeClasses, DeriveGeneric #-} module JavaScript.Web.XMLHttpRequest ( xhr , xhrByteString , xhrText , xhrString , Method(..) , Request(..) , RequestData(..) , Response(..) , ResponseType(..) , FormDataVal(..) , XHRError(..) ) where import Control.Applicative import Control.Exception import Control.Monad import GHCJS.Types import GHCJS.Prim import GHCJS.Marshal import GHCJS.Marshal.Pure import GHCJS.Internal.Types import qualified GHCJS.Buffer as Buffer import GHC.Generics import Data.ByteString import Data.Data import Data.JSString import Data.JSString.Internal.Type ( JSString(..) ) import Data.Typeable import Data.Proxy import Data.Text (Text) import Data.JSString.Text (textFromJSString) import qualified Data.JSString as JSS import JavaScript.JSON.Types.Internal ( SomeValue(..) ) import JavaScript.TypedArray.Internal.Types ( SomeTypedArray(..) ) import JavaScript.TypedArray.ArrayBuffer ( ArrayBuffer(..) ) import JavaScript.TypedArray.ArrayBuffer.Internal ( SomeArrayBuffer(..) ) import JavaScript.Web.Blob import JavaScript.Web.Blob.Internal import JavaScript.Web.File data Method = GET | POST | PUT | DELETE deriving (Show, Eq, Ord, Enum) data XHRError = XHRError String | XHRAborted deriving (Generic, Data, Typeable, Show, Eq) instance Exception XHRError methodJSString :: Method -> JSString methodJSString GET = "GET" methodJSString POST = "POST" methodJSString PUT = "PUT" methodJSString DELETE = "DELETE" type Header = (JSString, JSString) data FormDataVal = StringVal JSString | BlobVal Blob (Maybe JSString) | FileVal File (Maybe JSString) deriving (Typeable) data Request = Request { reqMethod :: Method , reqURI :: JSString , reqLogin :: Maybe (JSString, JSString) , reqHeaders :: [Header] , reqWithCredentials :: Bool , reqData :: RequestData } deriving (Typeable) data RequestData = NoData | StringData JSString | TypedArrayData (forall e. SomeTypedArray e Immutable) | FormData [(JSString, FormDataVal)] deriving (Typeable) data Response a = Response { contents :: Maybe a , status :: Int , getAllResponseHeaders :: IO JSString , getResponseHeader :: JSString -> IO (Maybe JSString) } instance Functor Response where fmap f r = r { contents = fmap f (contents r) } class ResponseType a where getResponseTypeString :: Proxy a -> JSString wrapResponseType :: JSVal -> a instance ResponseType ArrayBuffer where getResponseTypeString _ = "arraybuffer" wrapResponseType = SomeArrayBuffer instance m ~ Immutable => ResponseType JSString where getResponseTypeString _ = "text" wrapResponseType = JSString instance ResponseType Blob where getResponseTypeString _ = "blob" wrapResponseType = SomeBlob instance m ~ Immutable => ResponseType (SomeValue m) where getResponseTypeString _ = "json" wrapResponseType = SomeValue newtype JSFormData = JSFormData JSVal deriving (Typeable) newtype XHR = XHR JSVal deriving (Typeable) -- ----------------------------------------------------------------------------- -- main entry point xhr :: forall a. ResponseType a => Request -> IO (Response a) xhr req = js_createXHR >>= \x -> let doRequest = do case reqLogin req of Nothing -> js_open2 (methodJSString (reqMethod req)) (reqURI req) x Just (user, pass) -> js_open4 (methodJSString (reqMethod req)) (reqURI req) user pass x js_setResponseType (getResponseTypeString (Proxy :: Proxy a)) x forM_ (reqHeaders req) (\(n,v) -> js_setRequestHeader n v x) case reqWithCredentials req of True -> js_setWithCredentials x False -> return () r <- case reqData req of NoData -> js_send0 x StringData str -> js_send1 (pToJSVal str) x TypedArrayData (SomeTypedArray t) -> js_send1 t x FormData xs -> do fd@(JSFormData fd') <- js_createFormData forM_ xs $ \(name, val) -> case val of StringVal str -> js_appendFormData2 name (pToJSVal str) fd BlobVal (SomeBlob b) mbFile -> appendFormData name b mbFile fd FileVal (SomeBlob b) mbFile -> appendFormData name b mbFile fd js_send1 fd' x case r of 0 -> do status <- js_getStatus x r <- do hr <- js_hasResponse x if hr then Just . wrapResponseType <$> js_getResponse x else pure Nothing return $ Response r status (js_getAllResponseHeaders x) (\h -> getResponseHeader' h x) 1 -> throwIO XHRAborted 2 -> throwIO (XHRError "network request error") in doRequest `onException` js_abort x appendFormData :: JSString -> JSVal -> Maybe JSString -> JSFormData -> IO () appendFormData name val Nothing fd = js_appendFormData2 name val fd appendFormData name val (Just fileName) fd = js_appendFormData3 name val fileName fd getResponseHeader' :: JSString -> XHR -> IO (Maybe JSString) getResponseHeader' name x = do h <- js_getResponseHeader name x return $ if isNull h then Nothing else Just (JSString h) -- ----------------------------------------------------------------------------- -- utilities xhrString :: Request -> IO (Response String) xhrString = fmap (fmap JSS.unpack) . xhr xhrText :: Request -> IO (Response Text) xhrText = fmap (fmap textFromJSString) . xhr xhrByteString :: Request -> IO (Response ByteString) xhrByteString = fmap (fmap (Buffer.toByteString 0 Nothing . Buffer.createFromArrayBuffer)) . xhr -- ----------------------------------------------------------------------------- foreign import javascript unsafe "$1.withCredentials = true;" js_setWithCredentials :: XHR -> IO () foreign import javascript unsafe "new XMLHttpRequest()" js_createXHR :: IO XHR foreign import javascript unsafe "$2.responseType = $1;" js_setResponseType :: JSString -> XHR -> IO () foreign import javascript unsafe "$1.abort();" js_abort :: XHR -> IO () foreign import javascript unsafe "$3.setRequestHeader($1,$2);" js_setRequestHeader :: JSString -> JSString -> XHR -> IO () foreign import javascript unsafe "$3.open($1,$2)" js_open2 :: JSString -> JSString -> XHR -> IO () foreign import javascript unsafe "$5.open($1,$2,true,$4,$5);" js_open4 :: JSString -> JSString -> JSString -> JSString -> XHR -> IO () foreign import javascript unsafe "new FormData()" js_createFormData :: IO JSFormData foreign import javascript unsafe "$3.append($1,$2)" js_appendFormData2 :: JSString -> JSVal -> JSFormData -> IO () foreign import javascript unsafe "$4.append($1,$2,$3)" js_appendFormData3 :: JSString -> JSVal -> JSString -> JSFormData -> IO () foreign import javascript unsafe "$1.status" js_getStatus :: XHR -> IO Int foreign import javascript unsafe "$1.response" js_getResponse :: XHR -> IO JSVal foreign import javascript unsafe "$1.response ? true : false" js_hasResponse :: XHR -> IO Bool foreign import javascript unsafe "$1.getAllResponseHeaders()" js_getAllResponseHeaders :: XHR -> IO JSString foreign import javascript unsafe "$2.getResponseHeader($1)" js_getResponseHeader :: JSString -> XHR -> IO JSVal -- ----------------------------------------------------------------------------- foreign import javascript interruptible "h$sendXHR($1, null, $c);" js_send0 :: XHR -> IO Int foreign import javascript interruptible "h$sendXHR($2, $1, $c);" js_send1 :: JSVal -> XHR -> IO Int