@@ -1,45 +1,66 @@
1−{-# LANGUAGE CPP #-}
2−{-# LANGUAGE DeriveFunctor #-}
3−{-# LANGUAGE FlexibleContexts #-}
4−{-# LANGUAGE GADTs #-}
5−{-# LANGUAGE OverloadedStrings #-}
6−{-# LANGUAGE LambdaCase #-}
7−{-# LANGUAGE RankNTypes #-}
8−{-# LANGUAGE ScopedTypeVariables #-}
9−{-# LANGUAGE TupleSections #-}
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DeriveFunctor #-}
3+{-# LANGUAGE ExistentialQuantification #-}
4+{-# LANGUAGE FlexibleContexts #-}
5+{-# LANGUAGE FlexibleInstances #-}
6+{-# LANGUAGE FunctionalDependencies #-}
7+{-# LANGUAGE GADTs #-}
8+{-# LANGUAGE LambdaCase #-}
9+{-# LANGUAGE MultiParamTypeClasses #-}
10+{-# LANGUAGE NamedFieldPuns #-}
11+{-# LANGUAGE OverloadedStrings #-}
12+{-# LANGUAGE RankNTypes #-}
13+{-# LANGUAGE ScopedTypeVariables #-}
14+{-# LANGUAGE TupleSections #-}
15+{-# OPTIONS_GHC -fno-warn-orphans #-}
1016
1117module Servant.Common.Req where
1218
1319-------------------------------------------------------------------------------
14−import Control.Applicative (liftA2, liftA3)
15−import Control.Arrow ((&&&))
20+import Control.Applicative (liftA2, liftA3)
21+import Control.Arrow ((&&&))
1622import Control.Concurrent
17−import Control.Monad (join)
18−import Control.Monad.IO.Class (MonadIO, liftIO)
19−import Data.Bifunctor (first)
20−import qualified Data.ByteString.Builder as Builder
21−import qualified Data.ByteString.Lazy.Char8 as BL
22−import qualified Data.Map as Map
23−import Data.Maybe (catMaybes, fromMaybe)
23+import Control.Monad (join)
24+import Control.Monad.IO.Class (MonadIO, liftIO)
25+import Data.Aeson
26+import Data.Bifunctor (first)
27+import qualified Data.ByteString as BS
28+import qualified Data.ByteString.Builder as Builder
29+import Data.ByteString.Lazy (ByteString)
30+import qualified Data.ByteString.Lazy.Char8 as BL
2431import Data.Functor.Compose
25−import Data.Monoid ((<>))
26−import Data.Proxy (Proxy(..))
27−import Data.Text (Text)
28−import qualified Data.Text as T
29−import qualified Data.Text.Encoding as TE
30−import Data.Traversable (forM)
32+import qualified Data.JSString.Text as JS (lazyTextFromJSString)
33+import qualified Data.Map as Map
34+import Data.Maybe (catMaybes, fromMaybe)
35+import Data.Monoid ((<>))
36+import Data.Proxy (Proxy (..))
37+import Data.Text (Text)
38+import qualified Data.Text as T
39+import qualified Data.Text.Encoding as TE
40+import qualified Data.Text.Lazy as TL
41+import Data.Traversable (forM)
42+import qualified Language.Javascript.JSaddle as JS
3143import Language.Javascript.JSaddle.Monad (JSM, MonadJSM, liftJSM)
32−import qualified Network.URI as N
33−import Reflex.Dom.Core hiding (tag)
34−import Servant.Common.BaseUrl (BaseUrl, showBaseUrl,
35− SupportsServantReflex)
36−import Servant.API.ContentTypes (MimeUnrender(..), NoContent(..))
37−import Web.HttpApiData (ToHttpApiData(..))
44+import qualified Network.URI as N
45+import Reflex.Dom.Core hiding (tag)
46+import Servant.API.ContentTypes (Accept, FormUrlEncoded,
47+ JSON, NoContent (..),
48+ OctetStream, PlainText)
49+import Servant.Common.BaseUrl (BaseUrl,
50+ SupportsServantReflex,
51+ showBaseUrl)
52+import Web.FormUrlEncoded (FromForm, urlDecodeAsForm)
53+import Web.HttpApiData (ToHttpApiData (..))
3854-------------------------------------------------------------------------------
3955import Servant.API.BasicAuth
56+-------------------------------------------------------------------------------
4057
58+-- #ifndef __GHCJS__
59+-- jsonDecode :: JS.JSString -> Maybe a
60+-- jsonDecode = error "jsonDecode is only available in GHCJS"
61+-- #endif
4162
42−------------------------------------------------------------------------------
63+-----------------------------------------------------------------------------
4364-- | The result of a request event
4465data ReqResult tag a
4566 = ResponseSuccess tag a XhrResponse
@@ -140,18 +161,20 @@ data QueryPart t = QueryPartParam (Dynamic t (Either Text (Maybe Text)))
140161-------------------------------------------------------------------------------
141162-- The data structure used to build up request information while traversing
142163-- the shape of a servant API
143−data Req t = Req
164+data Req t = forall o. (IsXhrPayload o, Show o) => Req
144165 { reqMethod :: Text
145166 , reqPathParts :: [Dynamic t (Either Text Text)]
146167 , qParams :: [(Text, QueryPart t)]
147− , reqBody :: Maybe (Dynamic t (Either Text (BL.ByteString, Text)))
168+ , reqBody :: Maybe (Dynamic t (Either Text (o, Text)))
148169 , headers :: [(Text, Dynamic t (Either Text Text))]
149170 , respHeaders :: XhrResponseHeaders
150171 , authData :: Maybe (Dynamic t (Maybe BasicAuthData))
151172 }
152173
174+data SomeXhrRequest = forall o. (IsXhrPayload o, Show o) => SomeXhrRequest (XhrRequest (Maybe o))
175+
153176defReq :: Req t
154−defReq = Req "GET" [] [] Nothing [] def Nothing
177+defReq = Req "GET" [] [] (Nothing :: Maybe (Dynamic t (Either Text ((), Text)))) [] def Nothing
155178
156179prependToPathParts :: Dynamic t (Either Text Text) -> Req t -> Req t
157180prependToPathParts p req =
@@ -160,14 +183,13 @@ prependToPathParts p req =
160183addHeader :: (ToHttpApiData a, Reflex t) => Text -> Dynamic t (Either Text a) -> Req t -> Req t
161184addHeader name val req = req { headers = (name, (fmap . fmap) (TE.decodeUtf8 . toHeader) val) : headers req }
162185
163−
164186reqToReflexRequest
165− :: forall t. Reflex t
166− => Text
167− -> Dynamic t BaseUrl
168− -> Req t
169− -> Dynamic t (Either Text (XhrRequest XhrPayload))
170−reqToReflexRequest reqMeth reqHost req =
187+ :: forall t. Reflex t
188+ => Text
189+ -> Dynamic t BaseUrl
190+ -> Req t
191+ -> Dynamic t (Either Text SomeXhrRequest)
192+reqToReflexRequest reqMeth reqHost req@(Req _ _ _ (reqBody :: Maybe (Dynamic t (Either Text (o, Text)))) _ _ _) =
171193 let t :: Dynamic t [Either Text Text]
172194 t = sequence $ reverse $ reqPathParts req
173195
@@ -219,33 +241,33 @@ reqToReflexRequest reqMeth reqHost req =
219241 fmap (fmap (\rightVal -> (headerName, rightVal))) dynam
220242
221243 mkConfigBody :: Either Text [(Text,Text)]
222− -> (Either Text (BL.ByteString, Text))
223− -> Either Text (XhrRequestConfig XhrPayload)
244+ -> (Either Text (o, Text))
245+ -> Either Text (XhrRequestConfig (Maybe o))
224246 mkConfigBody ehs rb = case (ehs, rb) of
225247 (_, Left e) -> Left e
226248 (Left e, _) -> Left e
227249 (Right hs, Right (bBytes, bCT)) ->
228250 Right $ XhrRequestConfig
229− { _xhrRequestConfig_sendData = bytesToPayload bBytes
251+ { _xhrRequestConfig_sendData = Just bBytes
230252 , _xhrRequestConfig_headers =
231253 Map.insert "Content-Type" bCT (Map.fromList hs)
232254 , _xhrRequestConfig_user = Nothing
233255 , _xhrRequestConfig_password = Nothing
234− , _xhrRequestConfig_responseType = Just XhrResponseType_ArrayBuffer
256+ , _xhrRequestConfig_responseType = Nothing
235257 , _xhrRequestConfig_withCredentials = False
236258 , _xhrRequestConfig_responseHeaders = def
237259 }
238260
239− xhrOpts :: Dynamic t (Either Text (XhrRequestConfig XhrPayload))
240− xhrOpts = case reqBody req of
261+ xhrOpts :: Dynamic t (Either Text (XhrRequestConfig (Maybe o)))
262+ xhrOpts = case reqBody of
241263 Nothing -> ffor xhrHeaders $ \case
242264 Left e -> Left e
243265 Right hs -> Right $ def { _xhrRequestConfig_headers = Map.fromList hs
244266 , _xhrRequestConfig_user = Nothing
245267 , _xhrRequestConfig_password = Nothing
246− , _xhrRequestConfig_responseType = Just XhrResponseType_ArrayBuffer
247− , _xhrRequestConfig_sendData = ""
268+ , _xhrRequestConfig_responseType = Nothing
248269 , _xhrRequestConfig_withCredentials = False
270+ , _xhrRequestConfig_sendData = Nothing
249271 }
250272 Just rBody -> liftA2 mkConfigBody xhrHeaders rBody
251273
@@ -259,10 +281,10 @@ reqToReflexRequest reqMeth reqHost req =
259281 addAuth :: Dynamic t (Either Text (XhrRequestConfig x))
260282 -> Dynamic t (Either Text (XhrRequestConfig x))
261283 addAuth xhr = case authData req of
262− Nothing -> xhr
284+ Nothing -> xhr
263285 Just auth -> liftA2 mkAuth auth xhr
264286
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)
266288
267289 in xhrReq
268290
@@ -271,8 +293,14 @@ reqToReflexRequest reqMeth reqHost req =
271293displayHttpRequest :: Text -> Text
272294displayHttpRequest httpmethod = "HTTP " <> httpmethod <> " request"
273295
296+instance IsXhrPayload o => IsXhrPayload (Maybe o) where
297+ sendXhrPayload xhr = maybe (sendXhrPayload xhr ()) (sendXhrPayload xhr)
298+
299+instance (IsXhrPayload o, IsXhrPayload o') => IsXhrPayload (Either o o') where
300+ sendXhrPayload xhr = either (sendXhrPayload xhr) (sendXhrPayload xhr)
301+
274302-- | 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)
276304 => Text
277305 -> Dynamic t (f (Req t))
278306 -> Dynamic t BaseUrl
@@ -285,6 +313,7 @@ performRequests reqMeth rs reqHost opts trigger = do
285313 reqToReflexRequest reqMeth reqHost <$> fxhr) <$> rs
286314
287315 -- xhrReqs = fmap snd <$> xhrReqsAndDebugs
316+ reqs :: Event t (Compose ((,) tag) f (Either Text SomeXhrRequest))
288317 reqs = attachPromptlyDynWith
289318 (\fxhr t -> Compose (t, fxhr)) xhrReqs trigger
290319
@@ -300,32 +329,31 @@ performSomeRequestsAsync
300329 HasWebView (Performable m),
301330 PerformEvent t m,
302331 TriggerEvent t m,
303− Traversable f,
304− IsXhrPayload a,
305− Show a
332+ Traversable f
306333 )
307334 => ClientOptions
308− -> Event t (f (Either Text (XhrRequest a)))
335+ -> Event t (f (Either Text SomeXhrRequest))
309336 -> m (Event t (f (Either Text XhrResponse)))
310−performSomeRequestsAsync opts =
311− performSomeRequestsAsync' opts newXMLHttpRequest . fmap return
337+performSomeRequestsAsync opts sxhr =
338+ performSomeRequestsAsync' opts newXMLHttpRequest $ return <$> sxhr
312339
313340
314341------------------------------------------------------------------------------
315342-- | A modified version or Reflex.Dom.Xhr.performRequestsAsync
316343-- that accepts 'f (Either e (XhrRequestb))' events
317344performSomeRequestsAsync'
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)
319346 => ClientOptions
320− -> (XhrRequest b -> (a -> JSM ()) -> Performable m XMLHttpRequest)
321− -> Event t (Performable m (f (Either Text (XhrRequest b)))) -> m (Event t (f (Either Text a)))
347+ -> (forall b. IsXhrPayload b => XhrRequest b -> (a -> JSM ()) -> Performable m XMLHttpRequest)
348+ -> Event t (Performable m (f (Either Text SomeXhrRequest)))
349+ -> m (Event t (f (Either Text a)))
322350performSomeRequestsAsync' opts newXhr req = performEventAsync $ ffor req $ \hrs cb -> do
323351 rs <- hrs
324352 resps <- forM rs $ \r -> case r of
325353 Left e -> do
326354 resp <- liftIO $ newMVar (Left e)
327355 return resp
328− Right r' -> do
356+ Right (SomeXhrRequest r') -> do
329357 resp <- liftIO newEmptyMVar
330358 r'' <- liftJSM $ (optsRequestFixup opts) r'
331359 _ <- newXhr r'' $ liftIO . putMVar resp . Right
@@ -334,12 +362,6 @@ performSomeRequestsAsync' opts newXhr req = performEventAsync $ ffor req $ \hrs
334362 return ()
335363
336364
337−
338−type XhrPayload = T.Text
339−bytesToPayload :: BL.ByteString -> XhrPayload
340−bytesToPayload = TE.decodeUtf8 . BL.toStrict
341−
342−
343365performRequestsCT
344366 :: (SupportsServantReflex t m,
345367 MimeUnrender ct a, Traversable f)
@@ -352,15 +374,14 @@ performRequestsCT
352374 -> m (Event t (f (ReqResult tag a)))
353375performRequestsCT ct reqMeth reqs reqHost opts trigger = do
354376 resps <- performRequests reqMeth reqs reqHost opts trigger
355− let decodeResp (Just (XhrResponseBody_ArrayBuffer x)) =
356− first T.pack .
357− mimeUnrender ct $
358− BL.fromStrict x
359− decodeResp _ = Left "No body"
377+ let decodeResp x = first T.pack
378+ . mimeUnrender ct
379+ . JS.textToJSString
380+ =<< note "No body text" (_xhrResponse_responseText x)
360381 return $ fmap
361382 (\(t,rs) -> ffor rs $ \r -> case r of
362383 Left e -> RequestFailure t e
363− Right g -> evalResponse (decodeResp . _xhrResponse_response) (t,g)
384+ Right g -> evalResponse decodeResp (t,g)
364385 )
365386 resps
366387
@@ -387,7 +408,7 @@ evalResponse
387408 :: (XhrResponse -> Either Text a)
388409 -> (tag, XhrResponse)
389410 -> ReqResult tag a
390−evalResponse decode (tag, xhr) =
411+evalResponse decodeRes (tag, xhr) =
391412 let status = _xhrResponse_status xhr
392413 okStatus = status >= 200 && status < 400
393414 errMsg = fromMaybe
@@ -398,7 +419,7 @@ evalResponse decode (tag, xhr) =
398419 then either
399420 (\e -> ResponseFailure tag e xhr)
400421 (\v -> ResponseSuccess tag v xhr)
401− (decode xhr)
422+ (decodeRes xhr)
402423 else ResponseFailure tag errMsg xhr
403424 in respPayld
404425
@@ -415,8 +436,44 @@ fanReqResult' :: Reflex t => Event t (ReqResult tag a) -> (Event t (tag, Text),
415436fanReqResult' = fmapMaybe reqFailure' &&& fmapMaybe reqSuccess'
416437
417438
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
444+
445+instance FromJSON a => MimeUnrender JSON a where
446+ mimeUnrender _ = note "Failed to decode response" . jsonDecode
447+
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
453+
454+-- | @left show . TextL.decodeUtf8'@
455+instance MimeUnrender PlainText TL.Text where
456+ mimeUnrender _ = pure . JS.lazyTextFromJSString
457+
458+-- | @left show . TextS.decodeUtf8' . toStrict@
459+instance MimeUnrender PlainText T.Text where
460+ mimeUnrender _ = pure . JS.textFromJSString
461+
462+-- | @Right . BC.unpack@
463+instance MimeUnrender PlainText String where
464+ mimeUnrender _ = Right . T.unpack . JS.textFromJSString
465+
466+-- | @Right . id@
467+instance MimeUnrender OctetStream ByteString where
468+ mimeUnrender _ = pure . Builder.toLazyByteString . TE.encodeUtf8Builder . JS.textFromJSString
469+
470+-- | @Right . toStrict@
471+instance MimeUnrender OctetStream BS.ByteString where
472+ mimeUnrender _ = pure . TE.encodeUtf8 . JS.textFromJSString
473+
418474note :: 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
420477
421478fmapL :: (e -> e') -> Either e a -> Either e' a
422479fmapL _ (Right a) = Right a
@@ -427,4 +484,3 @@ builderToText = TE.decodeUtf8 . BL.toStrict . Builder.toLazyByteString
427484
428485escape :: T.Text -> T.Text
429486escape = T.pack . N.escapeURIString (not . N.isReserved) . T.unpack . TE.decodeUtf8 . BL.toStrict . Builder.toLazyByteString . toEncodedUrlPiece
430−