@@ -1,45 +1,66 @@
1−{-# LANGUAGE CPP #-}
1+{-# LANGUAGE CPP #-}
2−{-# LANGUAGE DeriveFunctor #-}
2+{-# LANGUAGE DeriveFunctor #-}
3+{-# LANGUAGE ExistentialQuantification #-}
3−{-# LANGUAGE FlexibleContexts #-}
4+{-# LANGUAGE FlexibleContexts #-}
5+{-# LANGUAGE FlexibleInstances #-}
6+{-# LANGUAGE FunctionalDependencies #-}
4−{-# LANGUAGE GADTs #-}
7+{-# LANGUAGE GADTs #-}
8+{-# LANGUAGE LambdaCase #-}
9+{-# LANGUAGE MultiParamTypeClasses #-}
10+{-# LANGUAGE NamedFieldPuns #-}
5−{-# LANGUAGE OverloadedStrings #-}
11+{-# LANGUAGE OverloadedStrings #-}
6−{-# LANGUAGE LambdaCase #-}
7−{-# LANGUAGE RankNTypes #-}
12+{-# LANGUAGE RankNTypes #-}
8−{-# LANGUAGE ScopedTypeVariables #-}
13+{-# LANGUAGE ScopedTypeVariables #-}
9−{-# LANGUAGE TupleSections #-}
14+{-# LANGUAGE TupleSections #-}
15+{-# OPTIONS_GHC -fno-warn-orphans #-}
11module Servant.Common.Req where
17module Servant.Common.Req where
13-------------------------------------------------------------------------------
19-------------------------------------------------------------------------------
14−import Control.Applicative (liftA2, liftA3)
20+import Control.Applicative (liftA2, liftA3)
15−import Control.Arrow ((&&&))
21+import Control.Arrow ((&&&))
16import Control.Concurrent
22import Control.Concurrent
17−import Control.Monad (join)
23+import Control.Monad (join)
18−import Control.Monad.IO.Class (MonadIO, liftIO)
24+import Control.Monad.IO.Class (MonadIO, liftIO)
19−import Data.Bifunctor (first)
26+import Data.Bifunctor (first)
20−import qualified Data.ByteString.Builder as Builder
27+import qualified Data.ByteString as BS
28+import qualified Data.ByteString.Builder as Builder
29+import Data.ByteString.Lazy (ByteString)
21−import qualified Data.ByteString.Lazy.Char8 as BL
30+import qualified Data.ByteString.Lazy.Char8 as BL
22−import qualified Data.Map as Map
23−import Data.Maybe (catMaybes, fromMaybe)
24import Data.Functor.Compose
31import Data.Functor.Compose
32+import qualified Data.JSString.Text as JS (lazyTextFromJSString)
33+import qualified Data.Map as Map
34+import Data.Maybe (catMaybes, fromMaybe)
25−import Data.Monoid ((<>))
35+import Data.Monoid ((<>))
26−import Data.Proxy (Proxy(..))
36+import Data.Proxy (Proxy (..))
27−import Data.Text (Text)
37+import Data.Text (Text)
28−import qualified Data.Text as T
38+import qualified Data.Text as T
29−import qualified Data.Text.Encoding as TE
39+import qualified Data.Text.Encoding as TE
40+import qualified Data.Text.Lazy as TL
30−import Data.Traversable (forM)
41+import Data.Traversable (forM)
42+import qualified Language.Javascript.JSaddle as JS
31import Language.Javascript.JSaddle.Monad (JSM, MonadJSM, liftJSM)
43import Language.Javascript.JSaddle.Monad (JSM, MonadJSM, liftJSM)
32−import qualified Network.URI as N
44+import qualified Network.URI as N
33−import Reflex.Dom.Core hiding (tag)
45+import Reflex.Dom.Core hiding (tag)
34−import Servant.Common.BaseUrl (BaseUrl, showBaseUrl,
46+import Servant.API.ContentTypes (Accept, FormUrlEncoded,
35− SupportsServantReflex)
47+ JSON, NoContent (..),
36−import Servant.API.ContentTypes (MimeUnrender(..), NoContent(..))
48+ OctetStream, PlainText)
49+import Servant.Common.BaseUrl (BaseUrl,
50+ SupportsServantReflex,
52+import Web.FormUrlEncoded (FromForm, urlDecodeAsForm)
37−import Web.HttpApiData (ToHttpApiData(..))
53+import Web.HttpApiData (ToHttpApiData (..))
38-------------------------------------------------------------------------------
54-------------------------------------------------------------------------------
39import Servant.API.BasicAuth
55import Servant.API.BasicAuth
56+-------------------------------------------------------------------------------
59+-- jsonDecode :: JS.JSString -> Maybe a
60+-- jsonDecode = error "jsonDecode is only available in GHCJS"
42−------------------------------------------------------------------------------
63+-----------------------------------------------------------------------------
43-- | The result of a request event
64-- | The result of a request event
44data ReqResult tag a
65data ReqResult tag a
45 = ResponseSuccess tag a XhrResponse
66 = ResponseSuccess tag a XhrResponse
@@ -140,18 +161,20 @@ data QueryPart t = QueryPartParam (Dynamic t (Either Text (Maybe Text)))
140-------------------------------------------------------------------------------
161-------------------------------------------------------------------------------
141-- The data structure used to build up request information while traversing
162-- The data structure used to build up request information while traversing
142-- the shape of a servant API
163-- the shape of a servant API
143−data Req t = Req
164+data Req t = forall o. (IsXhrPayload o, Show o) => Req
144 { reqMethod :: Text
165 { reqMethod :: Text
145 , reqPathParts :: [Dynamic t (Either Text Text)]
166 , reqPathParts :: [Dynamic t (Either Text Text)]
146 , qParams :: [(Text, QueryPart t)]
167 , qParams :: [(Text, QueryPart t)]
147− , reqBody :: Maybe (Dynamic t (Either Text (BL.ByteString, Text)))
168+ , reqBody :: Maybe (Dynamic t (Either Text (o, Text)))
148 , headers :: [(Text, Dynamic t (Either Text Text))]
169 , headers :: [(Text, Dynamic t (Either Text Text))]
149 , respHeaders :: XhrResponseHeaders
170 , respHeaders :: XhrResponseHeaders
150 , authData :: Maybe (Dynamic t (Maybe BasicAuthData))
171 , authData :: Maybe (Dynamic t (Maybe BasicAuthData))
174+data SomeXhrRequest = forall o. (IsXhrPayload o, Show o) => SomeXhrRequest (XhrRequest (Maybe o))
153defReq :: Req t
176defReq :: Req t
154−defReq = Req "GET" [] [] Nothing [] def Nothing
177+defReq = Req "GET" [] [] (Nothing :: Maybe (Dynamic t (Either Text ((), Text)))) [] def Nothing
156prependToPathParts :: Dynamic t (Either Text Text) -> Req t -> Req t
179prependToPathParts :: Dynamic t (Either Text Text) -> Req t -> Req t
157prependToPathParts p req =
180prependToPathParts p req =
@@ -160,14 +183,13 @@ prependToPathParts p req =
160addHeader :: (ToHttpApiData a, Reflex t) => Text -> Dynamic t (Either Text a) -> Req t -> Req t
183addHeader :: (ToHttpApiData a, Reflex t) => Text -> Dynamic t (Either Text a) -> Req t -> Req t
161addHeader name val req = req { headers = (name, (fmap . fmap) (TE.decodeUtf8 . toHeader) val) : headers req }
184addHeader name val req = req { headers = (name, (fmap . fmap) (TE.decodeUtf8 . toHeader) val) : headers req }
164reqToReflexRequest
186reqToReflexRequest
165− :: forall t. Reflex t
187+ :: forall t. Reflex t
167− -> Dynamic t BaseUrl
189+ -> Dynamic t BaseUrl
168− -> Req t
190+ -> Req t
169− -> Dynamic t (Either Text (XhrRequest XhrPayload))
191+ -> Dynamic t (Either Text SomeXhrRequest)
170−reqToReflexRequest reqMeth reqHost req =
192+reqToReflexRequest reqMeth reqHost req@(Req _ _ _ (reqBody :: Maybe (Dynamic t (Either Text (o, Text)))) _ _ _) =
171 let t :: Dynamic t [Either Text Text]
193 let t :: Dynamic t [Either Text Text]
172 t = sequence $ reverse $ reqPathParts req
194 t = sequence $ reverse $ reqPathParts req
@@ -219,33 +241,33 @@ reqToReflexRequest reqMeth reqHost req =
219 fmap (fmap (\rightVal -> (headerName, rightVal))) dynam
241 fmap (fmap (\rightVal -> (headerName, rightVal))) dynam
221 mkConfigBody :: Either Text [(Text,Text)]
243 mkConfigBody :: Either Text [(Text,Text)]
222− -> (Either Text (BL.ByteString, Text))
244+ -> (Either Text (o, Text))
223− -> Either Text (XhrRequestConfig XhrPayload)
245+ -> Either Text (XhrRequestConfig (Maybe o))
224 mkConfigBody ehs rb = case (ehs, rb) of
246 mkConfigBody ehs rb = case (ehs, rb) of
225 (_, Left e) -> Left e
247 (_, Left e) -> Left e
226 (Left e, _) -> Left e
248 (Left e, _) -> Left e
227 (Right hs, Right (bBytes, bCT)) ->
249 (Right hs, Right (bBytes, bCT)) ->
228 Right $ XhrRequestConfig
250 Right $ XhrRequestConfig
229− { _xhrRequestConfig_sendData = bytesToPayload bBytes
251+ { _xhrRequestConfig_sendData = Just bBytes
230 , _xhrRequestConfig_headers =
252 , _xhrRequestConfig_headers =
231 Map.insert "Content-Type" bCT (Map.fromList hs)
253 Map.insert "Content-Type" bCT (Map.fromList hs)
232 , _xhrRequestConfig_user = Nothing
254 , _xhrRequestConfig_user = Nothing
233 , _xhrRequestConfig_password = Nothing
255 , _xhrRequestConfig_password = Nothing
234− , _xhrRequestConfig_responseType = Just XhrResponseType_ArrayBuffer
256+ , _xhrRequestConfig_responseType = Nothing
235 , _xhrRequestConfig_withCredentials = False
257 , _xhrRequestConfig_withCredentials = False
236 , _xhrRequestConfig_responseHeaders = def
258 , _xhrRequestConfig_responseHeaders = def
239− xhrOpts :: Dynamic t (Either Text (XhrRequestConfig XhrPayload))
261+ xhrOpts :: Dynamic t (Either Text (XhrRequestConfig (Maybe o)))
240− xhrOpts = case reqBody req of
262+ xhrOpts = case reqBody of
241 Nothing -> ffor xhrHeaders $ \case
263 Nothing -> ffor xhrHeaders $ \case
242 Left e -> Left e
264 Left e -> Left e
243 Right hs -> Right $ def { _xhrRequestConfig_headers = Map.fromList hs
265 Right hs -> Right $ def { _xhrRequestConfig_headers = Map.fromList hs
244 , _xhrRequestConfig_user = Nothing
266 , _xhrRequestConfig_user = Nothing
245 , _xhrRequestConfig_password = Nothing
267 , _xhrRequestConfig_password = Nothing
246− , _xhrRequestConfig_responseType = Just XhrResponseType_ArrayBuffer
268+ , _xhrRequestConfig_responseType = Nothing
247− , _xhrRequestConfig_sendData = ""
248 , _xhrRequestConfig_withCredentials = False
269 , _xhrRequestConfig_withCredentials = False
270+ , _xhrRequestConfig_sendData = Nothing
250 Just rBody -> liftA2 mkConfigBody xhrHeaders rBody
272 Just rBody -> liftA2 mkConfigBody xhrHeaders rBody
@@ -259,10 +281,10 @@ reqToReflexRequest reqMeth reqHost req =
259 addAuth :: Dynamic t (Either Text (XhrRequestConfig x))
281 addAuth :: Dynamic t (Either Text (XhrRequestConfig x))
260 -> Dynamic t (Either Text (XhrRequestConfig x))
282 -> Dynamic t (Either Text (XhrRequestConfig x))
261 addAuth xhr = case authData req of
283 addAuth xhr = case authData req of
262− Nothing -> xhr
284+ Nothing -> xhr
263 Just auth -> liftA2 mkAuth auth xhr
285 Just auth -> liftA2 mkAuth auth xhr
265− xhrReq = (liftA2 . liftA2) (\p opt -> XhrRequest reqMeth p opt) xhrUrl (addAuth xhrOpts)
287+ xhrReq = (liftA2 . liftA2) (\p opt -> SomeXhrRequest $ XhrRequest reqMeth p opt) xhrUrl (addAuth xhrOpts)
267 in xhrReq
289 in xhrReq
@@ -271,8 +293,14 @@ reqToReflexRequest reqMeth reqHost req =
271displayHttpRequest :: Text -> Text
293displayHttpRequest :: Text -> Text
272displayHttpRequest httpmethod = "HTTP " <> httpmethod <> " request"
294displayHttpRequest httpmethod = "HTTP " <> httpmethod <> " request"
296+instance IsXhrPayload o => IsXhrPayload (Maybe o) where
297+ sendXhrPayload xhr = maybe (sendXhrPayload xhr ()) (sendXhrPayload xhr)
299+instance (IsXhrPayload o, IsXhrPayload o') => IsXhrPayload (Either o o') where
300+ sendXhrPayload xhr = either (sendXhrPayload xhr) (sendXhrPayload xhr)
274-- | This function performs the request
302-- | This function performs the request
275−performRequests :: forall t m f tag.(SupportsServantReflex t m, Traversable f)
303+performRequests :: forall t m f tag. (SupportsServantReflex t m, Traversable f)
277 -> Dynamic t (f (Req t))
305 -> Dynamic t (f (Req t))
278 -> Dynamic t BaseUrl
306 -> Dynamic t BaseUrl
@@ -285,6 +313,7 @@ performRequests reqMeth rs reqHost opts trigger = do
285 reqToReflexRequest reqMeth reqHost <$> fxhr) <$> rs
313 reqToReflexRequest reqMeth reqHost <$> fxhr) <$> rs
287 -- xhrReqs = fmap snd <$> xhrReqsAndDebugs
315 -- xhrReqs = fmap snd <$> xhrReqsAndDebugs
316+ reqs :: Event t (Compose ((,) tag) f (Either Text SomeXhrRequest))
288 reqs = attachPromptlyDynWith
317 reqs = attachPromptlyDynWith
289 (\fxhr t -> Compose (t, fxhr)) xhrReqs trigger
318 (\fxhr t -> Compose (t, fxhr)) xhrReqs trigger
@@ -300,32 +329,31 @@ performSomeRequestsAsync
300 HasWebView (Performable m),
329 HasWebView (Performable m),
301 PerformEvent t m,
330 PerformEvent t m,
302 TriggerEvent t m,
331 TriggerEvent t m,
303− Traversable f,
332+ Traversable f
307 => ClientOptions
334 => ClientOptions
308− -> Event t (f (Either Text (XhrRequest a)))
335+ -> Event t (f (Either Text SomeXhrRequest))
309 -> m (Event t (f (Either Text XhrResponse)))
336 -> m (Event t (f (Either Text XhrResponse)))
310−performSomeRequestsAsync opts =
337+performSomeRequestsAsync opts sxhr =
311− performSomeRequestsAsync' opts newXMLHttpRequest . fmap return
338+ performSomeRequestsAsync' opts newXMLHttpRequest $ return <$> sxhr
314------------------------------------------------------------------------------
341------------------------------------------------------------------------------
315-- | A modified version or Reflex.Dom.Xhr.performRequestsAsync
342-- | A modified version or Reflex.Dom.Xhr.performRequestsAsync
316-- that accepts 'f (Either e (XhrRequestb))' events
343-- that accepts 'f (Either e (XhrRequestb))' events
317performSomeRequestsAsync'
344performSomeRequestsAsync'
318− :: (MonadJSM (Performable m), PerformEvent t m, TriggerEvent t m, Traversable f, Show b)
345+ :: (MonadJSM (Performable m), PerformEvent t m, TriggerEvent t m, Traversable f)
319 => ClientOptions
346 => ClientOptions
320− -> (XhrRequest b -> (a -> JSM ()) -> Performable m XMLHttpRequest)
347+ -> (forall b. IsXhrPayload b => XhrRequest b -> (a -> JSM ()) -> Performable m XMLHttpRequest)
321− -> Event t (Performable m (f (Either Text (XhrRequest b)))) -> m (Event t (f (Either Text a)))
348+ -> Event t (Performable m (f (Either Text SomeXhrRequest)))
349+ -> m (Event t (f (Either Text a)))
322performSomeRequestsAsync' opts newXhr req = performEventAsync $ ffor req $ \hrs cb -> do
350performSomeRequestsAsync' opts newXhr req = performEventAsync $ ffor req $ \hrs cb -> do
323 rs <- hrs
351 rs <- hrs
324 resps <- forM rs $ \r -> case r of
352 resps <- forM rs $ \r -> case r of
325 Left e -> do
353 Left e -> do
326 resp <- liftIO $ newMVar (Left e)
354 resp <- liftIO $ newMVar (Left e)
327 return resp
355 return resp
328− Right r' -> do
356+ Right (SomeXhrRequest r') -> do
329 resp <- liftIO newEmptyMVar
357 resp <- liftIO newEmptyMVar
330 r'' <- liftJSM $ (optsRequestFixup opts) r'
358 r'' <- liftJSM $ (optsRequestFixup opts) r'
331 _ <- newXhr r'' $ liftIO . putMVar resp . Right
359 _ <- newXhr r'' $ liftIO . putMVar resp . Right
@@ -334,12 +362,6 @@ performSomeRequestsAsync' opts newXhr req = performEventAsync $ ffor req $ \hrs
334 return ()
362 return ()
338−type XhrPayload = T.Text
339−bytesToPayload :: BL.ByteString -> XhrPayload
340−bytesToPayload = TE.decodeUtf8 . BL.toStrict
343performRequestsCT
365performRequestsCT
344 :: (SupportsServantReflex t m,
366 :: (SupportsServantReflex t m,
345 MimeUnrender ct a, Traversable f)
367 MimeUnrender ct a, Traversable f)
@@ -352,15 +374,14 @@ performRequestsCT
352 -> m (Event t (f (ReqResult tag a)))
374 -> m (Event t (f (ReqResult tag a)))
353performRequestsCT ct reqMeth reqs reqHost opts trigger = do
375performRequestsCT ct reqMeth reqs reqHost opts trigger = do
354 resps <- performRequests reqMeth reqs reqHost opts trigger
376 resps <- performRequests reqMeth reqs reqHost opts trigger
355− let decodeResp (Just (XhrResponseBody_ArrayBuffer x)) =
356− first T.pack .
377+ let decodeResp x = first T.pack
357− mimeUnrender ct $
378+ . mimeUnrender ct
358− BL.fromStrict x
379+ . JS.textToJSString
359− decodeResp _ = Left "No body"
380+ =<< note "No body text" (_xhrResponse_responseText x)
360 return $ fmap
381 return $ fmap
361 (\(t,rs) -> ffor rs $ \r -> case r of
382 (\(t,rs) -> ffor rs $ \r -> case r of
362 Left e -> RequestFailure t e
383 Left e -> RequestFailure t e
363− Right g -> evalResponse (decodeResp . _xhrResponse_response) (t,g)
384+ Right g -> evalResponse decodeResp (t,g)
@@ -387,7 +408,7 @@ evalResponse
387 :: (XhrResponse -> Either Text a)
408 :: (XhrResponse -> Either Text a)
388 -> (tag, XhrResponse)
409 -> (tag, XhrResponse)
389 -> ReqResult tag a
410 -> ReqResult tag a
390−evalResponse decode (tag, xhr) =
411+evalResponse decodeRes (tag, xhr) =
391 let status = _xhrResponse_status xhr
412 let status = _xhrResponse_status xhr
392 okStatus = status >= 200 && status < 400
413 okStatus = status >= 200 && status < 400
393 errMsg = fromMaybe
414 errMsg = fromMaybe
@@ -398,7 +419,7 @@ evalResponse decode (tag, xhr) =
398 then either
419 then either
399 (\e -> ResponseFailure tag e xhr)
420 (\e -> ResponseFailure tag e xhr)
400 (\v -> ResponseSuccess tag v xhr)
421 (\v -> ResponseSuccess tag v xhr)
401− (decode xhr)
422+ (decodeRes xhr)
402 else ResponseFailure tag errMsg xhr
423 else ResponseFailure tag errMsg xhr
403 in respPayld
424 in respPayld
@@ -415,8 +436,44 @@ fanReqResult' :: Reflex t => Event t (ReqResult tag a) -> (Event t (tag, Text),
415fanReqResult' = fmapMaybe reqFailure' &&& fmapMaybe reqSuccess'
436fanReqResult' = fmapMaybe reqFailure' &&& fmapMaybe reqSuccess'
439+-- | Similar to 'Servant.API.ContentType.MimeUnrender' but with the differnce that
440+-- the second argument expects a JSString instead of ByteString.
441+-- This can lead to better performance ghcjs.
442+class Accept contentType => MimeUnrender contentType a where
443+ mimeUnrender :: Proxy contentType -> JS.JSString -> Either String a
445+instance FromJSON a => MimeUnrender JSON a where
446+ mimeUnrender _ = note "Failed to decode response" . jsonDecode
448+-- | @urlDecodeAsForm@
449+-- Note that the @mimeUnrender p (mimeRender p x) == Right x@ law only
450+-- holds if every element of x is non-null (i.e., not @("", "")@)
451+instance FromForm a => MimeUnrender FormUrlEncoded a where
452+ mimeUnrender _ = first T.unpack . urlDecodeAsForm . Builder.toLazyByteString . TE.encodeUtf8Builder . JS.textFromJSString
454+-- | @left show . TextL.decodeUtf8'@
455+instance MimeUnrender PlainText TL.Text where
456+ mimeUnrender _ = pure . JS.lazyTextFromJSString
458+-- | @left show . TextS.decodeUtf8' . toStrict@
459+instance MimeUnrender PlainText T.Text where
460+ mimeUnrender _ = pure . JS.textFromJSString
462+-- | @Right . BC.unpack@
463+instance MimeUnrender PlainText String where
464+ mimeUnrender _ = Right . T.unpack . JS.textFromJSString
467+instance MimeUnrender OctetStream ByteString where
468+ mimeUnrender _ = pure . Builder.toLazyByteString . TE.encodeUtf8Builder . JS.textFromJSString
470+-- | @Right . toStrict@
471+instance MimeUnrender OctetStream BS.ByteString where
472+ mimeUnrender _ = pure . TE.encodeUtf8 . JS.textFromJSString
418note :: e -> Maybe a -> Either e a
474note :: e -> Maybe a -> Either e a
419−note e = maybe (Left e) Right
475+note e Nothing = Left e
476+note _ (Just a) = Right a
421fmapL :: (e -> e') -> Either e a -> Either e' a
478fmapL :: (e -> e') -> Either e a -> Either e' a
422fmapL _ (Right a) = Right a
479fmapL _ (Right a) = Right a
@@ -427,4 +484,3 @@ builderToText = TE.decodeUtf8 . BL.toStrict . Builder.toLazyByteString
428escape :: T.Text -> T.Text
485escape :: T.Text -> T.Text
429escape = T.pack . N.escapeURIString (not . N.isReserved) . T.unpack . TE.decodeUtf8 . BL.toStrict . Builder.toLazyByteString . toEncodedUrlPiece
486escape = T.pack . N.escapeURIString (not . N.isReserved) . T.unpack . TE.decodeUtf8 . BL.toStrict . Builder.toLazyByteString . toEncodedUrlPiece