@@ -1,44 +1,65 @@
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)
20+import Control.Applicative (liftA2, liftA3)
1521import Control.Concurrent
16−import Control.Monad (join)
17−import Control.Monad.IO.Class (MonadIO, liftIO)
18−import Data.Bifunctor (first)
19−import qualified Data.ByteString.Builder as Builder
20−import qualified Data.ByteString.Lazy.Char8 as BL
21−import qualified Data.Map as Map
22−import Data.Maybe (catMaybes, fromMaybe)
22+import Control.Monad (join)
23+import Control.Monad.IO.Class (MonadIO, liftIO)
24+import Data.Aeson
25+import Data.Bifunctor (first)
26+import qualified Data.ByteString as BS
27+import qualified Data.ByteString.Builder as BB
28+import Data.ByteString.Lazy (ByteString)
29+import qualified Data.ByteString.Lazy.Char8 as BL
2330import Data.Functor.Compose
24−import Data.Monoid ((<>))
25−import Data.Proxy (Proxy(..))
26−import Data.Text (Text)
27−import qualified Data.Text as T
28−import qualified Data.Text.Encoding as TE
29−import Data.Traversable (forM)
31+import qualified Data.JSString.Text as JS (lazyTextFromJSString)
32+import qualified Data.Map as Map
33+import Data.Maybe (catMaybes, fromMaybe)
34+import Data.Monoid ((<>))
35+import Data.Proxy (Proxy (..))
36+import Data.Text (Text)
37+import qualified Data.Text as T
38+import qualified Data.Text.Encoding as T
39+import qualified Data.Text.Lazy as TL
40+import Data.Traversable (forM)
41+import qualified Language.Javascript.JSaddle as JS
3042import Language.Javascript.JSaddle.Monad (JSM, MonadJSM, liftJSM)
31−import qualified Network.URI as N
32−import Reflex.Dom.Core hiding (tag)
33−import Servant.Common.BaseUrl (BaseUrl, showBaseUrl,
34− SupportsServantReflex)
35−import Servant.API.ContentTypes (MimeUnrender(..), NoContent(..))
36−import Web.HttpApiData (ToHttpApiData(..))
43+import qualified Network.URI as N
44+import Reflex.Dom.Core hiding (tag)
45+import Servant.API.ContentTypes (Accept, FormUrlEncoded,
46+ JSON, NoContent (..),
47+ OctetStream, PlainText)
48+import Servant.Common.BaseUrl (BaseUrl,
49+ SupportsServantReflex,
50+ showBaseUrl)
51+import Web.FormUrlEncoded (FromForm, urlDecodeAsForm)
52+import Web.HttpApiData (ToHttpApiData (..))
3753-------------------------------------------------------------------------------
3854import Servant.API.BasicAuth
55+-------------------------------------------------------------------------------
3956
57+-- #ifndef __GHCJS__
58+-- jsonDecode :: JS.JSString -> Maybe a
59+-- jsonDecode = error "jsonDecode is only available in GHCJS"
60+-- #endif
4061
41−------------------------------------------------------------------------------
62+-----------------------------------------------------------------------------
4263-- | The result of a request event
4364data ReqResult tag a
4465 = ResponseSuccess tag a XhrResponse
@@ -132,34 +153,35 @@ data QueryPart t = QueryPartParam (Dynamic t (Either Text (Maybe Text)))
132153-------------------------------------------------------------------------------
133154-- The data structure used to build up request information while traversing
134155-- the shape of a servant API
135−data Req t = Req
156+data Req t = forall o. (IsXhrPayload o, Show o) => Req
136157 { reqMethod :: Text
137158 , reqPathParts :: [Dynamic t (Either Text Text)]
138159 , qParams :: [(Text, QueryPart t)]
139− , reqBody :: Maybe (Dynamic t (Either Text (BL.ByteString, Text)))
160+ , reqBody :: Maybe (Dynamic t (Either Text (o, Text)))
140161 , headers :: [(Text, Dynamic t (Either Text Text))]
141162 , respHeaders :: XhrResponseHeaders
142163 , authData :: Maybe (Dynamic t (Maybe BasicAuthData))
143164 }
144165
166+data SomeXhrRequest = forall o. (IsXhrPayload o, Show o) => SomeXhrRequest (XhrRequest (Maybe o))
167+
145168defReq :: Req t
146−defReq = Req "GET" [] [] Nothing [] def Nothing
169+defReq = Req "GET" [] [] (Nothing :: Maybe (Dynamic t (Either Text ((), Text)))) [] def Nothing
147170
148171prependToPathParts :: Dynamic t (Either Text Text) -> Req t -> Req t
149172prependToPathParts p req =
150173 req { reqPathParts = p : reqPathParts req }
151174
152175addHeader :: (ToHttpApiData a, Reflex t) => Text -> Dynamic t (Either Text a) -> Req t -> Req t
153−addHeader name val req = req { headers = (name, (fmap . fmap) (TE.decodeUtf8 . toHeader) val) : headers req }
154−
176+addHeader name val req = req { headers = (name, (fmap . fmap) (T.decodeUtf8 . toHeader) val) : headers req }
155177
156178reqToReflexRequest
157− :: forall t. Reflex t
158− => Text
159− -> Dynamic t BaseUrl
160− -> Req t
161− -> Dynamic t (Either Text (XhrRequest XhrPayload))
162−reqToReflexRequest reqMeth reqHost req =
179+ :: forall t. Reflex t
180+ => Text
181+ -> Dynamic t BaseUrl
182+ -> Req t
183+ -> Dynamic t (Either Text SomeXhrRequest)
184+reqToReflexRequest reqMeth reqHost req@(Req _ _ _ (reqBody :: Maybe (Dynamic t (Either Text (o, Text)))) _ _ _) =
163185 let t :: Dynamic t [Either Text Text]
164186 t = sequence $ reverse $ reqPathParts req
165187
@@ -211,14 +233,14 @@ reqToReflexRequest reqMeth reqHost req =
211233 fmap (fmap (\rightVal -> (headerName, rightVal))) dynam
212234
213235 mkConfigBody :: Either Text [(Text,Text)]
214− -> (Either Text (BL.ByteString, Text))
215− -> Either Text (XhrRequestConfig XhrPayload)
236+ -> (Either Text (o, Text))
237+ -> Either Text (XhrRequestConfig (Maybe o))
216238 mkConfigBody ehs rb = case (ehs, rb) of
217239 (_, Left e) -> Left e
218240 (Left e, _) -> Left e
219241 (Right hs, Right (bBytes, bCT)) ->
220242 Right $ XhrRequestConfig
221− { _xhrRequestConfig_sendData = bytesToPayload bBytes
243+ { _xhrRequestConfig_sendData = Just bBytes
222244 , _xhrRequestConfig_headers =
223245 Map.insert "Content-Type" bCT (Map.fromList hs)
224246 , _xhrRequestConfig_user = Nothing
@@ -228,16 +250,16 @@ reqToReflexRequest reqMeth reqHost req =
228250 , _xhrRequestConfig_responseHeaders = def
229251 }
230252
231− xhrOpts :: Dynamic t (Either Text (XhrRequestConfig XhrPayload))
232− xhrOpts = case reqBody req of
253+ xhrOpts :: Dynamic t (Either Text (XhrRequestConfig (Maybe o)))
254+ xhrOpts = case reqBody of
233255 Nothing -> ffor xhrHeaders $ \case
234256 Left e -> Left e
235257 Right hs -> Right $ def { _xhrRequestConfig_headers = Map.fromList hs
236258 , _xhrRequestConfig_user = Nothing
237259 , _xhrRequestConfig_password = Nothing
238260 , _xhrRequestConfig_responseType = Nothing
239− , _xhrRequestConfig_sendData = ""
240261 , _xhrRequestConfig_withCredentials = False
262+ , _xhrRequestConfig_sendData = Nothing
241263 }
242264 Just rBody -> liftA2 mkConfigBody xhrHeaders rBody
243265
@@ -245,16 +267,16 @@ reqToReflexRequest reqMeth reqHost req =
245267 mkAuth _ (Left e) = Left e
246268 mkAuth Nothing r = r
247269 mkAuth (Just (BasicAuthData u p)) (Right config) = Right $ config
248− { _xhrRequestConfig_user = Just $ TE.decodeUtf8 u
249− , _xhrRequestConfig_password = Just $ TE.decodeUtf8 p}
270+ { _xhrRequestConfig_user = Just $ T.decodeUtf8 u
271+ , _xhrRequestConfig_password = Just $ T.decodeUtf8 p}
250272
251273 addAuth :: Dynamic t (Either Text (XhrRequestConfig x))
252274 -> Dynamic t (Either Text (XhrRequestConfig x))
253275 addAuth xhr = case authData req of
254− Nothing -> xhr
276+ Nothing -> xhr
255277 Just auth -> liftA2 mkAuth auth xhr
256278
257− xhrReq = (liftA2 . liftA2) (\p opt -> XhrRequest reqMeth p opt) xhrUrl (addAuth xhrOpts)
279+ xhrReq = (liftA2 . liftA2) (\p opt -> SomeXhrRequest $ XhrRequest reqMeth p opt) xhrUrl (addAuth xhrOpts)
258280
259281 in xhrReq
260282
@@ -263,8 +285,14 @@ reqToReflexRequest reqMeth reqHost req =
263285displayHttpRequest :: Text -> Text
264286displayHttpRequest httpmethod = "HTTP " <> httpmethod <> " request"
265287
288+instance IsXhrPayload o => IsXhrPayload (Maybe o) where
289+ sendXhrPayload xhr = maybe (sendXhrPayload xhr ()) (sendXhrPayload xhr)
290+
291+instance (IsXhrPayload o, IsXhrPayload o') => IsXhrPayload (Either o o') where
292+ sendXhrPayload xhr = either (sendXhrPayload xhr) (sendXhrPayload xhr)
293+
266294-- | This function performs the request
267−performRequests :: forall t m f tag.(SupportsServantReflex t m, Traversable f)
295+performRequests :: forall t m f tag. (SupportsServantReflex t m, Traversable f)
268296 => Text
269297 -> Dynamic t (f (Req t))
270298 -> Dynamic t BaseUrl
@@ -277,6 +305,7 @@ performRequests reqMeth rs reqHost opts trigger = do
277305 reqToReflexRequest reqMeth reqHost <$> fxhr) <$> rs
278306
279307 -- xhrReqs = fmap snd <$> xhrReqsAndDebugs
308+ reqs :: Event t (Compose ((,) tag) f (Either Text SomeXhrRequest))
280309 reqs = attachPromptlyDynWith
281310 (\fxhr t -> Compose (t, fxhr)) xhrReqs trigger
282311
@@ -292,32 +321,31 @@ performSomeRequestsAsync
292321 HasWebView (Performable m),
293322 PerformEvent t m,
294323 TriggerEvent t m,
295− Traversable f,
296− IsXhrPayload a,
297− Show a
324+ Traversable f
298325 )
299326 => ClientOptions
300− -> Event t (f (Either Text (XhrRequest a)))
327+ -> Event t (f (Either Text SomeXhrRequest))
301328 -> m (Event t (f (Either Text XhrResponse)))
302−performSomeRequestsAsync opts =
303− performSomeRequestsAsync' opts newXMLHttpRequest . fmap return
329+performSomeRequestsAsync opts sxhr =
330+ performSomeRequestsAsync' opts newXMLHttpRequest $ return <$> sxhr
304331
305332
306333------------------------------------------------------------------------------
307334-- | A modified version or Reflex.Dom.Xhr.performRequestsAsync
308335-- that accepts 'f (Either e (XhrRequestb))' events
309336performSomeRequestsAsync'
310− :: (MonadJSM (Performable m), PerformEvent t m, TriggerEvent t m, Traversable f, Show b)
337+ :: (MonadJSM (Performable m), PerformEvent t m, TriggerEvent t m, Traversable f)
311338 => ClientOptions
312− -> (XhrRequest b -> (a -> JSM ()) -> Performable m XMLHttpRequest)
313− -> Event t (Performable m (f (Either Text (XhrRequest b)))) -> m (Event t (f (Either Text a)))
339+ -> (forall b. IsXhrPayload b => XhrRequest b -> (a -> JSM ()) -> Performable m XMLHttpRequest)
340+ -> Event t (Performable m (f (Either Text SomeXhrRequest)))
341+ -> m (Event t (f (Either Text a)))
314342performSomeRequestsAsync' opts newXhr req = performEventAsync $ ffor req $ \hrs cb -> do
315343 rs <- hrs
316344 resps <- forM rs $ \r -> case r of
317345 Left e -> do
318346 resp <- liftIO $ newMVar (Left e)
319347 return resp
320− Right r' -> do
348+ Right (SomeXhrRequest r') -> do
321349 resp <- liftIO newEmptyMVar
322350 r'' <- liftJSM $ (optsRequestFixup opts) r'
323351 _ <- newXhr r'' $ liftIO . putMVar resp . Right
@@ -326,12 +354,6 @@ performSomeRequestsAsync' opts newXhr req = performEventAsync $ ffor req $ \hrs
326354 return ()
327355
328356
329−
330−type XhrPayload = T.Text
331−bytesToPayload :: BL.ByteString -> XhrPayload
332−bytesToPayload = TE.decodeUtf8 . BL.toStrict
333−
334−
335357performRequestsCT
336358 :: (SupportsServantReflex t m,
337359 MimeUnrender ct a, Traversable f)
@@ -344,11 +366,10 @@ performRequestsCT
344366 -> m (Event t (f (ReqResult tag a)))
345367performRequestsCT ct reqMeth reqs reqHost opts trigger = do
346368 resps <- performRequests reqMeth reqs reqHost opts trigger
347− let decodeResp x = first T.pack .
348− mimeUnrender ct .
349− BL.fromStrict .
350− TE.encodeUtf8 =<< note "No body text"
351− (_xhrResponse_responseText x)
369+ let decodeResp x = first T.pack
370+ . mimeUnrender ct
371+ . JS.textToJSString
372+ =<< note "No body text" (_xhrResponse_responseText x)
352373 return $ fmap
353374 (\(t,rs) -> ffor rs $ \r -> case r of
354375 Left e -> RequestFailure t e
@@ -379,7 +400,7 @@ evalResponse
379400 :: (XhrResponse -> Either Text a)
380401 -> (tag, XhrResponse)
381402 -> ReqResult tag a
382−evalResponse decode (tag, xhr) =
403+evalResponse decodeRes (tag, xhr) =
383404 let okStatus = _xhrResponse_status xhr < 400
384405 errMsg = fromMaybe
385406 ("Empty response with error code " <>
@@ -389,20 +410,56 @@ evalResponse decode (tag, xhr) =
389410 then either
390411 (\e -> ResponseFailure tag e xhr)
391412 (\v -> ResponseSuccess tag v xhr)
392− (decode xhr)
413+ (decodeRes xhr)
393414 else ResponseFailure tag errMsg xhr
394415 in respPayld
395416
396417
418+-- | Similar to 'Servant.API.ContentType.MimeUnrender' but with the differnce that
419+-- the second argument expects a JSString instead of ByteString.
420+-- This can lead to better performance ghcjs.
421+class Accept contentType => MimeUnrender contentType a where
422+ mimeUnrender :: Proxy contentType -> JS.JSString -> Either String a
423+
424+instance FromJSON a => MimeUnrender JSON a where
425+ mimeUnrender _ = note "Failed to decode response" . jsonDecode
426+
427+-- | @urlDecodeAsForm@
428+-- Note that the @mimeUnrender p (mimeRender p x) == Right x@ law only
429+-- holds if every element of x is non-null (i.e., not @("", "")@)
430+instance FromForm a => MimeUnrender FormUrlEncoded a where
431+ mimeUnrender _ = first T.unpack . urlDecodeAsForm . BB.toLazyByteString . T.encodeUtf8Builder . JS.textFromJSString
432+
433+-- | @left show . TextL.decodeUtf8'@
434+instance MimeUnrender PlainText TL.Text where
435+ mimeUnrender _ = pure . JS.lazyTextFromJSString
436+
437+-- | @left show . TextS.decodeUtf8' . toStrict@
438+instance MimeUnrender PlainText T.Text where
439+ mimeUnrender _ = pure . JS.textFromJSString
440+
441+-- | @Right . BC.unpack@
442+instance MimeUnrender PlainText String where
443+ mimeUnrender _ = Right . T.unpack . JS.textFromJSString
444+
445+-- | @Right . id@
446+instance MimeUnrender OctetStream ByteString where
447+ mimeUnrender _ = pure . BB.toLazyByteString . T.encodeUtf8Builder . JS.textFromJSString
448+
449+-- | @Right . toStrict@
450+instance MimeUnrender OctetStream BS.ByteString where
451+ mimeUnrender _ = pure . T.encodeUtf8 . JS.textFromJSString
452+
397453note :: e -> Maybe a -> Either e a
398−note e = maybe (Left e) Right
454+note e Nothing = Left e
455+note _ (Just a) = Right a
399456
400457fmapL :: (e -> e') -> Either e a -> Either e' a
401458fmapL _ (Right a) = Right a
402459fmapL f (Left e) = Left (f e)
403460
404−builderToText :: Builder.Builder -> T.Text
405−builderToText = TE.decodeUtf8 . BL.toStrict . Builder.toLazyByteString
461+builderToText :: BB.Builder -> T.Text
462+builderToText = T.decodeUtf8 . BL.toStrict . BB.toLazyByteString
406463
407464escape :: T.Text -> T.Text
408−escape = T.pack . N.escapeURIString (not . N.isReserved) . T.unpack . TE.decodeUtf8 . BL.toStrict . Builder.toLazyByteString . toEncodedUrlPiece
465+escape = T.pack . N.escapeURIString (not . N.isReserved) . T.unpack . T.decodeUtf8 . BL.toStrict . BB.toLazyByteString . toEncodedUrlPiece