on bdbcbfa help sign in
16 files changed+705−274
since splitsnapshots
unifiedsplit
.gitignore−2 .travis.yml+4−4 ChangeLog.md−8 README.md+1−1 default.nix+18−10 exec/Example.hs+1−2 nix/nix.conf+1−2 nix/reflex-platform.nix+2−2 nix/servant-snap.nix+6−4 servant-reflex.cabal+10−17 src/Servant/Common/BaseUrl.hs+5−4 src/Servant/Common/Req.hs+134−79 src/Servant/Reflex.hs+296−79 src/Servant/Reflex/Multi.hs+224−55 testserver/testserver.cabal+3−3 travis.nix−2
.gitignore−2
@@ -7,5 +7,3 @@ log
77*.stats
88out
99/.stack-work/
10−TAGS
11−dist-newstyle
.travis.yml+4−4
@@ -1,17 +1,17 @@
11language: nix
2−nix: 2.3.6
2+nix: 2.1.1
33sudo: require
44
5−before_install:
5+install:
66 - sudo cp -rT $TRAVIS_BUILD_DIR/nix /etc/nix
77
88before_script:
9− - sudo systemctl restart nix-daemon.service
109 - git config --global user.email "travis-ci@example.com"
1110 - git config --global user.name "Travis-CI"
1211
1312script:
13+ - nix-env -iA cachix -f https://cachix.org/api/v1/install
14+ - cachix use imalsogreg
1415 - nix-build travis.nix -A build
15− - nix-build travis.nix -A buildGhc
1616 - nix-build travis.nix -A testserver
1717 - nix-build travis.nix -A testresults
ChangeLog.md−8
@@ -1,11 +1,3 @@
1−0.3.5
2−-----
3−
4− - Require reflex-dom-core >= 0.6 (for internal handling of case-insensitive headers)
5− - Require servant >= 0.16
6− - Eliminate use of deprecated widgets from reflex-dom-core
7− - Bump the version of reflex-platform used in nix-shell and test suite
8−
910.3.4
102-----
113
README.md+1−1
@@ -7,7 +7,7 @@
77Keeping your frontend in sync with your API server can be difficult - when the
88API changes its input parameters or return type, XHR requests from the
99frontend will fail at runtime. If your API is defined by
10−[servant](https://haskell-servant.readthedocs.io) combinators, you can use
10+[servant](haskell-servant.readthedocs.io) combinators, you can use
1111`servant-reflex` to share the API between the server and frontend.
1212Syncronization between is checked at compile time, and rather than building XHR
1313requests by hand, API endpoints are available behind `reflex`'s FRP semantics.
default.nix+18−10
@@ -1,21 +1,29 @@
1−{ mkDerivation, base, bytestring, case-insensitive, containers
2−, data-default, exceptions, ghcjs-dom, http-api-data, http-media
3−, jsaddle, mtl, network-uri, reflex, reflex-dom, reflex-dom-core
4−, safe, servant, servant-auth, stdenv, string-conversions, text
5−, transformers
1+{ mkDerivation, aeson, base, bytestring, case-insensitive
2+, containers, data-default, exceptions, ghcjs-dom, http-api-data
3+, http-media, jsaddle, mtl, network-uri, reflex, reflex-dom
4+, reflex-dom-core, safe, scientific, servant, servant-auth, stdenv
5+, string-conversions, text, transformers, megaparsec, wreq
66}:
77mkDerivation {
88 pname = "servant-reflex";
9− version = "0.3.6";
10− src = ./.;
9+ version = "0.3.4";
10+ src = builtins.filterSource
11+ (path: type:
12+ baseNameOf path != "result"
13+ && baseNameOf path != "nix"
14+ ) ./.;
15+ configureFlags = [ "-fexample" ];
1116 isLibrary = true;
1217 isExecutable = true;
1318 libraryHaskellDepends = [
1419 base bytestring case-insensitive containers data-default exceptions
1520 ghcjs-dom http-api-data http-media jsaddle mtl network-uri reflex
16− reflex-dom reflex-dom-core safe servant servant-auth string-conversions
17− text transformers
21+ reflex-dom-core safe servant servant-auth string-conversions text
22+ transformers megaparsec wreq
1823 ];
19− description = "servant API generator for reflex apps";
24+ executableHaskellDepends = [
25+ aeson base reflex reflex-dom scientific servant text
26+ ];
27+ description = "Servant reflex API generator";
2028 license = stdenv.lib.licenses.bsd3;
2129}
exec/Example.hs+1−2
@@ -21,8 +21,7 @@ import Servant.API
2121import API
2222import Data.Proxy
2323import Text.Read (readMaybe)
24−import Reflex.Dom.Core hiding (run, mainWidget)
25−import Reflex.Dom (mainWidget)
24+import Reflex.Dom.Core hiding (run)
2625------------------------------------------------------------------------------
2726import Servant.Reflex
2827import Servant.Reflex.Multi
nix/nix.conf+1−2
@@ -1,3 +1,2 @@
1−substituters = https://cache.nixos.org/ https://nixcache.reflex-frp.org/
1+substituters = https://cache.nixos.org https://nixcache.reflex-frp.org
22trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=
3−trusted-users = travis
nix/reflex-platform.nix+2−2
@@ -1,7 +1,7 @@
11let
2− rev = "1aba6f367982bd6dd78ec2fda75ab246a62d32c5" ;
2+ rev = "a15d3a2411e7ca7d4ee4853b57c72fe83faee272";
33in import (builtins.fetchTarball
44 {
55 url = "https://github.com/reflex-frp/reflex-platform/archive/${rev}.tar.gz";
6− sha256 = "137vg6i8yy4lp6rz4wbvvhfcc9d9qf7j1b3fnng3r7sj63k7j664" ;
6+ sha256 = "1dsvw0lah7761vndip1hqal4fjpjv84ravinnfhy83jgfav5ivna";
77 }) {}
nix/servant-snap.nix+6−4
@@ -1,7 +1,9 @@
1−# Servant-snap is marked broken in nixpkgs
1+# Servant-snap is stuck a bit behind atm. But we have a fork that mostly works
2+# except for streaming, which is good enough for this test server. See:
3+# https://github.com/haskell-servant/servant-snap/issues/20
24{ nixpkgs ? import <nixpkgs> {}}: self: super:
35self.callCabal2nix "servant-snap" (nixpkgs.fetchgit {
4− url = "https://github.com/haskell-servant/servant-snap.git";
5− rev = "af5172a6de5bb2a07eb1bf4c85952075ec6ecdf3";
6− sha256 = "sha256-fzzQGwG8Ko72OO/kwLMG52eNZSt3F2dxhNgM9gaO4yQ=";
6+ url = "https://github.com/antislava/servant-snap.git";
7+ rev = "fc9658e8f52ebce9e4f304ea0c6705d697d4fa84";
8+ sha256 = "0zlipmx1fb73mhpnndwmdmigxxrsdnsnb1157pgsrxpx89l9pjig";
79}) {}
servant-reflex.cabal+10−17
@@ -1,5 +1,5 @@
11Name: servant-reflex
2−Version: 0.3.6
2+Version: 0.3.4
33Synopsis: servant API generator for reflex apps
44Description: Generate reflex-compatible client functions from servant API descriptions
55License: BSD3
@@ -24,22 +24,24 @@ library
2424
2525 hs-source-dirs: src
2626 build-depends:
27+ aeson >= 0.11 && < 1.5,
2728 base >= 4.8 && < 5,
2829 bytestring >= 0.10 && < 0.11,
2930 case-insensitive >= 1.2.0.4 && < 1.3,
30− containers >= 0.5.6 && < 0.7,
31+ containers >= 0.5.6 && < 0.6,
3132 data-default >= 0.5 && < 0.8,
3233 exceptions >= 0.8 && < 0.11,
3334 ghcjs-dom >= 0.2 && < 0.10,
34− http-api-data >= 0.3.6 && < 0.5,
35− http-media >= 0.6 && < 0.9,
35+ http-api-data >= 0.3.6 && < 0.4,
36+ http-media >= 0.6 && < 0.8,
3637 jsaddle >= 0.8 && < 0.10,
3738 mtl >= 2.2.1 && < 2.3,
3839 network-uri >= 2.6 && < 2.7,
39− reflex >= 0.5 && < 0.9,
40− reflex-dom-core >= 0.6 && < 0.7,
40+ reflex >= 0.5 && < 0.7,
41+ reflex-dom-core >= 0.4 && < 0.6,
4142 safe >= 0.3.9 && < 0.4,
42− servant >= 0.16 && < 0.18,
43+ servant >= 0.13 && < 0.16,
44+ servant-checked-exceptions >= 2.0 && < 2.2,
4345 servant-auth >= 0.2.1 && < 0.4,
4446 string-conversions >= 0.4 && < 0.5,
4547 text >= 1.2 && < 1.3,
@@ -54,16 +56,7 @@ executable example
5456 buildable: True
5557 else
5658 buildable: False
57− build-depends:
58− aeson,
59− reflex,
60− servant-reflex,
61− base,
62− scientific,
63− servant,
64− reflex-dom,
65− reflex-dom-core,
66− text
59+ build-depends: aeson, reflex, servant-reflex, base, scientific, servant, reflex-dom-core, text
6760 default-language: Haskell2010
6861 main-is: Example.hs
6962 other-modules: API
src/Servant/Common/BaseUrl.hs+5−4
@@ -85,14 +85,15 @@ baseUrlWidget = elClass "div" "base-url" $ do
8585 where pathWidget :: m (Dynamic t BaseUrl)
8686 pathWidget = do
8787 text "Url base path"
88− t <- inputElement def
88+ t <- textInput (def {_textInputConfig_attributes =
89+ constDyn ("placeholder" =: "/a/b")})
8990 return $ BasePath <$> value t
9091 fullUrlWidget :: m (Dynamic t BaseUrl)
9192 fullUrlWidget = do
9293 schm <- dropdown Https (constDyn $ Https =: "https" <> Http =: "http") def
93− srv <- inputElement def
94+ srv <- textInput def {_textInputConfig_attributes = constDyn $ "placeholder" =: "example.com"}
9495 text ":"
95− prt <- inputElement def
96+ prt <- textInput def { _textInputConfig_attributes = constDyn $ "placeholder" =: "80"}
9697 port :: Dynamic t Int <- holdDyn 80 (fmapMaybe (readMaybe . T.unpack) $ updated (value prt))
97− path <- inputElement def
98+ path <- textInput def { _textInputConfig_attributes = constDyn $ "placeholder" =: "a/b" }
9899 return $ BaseFullUrl <$> value schm <*> value srv <*> port <*> value path
src/Servant/Common/Req.hs+134−79
@@ -1,46 +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 Data.ByteString (ByteString)
23−import qualified Data.Map as Map
24−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
2531import Data.Functor.Compose
26−import Data.Monoid ((<>))
27−import Data.Proxy (Proxy(..))
28−import Data.Text (Text)
29−import qualified Data.Text as T
30−import qualified Data.Text.Encoding as TE
31−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
3243import Language.Javascript.JSaddle.Monad (JSM, MonadJSM, liftJSM)
33−import qualified Network.URI as N
34−import Reflex.Dom.Core hiding (tag)
35−import Servant.Common.BaseUrl (BaseUrl, showBaseUrl,
36− SupportsServantReflex)
37−import Servant.API.ContentTypes (MimeUnrender(..), NoContent(..))
38−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 (..))
3954-------------------------------------------------------------------------------
4055import Servant.API.BasicAuth
56+-------------------------------------------------------------------------------
4157
58+-- #ifndef __GHCJS__
59+-- jsonDecode :: JS.JSString -> Maybe a
60+-- jsonDecode = error "jsonDecode is only available in GHCJS"
61+-- #endif
4262
43−------------------------------------------------------------------------------
63+-----------------------------------------------------------------------------
4464-- | The result of a request event
4565data ReqResult tag a
4666 = ResponseSuccess tag a XhrResponse
@@ -141,18 +161,20 @@ data QueryPart t = QueryPartParam (Dynamic t (Either Text (Maybe Text)))
141161-------------------------------------------------------------------------------
142162-- The data structure used to build up request information while traversing
143163-- the shape of a servant API
144−data Req t = Req
164+data Req t = forall o. (IsXhrPayload o, Show o) => Req
145165 { reqMethod :: Text
146166 , reqPathParts :: [Dynamic t (Either Text Text)]
147167 , qParams :: [(Text, QueryPart t)]
148− , reqBody :: Maybe (Dynamic t (Either Text (BL.ByteString, Text)))
168+ , reqBody :: Maybe (Dynamic t (Either Text (o, Text)))
149169 , headers :: [(Text, Dynamic t (Either Text Text))]
150170 , respHeaders :: XhrResponseHeaders
151171 , authData :: Maybe (Dynamic t (Maybe BasicAuthData))
152172 }
153173
174+data SomeXhrRequest = forall o. (IsXhrPayload o, Show o) => SomeXhrRequest (XhrRequest (Maybe o))
175+
154176defReq :: Req t
155−defReq = Req "GET" [] [] Nothing [] def Nothing
177+defReq = Req "GET" [] [] (Nothing :: Maybe (Dynamic t (Either Text ((), Text)))) [] def Nothing
156178
157179prependToPathParts :: Dynamic t (Either Text Text) -> Req t -> Req t
158180prependToPathParts p req =
@@ -161,14 +183,13 @@ prependToPathParts p req =
161183addHeader :: (ToHttpApiData a, Reflex t) => Text -> Dynamic t (Either Text a) -> Req t -> Req t
162184addHeader name val req = req { headers = (name, (fmap . fmap) (TE.decodeUtf8 . toHeader) val) : headers req }
163185
164−
165186reqToReflexRequest
166− :: forall t. Reflex t
167− => Text
168− -> Dynamic t BaseUrl
169− -> Req t
170− -> Dynamic t (Either Text (XhrRequest XhrPayload))
171−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)))) _ _ _) =
172193 let t :: Dynamic t [Either Text Text]
173194 t = sequence $ reverse $ reqPathParts req
174195
@@ -220,33 +241,33 @@ reqToReflexRequest reqMeth reqHost req =
220241 fmap (fmap (\rightVal -> (headerName, rightVal))) dynam
221242
222243 mkConfigBody :: Either Text [(Text,Text)]
223− -> (Either Text (BL.ByteString, Text))
224− -> Either Text (XhrRequestConfig XhrPayload)
244+ -> (Either Text (o, Text))
245+ -> Either Text (XhrRequestConfig (Maybe o))
225246 mkConfigBody ehs rb = case (ehs, rb) of
226247 (_, Left e) -> Left e
227248 (Left e, _) -> Left e
228249 (Right hs, Right (bBytes, bCT)) ->
229250 Right $ XhrRequestConfig
230− { _xhrRequestConfig_sendData = bytesToPayload bBytes
251+ { _xhrRequestConfig_sendData = Just bBytes
231252 , _xhrRequestConfig_headers =
232253 Map.insert "Content-Type" bCT (Map.fromList hs)
233254 , _xhrRequestConfig_user = Nothing
234255 , _xhrRequestConfig_password = Nothing
235− , _xhrRequestConfig_responseType = Just XhrResponseType_ArrayBuffer
256+ , _xhrRequestConfig_responseType = Nothing
236257 , _xhrRequestConfig_withCredentials = False
237258 , _xhrRequestConfig_responseHeaders = def
238259 }
239260
240− xhrOpts :: Dynamic t (Either Text (XhrRequestConfig XhrPayload))
241− xhrOpts = case reqBody req of
261+ xhrOpts :: Dynamic t (Either Text (XhrRequestConfig (Maybe o)))
262+ xhrOpts = case reqBody of
242263 Nothing -> ffor xhrHeaders $ \case
243264 Left e -> Left e
244265 Right hs -> Right $ def { _xhrRequestConfig_headers = Map.fromList hs
245266 , _xhrRequestConfig_user = Nothing
246267 , _xhrRequestConfig_password = Nothing
247− , _xhrRequestConfig_responseType = Just XhrResponseType_ArrayBuffer
248− , _xhrRequestConfig_sendData = mempty
268+ , _xhrRequestConfig_responseType = Nothing
249269 , _xhrRequestConfig_withCredentials = False
270+ , _xhrRequestConfig_sendData = Nothing
250271 }
251272 Just rBody -> liftA2 mkConfigBody xhrHeaders rBody
252273
@@ -260,10 +281,10 @@ reqToReflexRequest reqMeth reqHost req =
260281 addAuth :: Dynamic t (Either Text (XhrRequestConfig x))
261282 -> Dynamic t (Either Text (XhrRequestConfig x))
262283 addAuth xhr = case authData req of
263− Nothing -> xhr
284+ Nothing -> xhr
264285 Just auth -> liftA2 mkAuth auth xhr
265286
266− 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)
267288
268289 in xhrReq
269290
@@ -272,8 +293,14 @@ reqToReflexRequest reqMeth reqHost req =
272293displayHttpRequest :: Text -> Text
273294displayHttpRequest httpmethod = "HTTP " <> httpmethod <> " request"
274295
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+
275302-- | This function performs the request
276−performRequests :: forall t m f tag.(SupportsServantReflex t m, Traversable f)
303+performRequests :: forall t m f tag. (SupportsServantReflex t m, Traversable f)
277304 => Text
278305 -> Dynamic t (f (Req t))
279306 -> Dynamic t BaseUrl
@@ -286,6 +313,7 @@ performRequests reqMeth rs reqHost opts trigger = do
286313 reqToReflexRequest reqMeth reqHost <$> fxhr) <$> rs
287314
288315 -- xhrReqs = fmap snd <$> xhrReqsAndDebugs
316+ reqs :: Event t (Compose ((,) tag) f (Either Text SomeXhrRequest))
289317 reqs = attachPromptlyDynWith
290318 (\fxhr t -> Compose (t, fxhr)) xhrReqs trigger
291319
@@ -301,32 +329,31 @@ performSomeRequestsAsync
301329 HasWebView (Performable m),
302330 PerformEvent t m,
303331 TriggerEvent t m,
304− Traversable f,
305− IsXhrPayload a,
306− Show a
332+ Traversable f
307333 )
308334 => ClientOptions
309− -> Event t (f (Either Text (XhrRequest a)))
335+ -> Event t (f (Either Text SomeXhrRequest))
310336 -> m (Event t (f (Either Text XhrResponse)))
311−performSomeRequestsAsync opts =
312− performSomeRequestsAsync' opts newXMLHttpRequest . fmap return
337+performSomeRequestsAsync opts sxhr =
338+ performSomeRequestsAsync' opts newXMLHttpRequest $ return <$> sxhr
313339
314340
315341------------------------------------------------------------------------------
316342-- | A modified version or Reflex.Dom.Xhr.performRequestsAsync
317343-- that accepts 'f (Either e (XhrRequestb))' events
318344performSomeRequestsAsync'
319− :: (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)
320346 => ClientOptions
321− -> (XhrRequest b -> (a -> JSM ()) -> Performable m XMLHttpRequest)
322− -> 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)))
323350performSomeRequestsAsync' opts newXhr req = performEventAsync $ ffor req $ \hrs cb -> do
324351 rs <- hrs
325352 resps <- forM rs $ \r -> case r of
326353 Left e -> do
327354 resp <- liftIO $ newMVar (Left e)
328355 return resp
329− Right r' -> do
356+ Right (SomeXhrRequest r') -> do
330357 resp <- liftIO newEmptyMVar
331358 r'' <- liftJSM $ (optsRequestFixup opts) r'
332359 _ <- newXhr r'' $ liftIO . putMVar resp . Right
@@ -335,12 +362,6 @@ performSomeRequestsAsync' opts newXhr req = performEventAsync $ ffor req $ \hrs
335362 return ()
336363
337364
338−
339−type XhrPayload = ByteString
340−bytesToPayload :: BL.ByteString -> XhrPayload
341−bytesToPayload = BL.toStrict
342−
343−
344365performRequestsCT
345366 :: (SupportsServantReflex t m,
346367 MimeUnrender ct a, Traversable f)
@@ -353,15 +374,14 @@ performRequestsCT
353374 -> m (Event t (f (ReqResult tag a)))
354375performRequestsCT ct reqMeth reqs reqHost opts trigger = do
355376 resps <- performRequests reqMeth reqs reqHost opts trigger
356− let decodeResp (Just (XhrResponseBody_ArrayBuffer x)) =
357− first T.pack .
358− mimeUnrender ct $
359− BL.fromStrict x
360− 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)
361381 return $ fmap
362382 (\(t,rs) -> ffor rs $ \r -> case r of
363383 Left e -> RequestFailure t e
364− Right g -> evalResponse (decodeResp . _xhrResponse_response) (t,g)
384+ Right g -> evalResponse decodeResp (t,g)
365385 )
366386 resps
367387
@@ -388,7 +408,7 @@ evalResponse
388408 :: (XhrResponse -> Either Text a)
389409 -> (tag, XhrResponse)
390410 -> ReqResult tag a
391−evalResponse decode (tag, xhr) =
411+evalResponse decodeRes (tag, xhr) =
392412 let status = _xhrResponse_status xhr
393413 okStatus = status >= 200 && status < 400
394414 errMsg = fromMaybe
@@ -399,7 +419,7 @@ evalResponse decode (tag, xhr) =
399419 then either
400420 (\e -> ResponseFailure tag e xhr)
401421 (\v -> ResponseSuccess tag v xhr)
402− (decode xhr)
422+ (decodeRes xhr)
403423 else ResponseFailure tag errMsg xhr
404424 in respPayld
405425
@@ -416,8 +436,44 @@ fanReqResult' :: Reflex t => Event t (ReqResult tag a) -> (Event t (tag, Text),
416436fanReqResult' = fmapMaybe reqFailure' &&& fmapMaybe reqSuccess'
417437
418438
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+
419474note :: e -> Maybe a -> Either e a
420−note e = maybe (Left e) Right
475+note e Nothing = Left e
476+note _ (Just a) = Right a
421477
422478fmapL :: (e -> e') -> Either e a -> Either e' a
423479fmapL _ (Right a) = Right a
@@ -428,4 +484,3 @@ builderToText = TE.decodeUtf8 . BL.toStrict . Builder.toLazyByteString
428484
429485escape :: T.Text -> T.Text
430486escape = T.pack . N.escapeURIString (not . N.isReserved) . T.unpack . TE.decodeUtf8 . BL.toStrict . Builder.toLazyByteString . toEncodedUrlPiece
431−
src/Servant/Reflex.hs+296−79
@@ -1,14 +1,19 @@
11{-# LANGUAGE AllowAmbiguousTypes #-}
22{-# LANGUAGE CPP #-}
33{-# LANGUAGE DataKinds #-}
4+{-# LANGUAGE DefaultSignatures #-}
5+{-# LANGUAGE ExtendedDefaultRules #-}
46{-# LANGUAGE FlexibleContexts #-}
57{-# LANGUAGE FlexibleInstances #-}
8+{-# LANGUAGE FunctionalDependencies #-}
69{-# LANGUAGE GeneralizedNewtypeDeriving #-}
710{-# LANGUAGE InstanceSigs #-}
11+{-# LANGUAGE LambdaCase #-}
812{-# LANGUAGE MultiParamTypeClasses #-}
913{-# LANGUAGE OverloadedStrings #-}
1014{-# LANGUAGE PolyKinds #-}
1115{-# LANGUAGE Rank2Types #-}
16+{-# LANGUAGE RecordWildCards #-}
1217{-# LANGUAGE ScopedTypeVariables #-}
1318{-# LANGUAGE TupleSections #-}
1419{-# LANGUAGE TypeFamilies #-}
@@ -30,6 +35,7 @@ module Servant.Reflex
3035 , clientWithRoute
3136 , clientWithRouteAndResultHandler
3237 , BuildHeaderKeysTo(..)
38+ , GHCJS'MimeRender(..)
3339 , toHeaders
3440 , HasClient
3541 , Client
@@ -39,56 +45,101 @@ module Servant.Reflex
3945
4046------------------------------------------------------------------------------
4147import Control.Applicative
42−import qualified Data.CaseInsensitive as CI
48+import qualified Data.ByteString as BS
49+import qualified Data.ByteString.Lazy as BL
50+import Data.CaseInsensitive (mk)
4351import Data.Functor.Identity
44−import qualified Data.Map as Map
45−import Data.Monoid ((<>))
46−import Data.Proxy (Proxy (..))
47−import qualified Data.Set as Set
48−import Data.Text (Text)
49−import qualified Data.Text as T
50−import qualified Data.Text.Encoding as E
51−import GHC.Exts (Constraint)
52−import GHC.TypeLits (KnownSymbol, symbolVal)
53−import Servant.API ((:<|>) (..), (:>), BasicAuth,
54− BasicAuthData, BuildHeadersTo (..),
55− Capture, Header, Headers (..),
56− HttpVersion, IsSecure,
57− MimeRender (..), MimeUnrender,
58− NoContent, QueryFlag, QueryParam,
59− QueryParams, Raw, ReflectMethod (..),
60− RemoteHost, ReqBody,
61− ToHttpApiData (..), Vault, Verb,
62− contentType)
63−import Servant.API.Description (Summary)
64−import qualified Servant.Auth as Auth
65−
66−import Reflex.Dom.Core (Dynamic, Event, Reflex,
67− XhrRequest (..), XhrResponse (..),
68− XhrResponseHeaders (..),
69− attachPromptlyDynWith, constDyn, ffor,
70− fmapMaybe, leftmost,
71− performRequestsAsync)
52+import Data.Kind (Type)
53+import qualified Data.Map as Map
54+import Data.Monoid ((<>))
55+import Data.Proxy (Proxy (..))
56+import qualified Data.Set as Set
57+import Data.Text (Text)
58+import qualified Data.Text as T
59+import qualified Data.Text.Encoding as E
60+import qualified Data.Text.Lazy as TL
61+import GHC.Exts (Constraint)
62+import GHC.TypeLits (KnownSymbol,
63+ symbolVal)
64+import GHCJS.DOM.Types (Blob)
65+import Servant.API ((:<|>) (..),
66+ (:>),
67+ Accept (..),
68+ BasicAuth,
69+ BasicAuthData,
70+ BuildHeadersTo (..),
71+ Capture,
72+ FormUrlEncoded,
73+ Header,
74+ Headers (..),
75+ HttpVersion,
76+ IsSecure,
77+ JSON,
78+ MimeRender (..),
79+ NoContent,
80+ OctetStream,
81+ PlainText,
82+ QueryFlag,
83+ QueryParam,
84+ QueryParams,
85+ Raw,
86+ ReflectMethod (..),
87+ RemoteHost,
88+ ReqBody,
89+ ToHttpApiData (..),
90+ Vault, Verb,
91+ contentType)
92+import qualified Servant.Auth as Auth
93+
94+import Reflex.Dom.Core (Dynamic,
95+ Event,
96+ IsXhrPayload,
97+ Reflex,
98+ XhrRequest (..),
99+ XhrResponse (..),
100+ XhrResponseHeaders (..),
101+ attachPromptlyDynWith,
102+ constDyn,
103+ ffor,
104+ fmapMaybe,
105+ leftmost,
106+ performRequestsAsync)
72107------------------------------------------------------------------------------
73−import Servant.Common.BaseUrl (BaseUrl(..), Scheme(..), baseUrlWidget,
74− showBaseUrl,
75− SupportsServantReflex)
76−import Servant.Common.Req (ClientOptions(..),
77− defaultClientOptions,
78− Req, ReqResult(..), QParam(..),
79− QueryPart(..), addHeader, authData,
80− defReq, evalResponse, prependToPathParts,
81− -- performRequestCT,
82− performRequestsCT,
83− -- performRequestNoBody,
84− performRequestsNoBody,
85− performSomeRequestsAsync,
86− qParamToQueryPart, reqBody,
87− reqSuccess, reqFailure,
88− reqMethod, respHeaders,
89− response,
90− reqTag,
91− qParams, withCredentials)
108+import Servant.Checked.Exceptions.Internal (Envelope)
109+import Servant.Checked.Exceptions.Internal.Servant.API (NoThrow,
110+ Throwing,
111+ ThrowingNonterminal,
112+ Throws)
113+import Servant.Common.BaseUrl (BaseUrl (..),
114+ Scheme (..),
115+ SupportsServantReflex,
116+ baseUrlWidget,
117+ showBaseUrl)
118+import Servant.Common.Req (ClientOptions (..),
119+ MimeUnrender (..),
120+ QParam (..),
121+ QueryPart (..),
122+ Req (..),
123+ ReqResult (..),
124+ addHeader,
125+ authData,
126+ defReq,
127+ defaultClientOptions,
128+ evalResponse,
129+ performRequestsCT,
130+ performRequestsNoBody,
131+ performSomeRequestsAsync,
132+ prependToPathParts,
133+ qParamToQueryPart,
134+ qParams,
135+ reqBody,
136+ reqFailure,
137+ reqMethod,
138+ reqSuccess,
139+ reqTag,
140+ respHeaders,
141+ response,
142+ withCredentials)
92143
93144
94145-- * Accessing APIs as a Client
@@ -103,7 +154,7 @@ import Servant.Common.Req (ClientOptions(..),
103154-- >
104155-- > getAllBooks :: Event t l -> m (Event t (l, ReqResult [Book]))
105156-- > postNewBook :: Dynamic t (Maybe Book) -> Event t l
106−-- > -> m (Event t (l, ReqResult Book)))
157+-- -> m (Event t (l, ReqResult Book)))
107158-- > (getAllBooks :<|> postNewBook) = client myApi host
108159-- > where host = constDyn $ BaseUrl Http "localhost" 8080
109160client
@@ -228,26 +279,22 @@ instance {-# OVERLAPPING #-}
228279
229280toHeaders :: BuildHeadersTo ls => ReqResult tag a -> ReqResult tag (Headers ls a)
230281toHeaders r =
231− let hdrs = maybe []
232− (\xhr -> fmap (\(h,v) -> (CI.map E.encodeUtf8 h, E.encodeUtf8 v))
282+ let toBS = E.encodeUtf8
283+ hdrs = maybe []
284+ (\xhr -> fmap (\(h,v) -> (mk (toBS h), toBS v))
233285 (Map.toList $ _xhrResponse_headers xhr))
234286 (response r)
235287 in ffor r $ \a -> Headers {getResponse = a ,getHeadersHList = buildHeadersTo hdrs}
236288
237−
238289class BuildHeaderKeysTo hs where
239− buildHeaderKeysTo :: Proxy hs -> [CI.CI T.Text]
290+ buildHeaderKeysTo :: Proxy hs -> [T.Text]
240291
241292instance {-# OVERLAPPABLE #-} BuildHeaderKeysTo '[]
242293 where buildHeaderKeysTo _ = []
243294
244295instance {-# OVERLAPPABLE #-} (BuildHeaderKeysTo xs, KnownSymbol h)
245296 => BuildHeaderKeysTo ((Header h v) ': xs) where
246− buildHeaderKeysTo _ =
247− let
248− thisKey = CI.mk $ T.pack (symbolVal (Proxy :: Proxy h))
249− in thisKey : buildHeaderKeysTo (Proxy :: Proxy xs)
250−
297+ buildHeaderKeysTo _ = T.pack (symbolVal (Proxy :: Proxy h)) : buildHeaderKeysTo (Proxy :: Proxy xs)
251298
252299-- HEADERS Verb (Content) --
253300-- Headers combinator not treated in fully general case,
@@ -329,19 +376,6 @@ instance HasClient t m sublayout tag
329376 clientWithRouteAndResultHandler Proxy =
330377 clientWithRouteAndResultHandler (Proxy :: Proxy sublayout)
331378
332−-- | Using a 'Summary' combinator in your API doesn't affect the client
333−-- functions.
334−instance (HasClient t m sublayout tag, KnownSymbol sym)
335− => HasClient t m (Summary sym :> sublayout) tag where
336−
337− type Client t m (Summary sym :> sublayout) tag =
338− Client t m sublayout tag
339−
340− clientWithRouteAndResultHandler Proxy =
341− clientWithRouteAndResultHandler (Proxy :: Proxy sublayout)
342−
343−
344−
345379
346380-- | If you use a 'QueryParam' in one of your endpoints in your API,
347381-- the corresponding querying function will automatically take
@@ -513,22 +547,65 @@ instance SupportsServantReflex t m => HasClient t m Raw tag where
513547-- > where host = BaseUrl Http "localhost" 8080
514548-- > -- then you can just use "addBook" to query that endpoint
515549
516−instance (MimeRender ct a, HasClient t m sublayout tag, Reflex t)
517− => HasClient t m (ReqBody (ct ': cts) a :> sublayout) tag where
550+instance (GHCJS'MimeRender ct a, IsXhrPayload (ToSend ct a), Show (ToSend ct a),
551+ HasClient t m sublayout tag, Reflex t)
552+ => HasClient t m (ReqBody (ct ': cts) (a :: Type) :> sublayout) tag where
518553
519554 type Client t m (ReqBody (ct ': cts) a :> sublayout) tag =
520− Dynamic t (Either Text a) -> Client t m sublayout tag
555+ Dynamic t (Either Text (ToConvert ct a)) -> Client t m sublayout tag
521556
522− clientWithRouteAndResultHandler Proxy q t req baseurl opts wrap body =
557+ clientWithRouteAndResultHandler Proxy q t Req{..} baseurl opts wrap body =
523558 clientWithRouteAndResultHandler (Proxy :: Proxy sublayout) q t req' baseurl opts wrap
524− where req' = req { reqBody = bodyBytesCT }
559+ where req' = Req { reqBody = Just $ (fmap . fmap)
560+ (\b -> (ghcjsMimeRender ctProxy atProxy b, ctString)) body
561+ , ..}
525562 ctProxy = Proxy :: Proxy ct
563+ atProxy = Proxy :: Proxy a
526564 ctString = T.pack $ show $ contentType ctProxy
527− bodyBytesCT = Just $ (fmap . fmap)
528− (\b -> (mimeRender ctProxy b, ctString))
529− body
530565
566+class Accept ctype => GHCJS'MimeRender (ctype :: Type) (a :: Type) where
567+ type ToConvert ctype a :: Type
568+ type ToConvert ctype a = a
531569
570+ type ToSend ctype a :: Type
571+ type ToSend ctype a = BS.ByteString
572+
573+ ghcjsMimeRender
574+ :: IsXhrPayload (ToSend ctype a)
575+ => Proxy ctype -> Proxy a -> ToConvert ctype a -> ToSend ctype a
576+ default ghcjsMimeRender
577+ :: (MimeRender ctype a,
578+ MimeRender ctype (ToConvert ctype a), ToSend ctype a ~ BS.ByteString)
579+ => Proxy ctype -> Proxy a -> ToConvert ctype a -> ToSend ctype a
580+ ghcjsMimeRender ctype _ = BL.toStrict . mimeRender ctype
581+
582+instance MimeRender JSON x => GHCJS'MimeRender JSON x
583+
584+instance MimeRender FormUrlEncoded x => GHCJS'MimeRender FormUrlEncoded x
585+
586+instance GHCJS'MimeRender OctetStream BL.ByteString where
587+ type ToConvert OctetStream BL.ByteString = Either BL.ByteString Blob
588+ type ToSend OctetStream BL.ByteString = Either BS.ByteString Blob
589+ ghcjsMimeRender _ _ = \case Left bl -> Left $ BL.toStrict bl; Right x -> Right x
590+
591+instance GHCJS'MimeRender PlainText String where
592+ type ToSend PlainText String = Text
593+ ghcjsMimeRender _ _ = T.pack
594+
595+instance GHCJS'MimeRender PlainText Text where
596+ type ToSend PlainText Text = Text
597+ ghcjsMimeRender _ _ = id
598+
599+instance GHCJS'MimeRender PlainText TL.Text where
600+ type ToSend PlainText TL.Text = Text
601+ ghcjsMimeRender _ _ = TL.toStrict
602+
603+instance GHCJS'MimeRender PlainText BL.ByteString where
604+ type ToSend PlainText BL.ByteString = BS.ByteString
605+ ghcjsMimeRender _ _ = BL.toStrict
606+
607+instance GHCJS'MimeRender PlainText BS.ByteString where
608+ ghcjsMimeRender _ _ = id
532609
533610-- | Make the querying function append @path@ to the request path.
534611instance (KnownSymbol path, HasClient t m sublayout tag, Reflex t) => HasClient t m (path :> sublayout) tag where
@@ -623,3 +700,143 @@ type family HasCookieAuth xs :: Constraint where
623700
624701class CookieAuthNotEnabled
625702
703+{- servant-checked-exceptions
704+~~~~~~~~~~~~~~~~~~~~~
705+-}
706+
707+-- | Change a 'Throws' into 'Throwing'.
708+instance (Monad m, HasClient t m (Throwing '[e] :> api) tag) => HasClient t m (Throws e :> api) tag where
709+ type Client t m (Throws e :> api) tag = Client t m (Throwing '[e] :> api) tag
710+
711+ clientWithRouteAndResultHandler
712+ :: Proxy (Throws e :> api)
713+ -> Proxy m
714+ -> Proxy tag
715+ -> Req t
716+ -> Dynamic t BaseUrl
717+ -> ClientOptions
718+ -> (forall a. Event t (ReqResult tag a) -> m (Event t (ReqResult tag a)))
719+ -> Client t m (Throwing '[e] :> api) tag
720+ clientWithRouteAndResultHandler _ = clientWithRouteAndResultHandler (Proxy :: Proxy (Throwing '[e] :> api))
721+
722+-- | When @'Throwing' es@ comes before a 'Verb', change it into the same 'Verb'
723+-- but returning an @'Envelope' es@.
724+instance (HasClient t m (Verb method status ctypes (Envelope es a)) tag) =>
725+ HasClient t m (Throwing es :> Verb method status ctypes a) tag where
726+
727+ type Client t m (Throwing es :> Verb method status ctypes a) tag =
728+ Client t m (Verb method status ctypes (Envelope es a)) tag
729+
730+ clientWithRouteAndResultHandler
731+ :: Proxy (Throwing es :> Verb method status ctypes a)
732+ -> Proxy m
733+ -> Proxy tag
734+ -> Req t
735+ -> Dynamic t BaseUrl
736+ -> ClientOptions
737+ -> (forall x. Event t (ReqResult tag x) -> m (Event t (ReqResult tag x)))
738+ -> Client t m (Verb method status ctypes (Envelope es a)) tag
739+ clientWithRouteAndResultHandler Proxy =
740+ clientWithRouteAndResultHandler (Proxy :: Proxy (Verb method status ctypes (Envelope es a)))
741+
742+-- | When 'NoThrow' comes before a 'Verb', change it into the same 'Verb'
743+-- but returning an @'Envelope' \'[]@.
744+instance (Monad m, HasClient t m (Verb method status ctypes (Envelope '[] a)) tag) =>
745+ HasClient t m (NoThrow :> Verb method status ctypes a) tag where
746+
747+ type Client t m (NoThrow :> Verb method status ctypes a) tag =
748+ Client t m (Verb method status ctypes (Envelope '[] a)) tag
749+
750+ clientWithRouteAndResultHandler
751+ :: Proxy (NoThrow :> Verb method status ctypes a)
752+ -> Proxy m
753+ -> Proxy tag
754+ -> Req t
755+ -> Dynamic t BaseUrl
756+ -> ClientOptions
757+ -> (forall x. Event t (ReqResult tag x) -> m (Event t (ReqResult tag x)))
758+ -> Client t m (Verb method status ctypes (Envelope '[] a)) tag
759+ clientWithRouteAndResultHandler Proxy =
760+ clientWithRouteAndResultHandler (Proxy :: Proxy (Verb method status ctypes (Envelope '[] a)))
761+
762+-- | When @'Throwing' es@ comes before ':<|>', push @'Throwing' es@ into each
763+-- branch of the API.
764+instance (Monad m, HasClient t m ((Throwing es :> api1) :<|> (Throwing es :> api2)) tag) =>
765+ HasClient t m (Throwing es :> (api1 :<|> api2)) tag where
766+
767+ type Client t m (Throwing es :> (api1 :<|> api2)) tag =
768+ Client t m ((Throwing es :> api1) :<|> (Throwing es :> api2)) tag
769+
770+ clientWithRouteAndResultHandler
771+ :: Proxy (Throwing es :> (api1 :<|> api2))
772+ -> Proxy m
773+ -> Proxy tag
774+ -> Req t
775+ -> Dynamic t BaseUrl
776+ -> ClientOptions
777+ -> (forall a. Event t (ReqResult tag a) -> m (Event t (ReqResult tag a)))
778+ -> Client t m ((Throwing es :> api1) :<|> (Throwing es :> api2)) tag
779+ clientWithRouteAndResultHandler _ =
780+ clientWithRouteAndResultHandler (Proxy :: Proxy ((Throwing es :> api1) :<|> (Throwing es :> api2)))
781+
782+-- | When 'NoThrow' comes before ':<|>', push 'NoThrow' into each branch of the
783+-- API.
784+instance (Monad m, HasClient t m ((NoThrow :> api1) :<|> (NoThrow :> api2)) tag) =>
785+ HasClient t m (NoThrow :> (api1 :<|> api2)) tag where
786+
787+ type Client t m (NoThrow :> (api1 :<|> api2)) tag =
788+ Client t m ((NoThrow :> api1) :<|> (NoThrow :> api2)) tag
789+
790+ clientWithRouteAndResultHandler
791+ :: Proxy (NoThrow :> (api1 :<|> api2))
792+ -> Proxy m
793+ -> Proxy tag
794+ -> Req t
795+ -> Dynamic t BaseUrl
796+ -> ClientOptions
797+ -> (forall a. Event t (ReqResult tag a) -> m (Event t (ReqResult tag a)))
798+ -> Client t m ((NoThrow :> api1) :<|> (NoThrow :> api2)) tag
799+ clientWithRouteAndResultHandler _ =
800+ clientWithRouteAndResultHandler (Proxy :: Proxy ((NoThrow :> api1) :<|> (NoThrow :> api2)))
801+
802+-- | When a @'Throws' e@ comes immediately after a @'Throwing' es@, 'Snoc' the
803+-- @e@ onto the @es@. Otherwise, if @'Throws' e@ comes before any other
804+-- combinator, push it down so it is closer to the 'Verb'.
805+instance (Monad m, HasClient t m (ThrowingNonterminal (Throwing es :> api :> apis)) tag) =>
806+ HasClient t m (Throwing es :> api :> apis) tag where
807+
808+ type Client t m (Throwing es :> api :> apis) tag =
809+ Client t m (ThrowingNonterminal (Throwing es :> api :> apis)) tag
810+
811+ clientWithRouteAndResultHandler
812+ :: Proxy (Throwing es :> api :> apis)
813+ -> Proxy m
814+ -> Proxy tag
815+ -> Req t
816+ -> Dynamic t BaseUrl
817+ -> ClientOptions
818+ -> (forall a. Event t (ReqResult tag a) -> m (Event t (ReqResult tag a)))
819+ -> Client t m (ThrowingNonterminal (Throwing es :> api :> apis)) tag
820+ clientWithRouteAndResultHandler _ =
821+ clientWithRouteAndResultHandler (Proxy :: Proxy (ThrowingNonterminal (Throwing es :> api :> apis)))
822+
823+-- | When 'NoThrow' comes before any other combinator, push it down so it is
824+-- closer to the 'Verb'.
825+instance (Monad m, HasClient t m (api :> NoThrow :> apis) tag) =>
826+ HasClient t m (NoThrow :> api :> apis) tag where
827+
828+ type Client t m (NoThrow :> api :> apis) tag =
829+ Client t m (api :> NoThrow :> apis) tag
830+
831+ clientWithRouteAndResultHandler
832+ :: Proxy (NoThrow :> api :> apis)
833+ -> Proxy m
834+ -> Proxy tag
835+ -> Req t
836+ -> Dynamic t BaseUrl
837+ -> ClientOptions
838+ -> (forall a. Event t (ReqResult tag a) -> m (Event t (ReqResult tag a)))
839+ -> Client t m (api :> NoThrow :> apis) tag
840+ clientWithRouteAndResultHandler _ =
841+ clientWithRouteAndResultHandler (Proxy :: Proxy (api :> NoThrow :> apis))
842+
src/Servant/Reflex/Multi.hs+224−55
@@ -7,6 +7,7 @@
77{-# LANGUAGE MultiParamTypeClasses #-}
88{-# LANGUAGE OverloadedStrings #-}
99{-# LANGUAGE PolyKinds #-}
10+{-# LANGUAGE RecordWildCards #-}
1011{-# LANGUAGE ScopedTypeVariables #-}
1112{-# LANGUAGE TupleSections #-}
1213{-# LANGUAGE TypeFamilies #-}
@@ -37,46 +38,83 @@ module Servant.Reflex.Multi (
3738 ) where
3839
3940------------------------------------------------------------------------------
40−import Control.Applicative (liftA2)
41−import Data.Functor.Compose (Compose (..), getCompose)
42−import Data.Proxy (Proxy (..))
43−import qualified Data.Set as Set
44−import Data.Text (Text)
45−import qualified Data.Text as T
46−import qualified Data.Text.Encoding as E
47−import GHC.TypeLits (KnownSymbol, symbolVal)
48−import Servant.API ((:<|>) (..), (:>), BasicAuth,
49− BasicAuthData, BuildHeadersTo (..),
50− Capture, Header, Headers (..),
51− HttpVersion, IsSecure,
52− MimeRender (..), MimeUnrender,
53− NoContent, QueryFlag, QueryParam,
54− QueryParams, Raw, ReflectMethod (..),
55− RemoteHost, ReqBody,
56− ToHttpApiData (..), Vault, Verb,
57− contentType)
58−import Servant.API.Description (Summary)
59−
60−import Reflex.Dom.Core (Dynamic, Event, Reflex,
61− XhrRequest (..),
62− XhrResponseHeaders (..),
63− attachPromptlyDynWith, constDyn)
41+import Control.Applicative (liftA2)
42+import Data.Functor.Compose (Compose (..),
43+ getCompose)
44+import Data.Proxy (Proxy (..))
45+import qualified Data.Set as Set
46+import Data.Text (Text)
47+import qualified Data.Text as T
48+import qualified Data.Text.Encoding as E
49+import GHC.TypeLits (KnownSymbol,
50+ symbolVal)
51+import Servant.API ((:<|>) (..),
52+ (:>),
53+ BasicAuth,
54+ BasicAuthData,
55+ BuildHeadersTo (..),
56+ Capture,
57+ Header,
58+ Headers (..),
59+ HttpVersion,
60+ IsSecure,
61+ NoContent,
62+ QueryFlag,
63+ QueryParam,
64+ QueryParams,
65+ Raw,
66+ ReflectMethod (..),
67+ RemoteHost,
68+ ReqBody,
69+ ToHttpApiData (..),
70+ Vault, Verb,
71+ contentType)
72+
73+import Reflex.Dom.Core (Dynamic,
74+ Event,
75+ IsXhrPayload,
76+ Reflex,
77+ XhrRequest (..),
78+ XhrResponseHeaders (..),
79+ attachPromptlyDynWith,
80+ constDyn)
6481------------------------------------------------------------------------------
65−import Servant.Common.BaseUrl (BaseUrl (..), Scheme (..),
66− SupportsServantReflex)
67−import Servant.Common.Req (ClientOptions,
68− QParam (..), QueryPart (..), Req,
69− ReqResult (..), addHeader, authData,
70− defReq,
71− defaultClientOptions,
72− performRequestsCT,
73− performRequestsNoBody,
74− performSomeRequestsAsync,
75− prependToPathParts, qParamToQueryPart,
76− qParams, reqBody, reqFailure,
77− reqMethod, reqSuccess, reqSuccess',
78− respHeaders, response, withCredentials)
79−import Servant.Reflex (BuildHeaderKeysTo (..), toHeaders)
82+import Servant.Checked.Exceptions.Internal (Envelope)
83+import Servant.Checked.Exceptions.Internal.Servant.API (NoThrow,
84+ Throwing,
85+ ThrowingNonterminal,
86+ Throws)
87+import Servant.Common.BaseUrl (BaseUrl (..),
88+ Scheme (..),
89+ SupportsServantReflex)
90+import Servant.Common.Req (ClientOptions,
91+ MimeUnrender,
92+ QParam (..),
93+ QueryPart (..),
94+ Req (..),
95+ ReqResult (..),
96+ SomeXhrRequest (..),
97+ addHeader,
98+ authData,
99+ defReq,
100+ defaultClientOptions,
101+ performRequestsCT,
102+ performRequestsNoBody,
103+ performSomeRequestsAsync,
104+ prependToPathParts,
105+ qParamToQueryPart,
106+ qParams,
107+ reqBody,
108+ reqFailure,
109+ reqMethod,
110+ reqSuccess,
111+ reqSuccess',
112+ respHeaders,
113+ response,
114+ withCredentials)
115+import Servant.Reflex (BuildHeaderKeysTo (..),
116+ GHCJS'MimeRender (..),
117+ toHeaders)
80118
81119
82120------------------------------------------------------------------------------
@@ -242,17 +280,6 @@ instance HasClientMulti t m sublayout f tag
242280 clientWithRouteMulti (Proxy :: Proxy sublayout) q f tag
243281
244282
245−------------------------------------------------------------------------------
246−instance (HasClientMulti t m sublayout f tag, KnownSymbol sym)
247− => HasClientMulti t m (Summary sym :> sublayout) f tag where
248−
249− type ClientMulti t m (Summary sym :> sublayout) f tag =
250− ClientMulti t m sublayout f tag
251−
252− clientWithRouteMulti Proxy q f tag =
253− clientWithRouteMulti (Proxy :: Proxy sublayout) q f tag
254−
255−
256283------------------------------------------------------------------------------
257284instance (KnownSymbol sym,
258285 ToHttpApiData a,
@@ -326,29 +353,31 @@ instance (SupportsServantReflex t m,
326353 clientWithRouteMulti _ _ _ _ _ _ opts rawReqs triggers = do
327354 let rawReqs' = sequence rawReqs :: Dynamic t (f (Either Text (XhrRequest ())))
328355 rawReqs'' = attachPromptlyDynWith (\fxhr t -> Compose (t, fxhr)) rawReqs' triggers
329− resps <- fmap (fmap aux . sequenceA . getCompose) <$> performSomeRequestsAsync opts rawReqs''
356+ rawReqs''' = (fmap . fmap . fmap) (SomeXhrRequest . fmap Just) rawReqs''
357+ resps <- fmap (fmap aux . sequenceA . getCompose) <$> performSomeRequestsAsync opts rawReqs'''
330358 return resps
331359 where
332360 aux (tag, Right r) = ResponseSuccess tag () r
333361 aux (tag, Left e) = RequestFailure tag e
334362
335363
336−instance (MimeRender ct a,
364+instance (GHCJS'MimeRender ct a, IsXhrPayload (ToSend ct a), Show (ToSend ct a),
337365 HasClientMulti t m sublayout f tag,
338366 Reflex t,
339367 Applicative f)
340368 => HasClientMulti t m (ReqBody (ct ': cts) a :> sublayout) f tag where
341369
342370 type ClientMulti t m (ReqBody (ct ': cts) a :> sublayout) f tag =
343− Dynamic t (f (Either Text a)) -> ClientMulti t m sublayout f tag
371+ Dynamic t (f (Either Text (ToConvert ct a))) -> ClientMulti t m sublayout f tag
344372
345373 clientWithRouteMulti Proxy q f tag reqs baseurl opts bodies =
346374 clientWithRouteMulti (Proxy :: Proxy sublayout) q f tag reqs' baseurl opts
347− where req' b r = r { reqBody = bodyBytesCT (constDyn b) }
375+ where req' b Req{..} = Req { reqBody = bodyBytesCT (constDyn b), .. }
348376 ctProxy = Proxy :: Proxy ct
377+ atProxy = Proxy :: Proxy a
349378 ctString = T.pack $ show $ contentType ctProxy
350379 bodyBytesCT b = Just $ (fmap . fmap)
351− (\b' -> (mimeRender ctProxy b', ctString))
380+ (\b' -> (ghcjsMimeRender ctProxy atProxy b', ctString))
352381 b
353382 reqs' = liftA2 req' <$> bodies <*> reqs
354383
@@ -399,3 +428,143 @@ instance (HasClientMulti t m api f tag, Reflex t, Applicative f)
399428 where
400429 req' a r = r { authData = Just (constDyn a) }
401430 reqs' = liftA2 req' <$> authdatas <*> reqs
431+
432+{- servant-checked-exceptions
433+~~~~~~~~~~~~~~~~~~~~~
434+-}
435+
436+-- | Change a 'Throws' into 'Throwing'.
437+instance (HasClientMulti t m (Throwing '[e] :> api) f tag) => HasClientMulti t m (Throws e :> api) f tag where
438+ type ClientMulti t m (Throws e :> api) f tag = ClientMulti t m (Throwing '[e] :> api) f tag
439+
440+ clientWithRouteMulti
441+ :: Proxy (Throws e :> api)
442+ -> Proxy m
443+ -> Proxy f
444+ -> Proxy tag
445+ -> Dynamic t (f (Req t))
446+ -> Dynamic t BaseUrl
447+ -> ClientOptions
448+ -> ClientMulti t m (Throwing '[e] :> api) f tag
449+ clientWithRouteMulti _ = clientWithRouteMulti (Proxy :: Proxy (Throwing '[e] :> api))
450+
451+-- | When @'Throwing' es@ comes before a 'Verb', change it into the same 'Verb'
452+-- but returning an @'Envelope' es@.
453+instance (HasClientMulti t m (Verb method status ctypes (Envelope es a)) f tag) =>
454+ HasClientMulti t m (Throwing es :> Verb method status ctypes a) f tag where
455+
456+ type ClientMulti t m (Throwing es :> Verb method status ctypes a) f tag =
457+ ClientMulti t m (Verb method status ctypes (Envelope es a)) f tag
458+
459+ clientWithRouteMulti
460+ :: Proxy (Throwing es :> Verb method status ctypes a)
461+ -> Proxy m
462+ -> Proxy f
463+ -> Proxy tag
464+ -> Dynamic t (f (Req t))
465+ -> Dynamic t BaseUrl
466+ -> ClientOptions
467+ -> ClientMulti t m (Verb method status ctypes (Envelope es a)) f tag
468+ clientWithRouteMulti Proxy =
469+ clientWithRouteMulti (Proxy :: Proxy (Verb method status ctypes (Envelope es a)))
470+
471+-- | When 'NoThrow' comes before a 'Verb', change it into the same 'Verb'
472+-- but returning an @'Envelope' \'[]@.
473+instance (HasClientMulti t m (Verb method status ctypes (Envelope '[] a)) f tag) =>
474+ HasClientMulti t m (NoThrow :> Verb method status ctypes a) f tag where
475+
476+ type ClientMulti t m (NoThrow :> Verb method status ctypes a) f tag =
477+ ClientMulti t m (Verb method status ctypes (Envelope '[] a)) f tag
478+
479+ clientWithRouteMulti
480+ :: Proxy (NoThrow :> Verb method status ctypes a)
481+ -> Proxy m
482+ -> Proxy f
483+ -> Proxy tag
484+ -> Dynamic t (f (Req t))
485+ -> Dynamic t BaseUrl
486+ -> ClientOptions
487+ -> ClientMulti t m (Verb method status ctypes (Envelope '[] a)) f tag
488+ clientWithRouteMulti Proxy =
489+ clientWithRouteMulti (Proxy :: Proxy (Verb method status ctypes (Envelope '[] a)))
490+
491+-- | When @'Throwing' es@ comes before ':<|>', push @'Throwing' es@ into each
492+-- branch of the API.
493+instance HasClientMulti t m ((Throwing es :> api1) :<|> (Throwing es :> api2)) f tag =>
494+ HasClientMulti t m (Throwing es :> (api1 :<|> api2)) f tag where
495+
496+ type ClientMulti t m (Throwing es :> (api1 :<|> api2)) f tag =
497+ ClientMulti t m ((Throwing es :> api1) :<|> (Throwing es :> api2)) f tag
498+
499+ clientWithRouteMulti
500+ :: Proxy (Throwing es :> (api1 :<|> api2))
501+ -> Proxy m
502+ -> Proxy f
503+ -> Proxy tag
504+ -> Dynamic t (f (Req t))
505+ -> Dynamic t BaseUrl
506+ -> ClientOptions
507+ -> ClientMulti t m ((Throwing es :> api1) :<|> (Throwing es :> api2)) f tag
508+ clientWithRouteMulti _ =
509+ clientWithRouteMulti (Proxy :: Proxy ((Throwing es :> api1) :<|> (Throwing es :> api2)))
510+
511+-- | When 'NoThrow' comes before ':<|>', push 'NoThrow' into each branch of the
512+-- API.
513+instance HasClientMulti t m ((NoThrow :> api1) :<|> (NoThrow :> api2)) f tag =>
514+ HasClientMulti t m (NoThrow :> (api1 :<|> api2)) f tag where
515+
516+ type ClientMulti t m (NoThrow :> (api1 :<|> api2)) f tag =
517+ ClientMulti t m ((NoThrow :> api1) :<|> (NoThrow :> api2)) f tag
518+
519+ clientWithRouteMulti
520+ :: Proxy (NoThrow :> (api1 :<|> api2))
521+ -> Proxy m
522+ -> Proxy f
523+ -> Proxy tag
524+ -> Dynamic t (f (Req t))
525+ -> Dynamic t BaseUrl
526+ -> ClientOptions
527+ -> ClientMulti t m ((NoThrow :> api1) :<|> (NoThrow :> api2)) f tag
528+ clientWithRouteMulti _ =
529+ clientWithRouteMulti (Proxy :: Proxy ((NoThrow :> api1) :<|> (NoThrow :> api2)))
530+
531+-- | When a @'Throws' e@ comes immediately after a @'Throwing' es@, 'Snoc' the
532+-- @e@ onto the @es@. Otherwise, if @'Throws' e@ comes before any other
533+-- combinator, push it down so it is closer to the 'Verb'.
534+instance HasClientMulti t m (ThrowingNonterminal (Throwing es :> api :> apis)) f tag =>
535+ HasClientMulti t m (Throwing es :> api :> apis) f tag where
536+
537+ type ClientMulti t m (Throwing es :> api :> apis) f tag =
538+ ClientMulti t m (ThrowingNonterminal (Throwing es :> api :> apis)) f tag
539+
540+ clientWithRouteMulti
541+ :: Proxy (Throwing es :> api :> apis)
542+ -> Proxy m
543+ -> Proxy f
544+ -> Proxy tag
545+ -> Dynamic t (f (Req t))
546+ -> Dynamic t BaseUrl
547+ -> ClientOptions
548+ -> ClientMulti t m (ThrowingNonterminal (Throwing es :> api :> apis)) f tag
549+ clientWithRouteMulti _ =
550+ clientWithRouteMulti (Proxy :: Proxy (ThrowingNonterminal (Throwing es :> api :> apis)))
551+
552+-- | When 'NoThrow' comes before any other combinator, push it down so it is
553+-- closer to the 'Verb'.
554+instance HasClientMulti t m (api :> NoThrow :> apis) f tag =>
555+ HasClientMulti t m (NoThrow :> api :> apis) f tag where
556+
557+ type ClientMulti t m (NoThrow :> api :> apis) f tag =
558+ ClientMulti t m (api :> NoThrow :> apis) f tag
559+
560+ clientWithRouteMulti
561+ :: Proxy (NoThrow :> api :> apis)
562+ -> Proxy m
563+ -> Proxy f
564+ -> Proxy tag
565+ -> Dynamic t (f (Req t))
566+ -> Dynamic t BaseUrl
567+ -> ClientOptions
568+ -> ClientMulti t m (api :> NoThrow :> apis) f tag
569+ clientWithRouteMulti _ =
570+ clientWithRouteMulti (Proxy :: Proxy (api :> NoThrow :> apis))
testserver/testserver.cabal+3−3
@@ -19,13 +19,13 @@ executable back
1919 main-is: Main.hs
2020 -- other-modules:
2121 -- other-extensions:
22− build-depends: aeson >= 0.9 && < 1.5
23− , base >=4.8 && < 5
22+ build-depends: aeson >= 0.9 && < 1.4
23+ , base >=4.8 && <4.12
2424 , mtl
2525 , snap >= 1.0 && < 1.2
2626 , snap-server >= 1.0 && < 1.2
2727 , snap-core >= 1.0 && < 1.1
28− , servant >= 0.16 && < 0.18
28+ , servant >= 0.8 && < 0.15
2929 , servant-snap >= 0.8 && < 0.9
3030 , text >= 1.0 && < 1.3
3131 -- hs-source-dirs:
travis.nix−2
@@ -31,7 +31,6 @@ let
3131 servant-snap = dontCheck ((import ./nix/servant-snap.nix {}) self super);
3232 testdriver = self.callCabal2nix "testdriver" ./testdriver {};
3333 testserver = import nix/testserver.nix ghcjsPkgs.servant-reflex self super;
34− servant-reflex = self.callPackage ./default.nix {};
3534 };
3635 };
3736
@@ -48,7 +47,6 @@ in
4847 inherit reflexPlatform ghcPkgs ghcjsPkgs;
4948
5049 build = ghcjsPkgs.servant-reflex;
51− buildGhc = ghcPkgs.servant-reflex;
5250 testserver = ghcPkgs.testserver;
5351 testdriver = ghcPkgs.testdriver;
5452 inherit testresults;