haste-lib-0.6.0.0: src/Haste/Ajax.hs
{-# LANGUAGE GADTs, OverloadedStrings, ScopedTypeVariables #-}
-- | XMLHttpRequest support. IE9 and older are not supported.
module Haste.Ajax
( Method (..)
, AjaxData (..)
, AjaxError (..)
, ajax, ajaxWithMime
) where
import Haste.Prim.Foreign
import Haste.Prim
import Haste.Prim.JSType
import Control.Monad.IO.Class
import Control.Monad (join)
import Haste.Concurrent
import Data.Proxy
import Haste.Binary hiding (get)
class (ToAny a, FromAny a) => AjaxData a where
-- | The MIME type of the data represented by this type.
mimeType :: Proxy a -> JSString
-- | The XHR response type that corresponds to this type.
responseType :: Proxy a -> JSString
instance AjaxData JSString where
mimeType _ = ""
responseType _ = ""
instance AjaxData Blob where
mimeType _ = "application/octet-stream"
responseType _ = "blob"
instance AjaxData () where
mimeType _ = ""
responseType _ = ""
-- | Make an AJAX request. The return value is either the body of the requested
-- document, or an error.
ajax :: forall m post resp.
(MonadConc m, AjaxData post, AjaxData resp)
=> Method post -- ^ HTTP method to use for request. @post@ is always @()@
-- for GET requests.
-> URL -- ^ URL to request.
-> m (Either AjaxError resp)
ajax = ajaxWithMime ""
-- | Like 'ajax', but accepts a custom MIME type for POST data.
-- Use this with the appropriate MIME type and POST data if you want to send,
-- for instance, form data.
ajaxWithMime :: forall m post resp.
(MonadConc m, AjaxData post, AjaxData resp)
=> JSString -- ^ MIME type of any POST data. Decided by the 'AjaxData'
-- instance for @post@ if the empty string is given.
-- Only relevant to POST requests.
-> Method post -- ^ HTTP method to use for request.
-> URL -- ^ URL to request.
-> m (Either AjaxError resp)
ajaxWithMime mime method url = do
res <- newEmptyMVar
liftIO $ ajaxReq methodStr url mime' respType postdata $ \merr md -> do
case (md, merr) of
(Just d, _) -> fromAny d >>= concurrent . putMVar res . Right
(_, Just err) -> concurrent $ putMVar res (Left err)
liftCIO $ takeMVar res
where
mime'
| "" /= mime = mime
| otherwise = mimeType (Proxy :: Proxy post)
respType = responseType (Proxy :: Proxy resp)
(postdata, methodStr) = case method of
GET -> (Nothing, "GET")
POST d -> (Just $ toAny d, "POST")
PUT d -> (Just $ toAny d, "PUT")
DELETE -> (Nothing, "DELETE")
-- | An error which occurred during an AJAX request.
-- Might be either a network error (denied by CSP, host unreachable etc.)
-- or an HTTP error, with status code and description.
data AjaxError
= NetworkError
| HttpError Int JSString
deriving (Show, Eq)
instance FromAny AjaxError where
fromAny x = do
errtype <- get x "type"
case errtype of
"network" -> pure NetworkError
"http" -> HttpError <$> get x "status" <*> get x "status-text"
_ -> fail $ "unknown type of ajax error: " ++ fromJSStr errtype
-- | HTTP method to use for request. POST requests take an (optionally empty)
-- JSString representing data to POST.
data Method a where
GET :: Method ()
DELETE :: Method ()
POST :: a -> Method a
PUT :: a -> Method a
ajaxReq :: JSString -- ^ method (GET/POST)
-> JSString -- ^ URI
-> JSString -- ^ Outgoing MIME type; empty string means default
-> JSString -- ^ responseType field
-> Maybe JSAny -- ^ POST data
-> (Maybe AjaxError -> Maybe JSAny -> IO ())
-- ^ Callback; if successful, first argument is 0, the second the
-- empty string, and the third the response data.
-- If not, third argument is null and the other two give
-- HTTP status and error message.
-> IO ()
ajaxReq = ffi "(function(method, uri, mimeout, responseType, postdata, cb) {\
\var xhr = new XMLHttpRequest();\
\xhr.open(method, uri);\
\xhr.responseType = responseType;\
\if(mimeout != '') {\
\xhr.setRequestHeader('Content-type', mimeout);\
\}\
\xhr.addEventListener('load', function() {\
\if(xhr.status < 400) {cb(null, xhr.response);}\
\else {cb({'type':'http', 'status':xhr.status, 'status-text': xhr.statusText}, null);}\
\});\
\xhr.addEventListener('error', function() {\
\if(xhr.status != 0) {\
\cb({'type':'http', 'status':xhr.status, 'status-text': xhr.statusText}, null);\
\} else {\
\cb({'type':'network'}, null);\
\}\
\});\
\xhr.send(postdata);\
\})"