{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
#ifdef USE_TEMPLATE_HASKELL
{-# LANGUAGE TemplateHaskell #-}
#endif
module Reflex.Dom.Xhr
(
getAndDecode
, getMay
, postJson
, decodeXhrResponse
, decodeText
, XhrRequest (..)
, XhrRequestConfig (..)
, xhrRequest
, xhrRequestConfig_headers
, xhrRequestConfig_password
, xhrRequestConfig_responseType
, xhrRequestConfig_sendData
, xhrRequestConfig_user
, xhrRequestConfig_withCredentials
, xhrRequestConfig_responseHeaders
, xhrRequest_config
, xhrRequest_method
, xhrRequest_url
, performMkRequestAsync
, performMkRequestsAsync
, performRequestAsync
, performRequestAsyncWithError
, performRequestsAsync
, performRequestsAsyncWithError
, XhrResponse (..)
, XhrResponseBody (..)
, XhrResponseHeaders (..)
, XhrResponseType (..)
, xhrResponse_response
, xhrResponse_responseText
, xhrResponse_status
, xhrResponse_statusText
, xhrResponse_headers
, xhrResponse_body
, _xhrResponse_body
, XhrException (..)
, IsXhrPayload (..)
, XMLHttpRequest
, newXMLHttpRequest
, newXMLHttpRequestWithError
, xmlHttpRequestGetReadyState
, xmlHttpRequestGetResponseText
, xmlHttpRequestGetStatus
, xmlHttpRequestGetStatusText
, xmlHttpRequestNew
, xmlHttpRequestOnreadystatechange
, xmlHttpRequestOpen
, xmlHttpRequestSetRequestHeader
, xmlHttpRequestSetResponseType
)
where
import Reflex.Class
import Reflex.Dom.Class
import Reflex.PerformEvent.Class
import Reflex.TriggerEvent.Class
import Reflex.Dom.Xhr.Exception
import Reflex.Dom.Xhr.Foreign
import Reflex.Dom.Xhr.ResponseType
import Control.Concurrent
import Control.Exception (handle)
import Control.Lens
import Control.Monad hiding (forM)
import Control.Monad.IO.Class
import Data.Aeson
#if MIN_VERSION_aeson(1,0,0)
import Data.Aeson.Text
#else
import Data.Aeson.Encode
#endif
import qualified Data.ByteString.Lazy as BL
import Data.Default
import qualified Data.List as L
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Maybe
import qualified Data.Set as Set
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding
import qualified Data.Text.Lazy as LT
import qualified Data.Text.Lazy.Builder as B
import Data.Traversable
import Data.Typeable
import Language.Javascript.JSaddle.Monad (JSM, askJSM, runJSM, MonadJSM, liftJSM)
data XhrRequest a
= XhrRequest { _xhrRequest_method :: Text
, _xhrRequest_url :: Text
, _xhrRequest_config :: XhrRequestConfig a
}
deriving (Show, Read, Eq, Ord, Typeable, Functor)
data XhrRequestConfig a
= XhrRequestConfig { _xhrRequestConfig_headers :: Map Text Text
, _xhrRequestConfig_user :: Maybe Text
, _xhrRequestConfig_password :: Maybe Text
, _xhrRequestConfig_responseType :: Maybe XhrResponseType
, _xhrRequestConfig_sendData :: a
, _xhrRequestConfig_withCredentials :: Bool
, _xhrRequestConfig_responseHeaders :: XhrResponseHeaders
}
deriving (Show, Read, Eq, Ord, Typeable, Functor)
data XhrResponse
= XhrResponse { _xhrResponse_status :: Word
, _xhrResponse_statusText :: Text
, _xhrResponse_response :: Maybe XhrResponseBody
, _xhrResponse_responseText :: Maybe Text
, _xhrResponse_headers :: Map Text Text
}
deriving (Typeable)
data XhrResponseHeaders =
OnlyHeaders (Set.Set Text)
| AllHeaders
deriving (Show, Read, Eq, Ord, Typeable)
instance Default XhrResponseHeaders where
def = OnlyHeaders mempty
{-# DEPRECATED _xhrResponse_body "Use _xhrResponse_response or _xhrResponse_responseText instead." #-}
_xhrResponse_body :: XhrResponse -> Maybe Text
_xhrResponse_body = _xhrResponse_responseText
{-# DEPRECATED xhrResponse_body "Use xhrResponse_response or xhrResponse_responseText instead." #-}
xhrResponse_body :: Lens' XhrResponse (Maybe Text)
xhrResponse_body = lens _xhrResponse_responseText (\r t -> r { _xhrResponse_responseText = t })
instance a ~ () => Default (XhrRequestConfig a) where
def = XhrRequestConfig { _xhrRequestConfig_headers = Map.empty
, _xhrRequestConfig_user = Nothing
, _xhrRequestConfig_password = Nothing
, _xhrRequestConfig_responseType = Nothing
, _xhrRequestConfig_sendData = ()
, _xhrRequestConfig_withCredentials = False
, _xhrRequestConfig_responseHeaders = def
}
xhrRequest :: Text -> Text -> XhrRequestConfig a -> XhrRequest a
xhrRequest = XhrRequest
newXMLHttpRequestWithError
:: (HasJSContext m, MonadJSM m, IsXhrPayload a)
=> XhrRequest a
-> (Either XhrException XhrResponse -> JSM ())
-> m XMLHttpRequest
newXMLHttpRequestWithError req cb = do
xhr <- xmlHttpRequestNew
ctx <- askJSM
void $ liftIO $ forkIO $ handle ((`runJSM` ctx) . cb . Left) $ void . (`runJSM` ctx) $ do
let c = _xhrRequest_config req
rt = _xhrRequestConfig_responseType c
creds = _xhrRequestConfig_withCredentials c
xmlHttpRequestOpen
xhr
(_xhrRequest_method req)
(_xhrRequest_url req)
True
(fromMaybe "" $ _xhrRequestConfig_user c)
(fromMaybe "" $ _xhrRequestConfig_password c)
iforM_ (_xhrRequestConfig_headers c) $ xmlHttpRequestSetRequestHeader xhr
maybe (return ()) (xmlHttpRequestSetResponseType xhr . fromResponseType) rt
xmlHttpRequestSetWithCredentials xhr creds
_ <- xmlHttpRequestOnreadystatechange xhr $ do
readyState <- xmlHttpRequestGetReadyState xhr
status <- xmlHttpRequestGetStatus xhr
statusText <- xmlHttpRequestGetStatusText xhr
when (readyState == 4) $ do
t <- if rt == Just XhrResponseType_Text || isNothing rt
then xmlHttpRequestGetResponseText xhr
else return Nothing
r <- xmlHttpRequestGetResponse xhr
h <- case _xhrRequestConfig_responseHeaders c of
AllHeaders -> parseAllHeadersString <$>
xmlHttpRequestGetAllResponseHeaders xhr
OnlyHeaders xs -> traverse (xmlHttpRequestGetResponseHeader xhr)
(Map.fromSet id xs)
_ <- liftJSM $ cb $ Right
XhrResponse { _xhrResponse_status = status
, _xhrResponse_statusText = statusText
, _xhrResponse_response = r
, _xhrResponse_responseText = t
, _xhrResponse_headers = h
}
return ()
_ <- xmlHttpRequestSend xhr (_xhrRequestConfig_sendData c)
return ()
return xhr
parseAllHeadersString :: Text -> Map Text Text
parseAllHeadersString s = Map.fromList $ fmap (stripBoth . T.span (/=':')) $
L.dropWhileEnd T.null $ T.splitOn (T.pack "\r\n") s
where stripBoth (txt1, txt2) = (T.strip txt1, T.strip $ T.drop 1 txt2)
newXMLHttpRequest :: (HasJSContext m, MonadJSM m, IsXhrPayload a) => XhrRequest a -> (XhrResponse -> JSM ()) -> m XMLHttpRequest
newXMLHttpRequest req cb = newXMLHttpRequestWithError req $ mapM_ cb
performRequestAsyncWithError
:: (MonadJSM (Performable m), HasJSContext (Performable m), PerformEvent t m, TriggerEvent t m, IsXhrPayload a)
=> Event t (XhrRequest a)
-> m (Event t (Either XhrException XhrResponse))
performRequestAsyncWithError = performRequestAsync' newXMLHttpRequestWithError . fmap return
performRequestAsync :: (MonadJSM (Performable m), HasJSContext (Performable m), PerformEvent t m, TriggerEvent t m, IsXhrPayload a) => Event t (XhrRequest a) -> m (Event t XhrResponse)
performRequestAsync = performRequestAsync' newXMLHttpRequest . fmap return
performMkRequestAsync :: (MonadJSM (Performable m), HasJSContext (Performable m), PerformEvent t m, TriggerEvent t m, IsXhrPayload a) => Event t (Performable m (XhrRequest a)) -> m (Event t XhrResponse)
performMkRequestAsync = performRequestAsync' newXMLHttpRequest
performRequestAsync' :: (MonadJSM (Performable m), PerformEvent t m, TriggerEvent t m) => (XhrRequest p -> (a -> JSM ()) -> Performable m XMLHttpRequest) -> Event t (Performable m (XhrRequest p)) -> m (Event t a)
performRequestAsync' newXhr req = performEventAsync $ ffor req $ \hr cb -> do
r <- hr
_ <- newXhr r $ liftIO . cb
return ()
performRequestsAsyncWithError
:: (MonadJSM (Performable m), HasJSContext (Performable m), PerformEvent t m, TriggerEvent t m, Traversable f, IsXhrPayload a)
=> Event t (f (XhrRequest a)) -> m (Event t (f (Either XhrException XhrResponse)))
performRequestsAsyncWithError = performRequestsAsync' newXMLHttpRequestWithError . fmap return
performRequestsAsync :: (MonadJSM (Performable m), HasJSContext (Performable m), PerformEvent t m, TriggerEvent t m, Traversable f, IsXhrPayload a) => Event t (f (XhrRequest a)) -> m (Event t (f XhrResponse))
performRequestsAsync = performRequestsAsync' newXMLHttpRequest . fmap return
performMkRequestsAsync :: (MonadJSM (Performable m), HasJSContext (Performable m), PerformEvent t m, TriggerEvent t m, Traversable f, IsXhrPayload a) => Event t (Performable m (f (XhrRequest a))) -> m (Event t (f XhrResponse))
performMkRequestsAsync = performRequestsAsync' newXMLHttpRequest
performRequestsAsync' :: (MonadJSM (Performable m), PerformEvent t m, TriggerEvent t m, Traversable f) => (XhrRequest b -> (a -> JSM ()) -> Performable m XMLHttpRequest) -> Event t (Performable m (f (XhrRequest b))) -> m (Event t (f a))
performRequestsAsync' newXhr req = performEventAsync $ ffor req $ \hrs cb -> do
rs <- hrs
resps <- forM rs $ \r -> do
resp <- liftIO newEmptyMVar
_ <- newXhr r $ liftIO . putMVar resp
return resp
_ <- liftIO $ forkIO $ cb =<< forM resps takeMVar
return ()
getAndDecode :: (MonadIO m, MonadJSM (Performable m), PerformEvent t m, HasJSContext (Performable m), TriggerEvent t m, FromJSON a) => Event t Text -> m (Event t (Maybe a))
getAndDecode url = do
r <- performRequestAsync $ fmap (\x -> XhrRequest "GET" x def) url
return $ fmap decodeXhrResponse r
postJson :: (ToJSON a) => Text -> a -> XhrRequest Text
postJson url a =
XhrRequest "POST" url $ def { _xhrRequestConfig_headers = headerUrlEnc
, _xhrRequestConfig_sendData = body
}
where headerUrlEnc = "Content-type" =: "application/json"
body = LT.toStrict $ B.toLazyText $ encodeToTextBuilder $ toJSON a
getMay :: (Monad m, Reflex t) => (Event t a -> m (Event t b)) -> Event t (Maybe a) -> m (Event t (Maybe b))
getMay f e = do
e' <- f (fmapMaybe id e)
return $ leftmost [fmap Just e', fmapMaybe (maybe (Just Nothing) (const Nothing)) e]
decodeText :: FromJSON a => Text -> Maybe a
decodeText = decode . BL.fromStrict . encodeUtf8
decodeXhrResponse :: FromJSON a => XhrResponse -> Maybe a
decodeXhrResponse = decodeText <=< _xhrResponse_responseText
#ifdef USE_TEMPLATE_HASKELL
concat <$> mapM makeLenses
[ ''XhrRequest
, ''XhrRequestConfig
, ''XhrResponse
]
#else
xhrRequest_method :: Lens' (XhrRequest a) Text
xhrRequest_method f (XhrRequest x1 x2 x3) = (\y -> XhrRequest y x2 x3) <$> f x1
{-# INLINE xhrRequest_method #-}
xhrRequest_url :: Lens' (XhrRequest a) Text
xhrRequest_url f (XhrRequest x1 x2 x3) = (\y -> XhrRequest x1 y x3) <$> f x2
{-# INLINE xhrRequest_url #-}
xhrRequest_config :: Lens' (XhrRequest a) (XhrRequestConfig a)
xhrRequest_config f (XhrRequest x1 x2 x3) = (\y -> XhrRequest x1 x2 y) <$> f x3
{-# INLINE xhrRequest_config #-}
xhrRequestConfig_headers :: Lens' (XhrRequestConfig a) (Map Text Text)
xhrRequestConfig_headers f (XhrRequestConfig x1 x2 x3 x4 x5 x6 x7) = (\y -> XhrRequestConfig y x2 x3 x4 x5 x6 x7) <$> f x1
{-# INLINE xhrRequestConfig_headers #-}
xhrRequestConfig_user :: Lens' (XhrRequestConfig a) (Maybe Text)
xhrRequestConfig_user f (XhrRequestConfig x1 x2 x3 x4 x5 x6 x7) = (\y -> XhrRequestConfig x1 y x3 x4 x5 x6 x7) <$> f x2
{-# INLINE xhrRequestConfig_user #-}
xhrRequestConfig_password :: Lens' (XhrRequestConfig a) (Maybe Text)
xhrRequestConfig_password f (XhrRequestConfig x1 x2 x3 x4 x5 x6 x7) = (\y -> XhrRequestConfig x1 x2 y x4 x5 x6 x7) <$> f x3
{-# INLINE xhrRequestConfig_password #-}
xhrRequestConfig_responseType :: Lens' (XhrRequestConfig a) (Maybe XhrResponseType)
xhrRequestConfig_responseType f (XhrRequestConfig x1 x2 x3 x4 x5 x6 x7) = (\y -> XhrRequestConfig x1 x2 x3 y x5 x6 x7) <$> f x4
{-# INLINE xhrRequestConfig_responseType #-}
xhrRequestConfig_sendData :: Lens (XhrRequestConfig a) (XhrRequestConfig b) a b
xhrRequestConfig_sendData f (XhrRequestConfig x1 x2 x3 x4 x5 x6 x7) = (\y -> XhrRequestConfig x1 x2 x3 x4 y x6 x7) <$> f x5
{-# INLINE xhrRequestConfig_sendData #-}
xhrRequestConfig_withCredentials :: Lens' (XhrRequestConfig a) Bool
xhrRequestConfig_withCredentials f (XhrRequestConfig x1 x2 x3 x4 x5 x6 x7) = (\y -> XhrRequestConfig x1 x2 x3 x4 x5 y x7) <$> f x6
{-# INLINE xhrRequestConfig_withCredentials #-}
xhrRequestConfig_responseHeaders :: Lens' (XhrRequestConfig a) XhrResponseHeaders
xhrRequestConfig_responseHeaders f (XhrRequestConfig x1 x2 x3 x4 x5 x6 x7) = (\y -> XhrRequestConfig x1 x2 x3 x4 x5 x6 y) <$> f x7
{-# INLINE xhrRequestConfig_responseHeaders #-}
xhrResponse_status :: Lens' XhrResponse Word
xhrResponse_status f (XhrResponse x1 x2 x3 x4 x5) = (\y -> XhrResponse y x2 x3 x4 x5) <$> f x1
{-# INLINE xhrResponse_status #-}
xhrResponse_statusText :: Lens' XhrResponse Text
xhrResponse_statusText f (XhrResponse x1 x2 x3 x4 x5) = (\y -> XhrResponse x1 y x3 x4 x5) <$> f x2
{-# INLINE xhrResponse_statusText #-}
xhrResponse_response :: Lens' XhrResponse (Maybe XhrResponseBody)
xhrResponse_response f (XhrResponse x1 x2 x3 x4 x5) = (\y -> XhrResponse x1 x2 y x4 x5) <$> f x3
{-# INLINE xhrResponse_response #-}
xhrResponse_responseText :: Lens' XhrResponse (Maybe Text)
xhrResponse_responseText f (XhrResponse x1 x2 x3 x4 x5) = (\y -> XhrResponse x1 x2 x3 y x5) <$> f x4
{-# INLINE xhrResponse_responseText #-}
xhrResponse_headers :: Lens' XhrResponse (Map Text Text)
xhrResponse_headers f (XhrResponse x1 x2 x3 x4 x5) = (\y -> XhrResponse x1 x2 x3 x4 y) <$> f x5
{-# INLINE xhrResponse_headers #-}
#endif