git.iko.soy/typeable/servant
on master help sign in
Add servant-client-jsaddle

- servant-client-jsaddle: Remove some debug printing - Update .travis.yml with haskell-ci - servant-client-jsaddle: bump base bounds - Add libgirepository1.0-dev - servant-client-jsaddle: bump upper bound on containers - servant-client-jsaddle: relax upper bound on semigroupoids - servant-client-jsaddle: bump servant-client-core dependency - servant-client-jsaddle: fix compatibility - servant-client-jsaddle: import correct module - .travis.yml: run xvfb for headless GUI testing - Use ghcjs-dom instead of jsaddle-dom directly. - Also use ghcjs-dom in tests. - Ignore exceptions on send - they are handled in toResponse. - Apparently ghcjs-dom does use the same exception these days. - Got rid of obsolete comment. - Make sure response gets handled even in case of exception. - Update servant-client-jsaddle/servant-client-jsaddle.cabal Dependingon ghcjs-dom avoids the dependency on jsaddle-dom on ghcjs and have slightly better performance on ghcjs. Co-Authored-By: Herbert Valerio Riedel <hvr@gnu.org>

commite14a14fb5cfcc2cd5e2fb59b15d871c8235f7b35 authorRobert Hensing <robert@roberthensing.nl> dateSat 28 Jul 2018, 14:40 (8 years ago) parent12e64fd
15 files changed+700−7
.travis.yml+14−5 Makefile+1−1 cabal.ghcjs.project+2−1 cabal.haskell-ci+2 cabal.project+3 screenshot.png servant-jsaddle/CHANGELOG.md+4 servant-jsaddle/LICENSE+30 servant-jsaddle/README.md+15 servant-jsaddle/Setup.hs+2 servant-jsaddle/servant-jsaddle.cabal+125 servant-jsaddle/src/Servant/Client/Internal/JSaddleXhrClient.hs+311 servant-jsaddle/src/Servant/Client/JSaddle.hs+20 servant-jsaddle/test/Servant/Client/JSaddleSpec.hs+163 servant-jsaddle/test/Spec.hs+8
.travis.yml+14−5
@@ -4,7 +4,7 @@
44#
55# For more information, see https://github.com/haskell-CI/haskell-ci
66#
7−# version: 0.5.20180907
7+# version: 0.5.20190908
88#
99language: c
1010dist: xenial
@@ -14,6 +14,8 @@ git:
1414branches:
1515 only:
1616 - master
17+addons:
18+ google: stable
1719cache:
1820 directories:
1921 - $HOME/.cabal/packages
@@ -115,12 +117,14 @@ install:
115117 echo " prefix: $CABALHOME" >> $CABALHOME/config
116118 echo "repository hackage.haskell.org" >> $CABALHOME/config
117119 echo " url: http://hackage.haskell.org/" >> $CABALHOME/config
120+ - GHCJOBS=-j2
118121 - |
119− echo "program-default-options" >> $CABALHOME/config
120− echo " ghc-options: -j2" >> $CABALHOME/config
122+ echo "program-default-options" >> $CABALHOME/config
123+ echo " ghc-options: $GHCJOBS +RTS -M6G -RTS" >> $CABALHOME/config
121124 - cat $CABALHOME/config
122125 - rm -fv cabal.project cabal.project.local cabal.project.freeze
123126 - travis_retry ${CABAL} v2-update -v
127+ - if $GHCJS ; then (cd /tmp && ${CABAL} v2-install -w ghc-8.4.4 cabal-plan | color_cabal_output) ; fi
124128 - if $GHCJS ; then (cd /tmp && ${CABAL} v2-install -w ghc-8.4.4 hspec-discover | color_cabal_output) ; fi
125129 # Generate cabal.project
126130 - rm -rf cabal.project cabal.project.local cabal.project.freeze
@@ -128,6 +132,7 @@ install:
128132 - |
129133 echo "packages: servant" >> cabal.project
130134 if ! $GHCJS ; then echo "packages: servant-client" >> cabal.project ; fi
135+ echo "packages: servant-jsaddle" >> cabal.project
131136 echo "packages: servant-client-core" >> cabal.project
132137 if ! $GHCJS ; then echo "packages: servant-http-streams" >> cabal.project ; fi
133138 if ! $GHCJS ; then echo "packages: servant-docs" >> cabal.project ; fi
@@ -165,11 +170,12 @@ install:
165170 echo "allow-newer: io-streams-1.5.1.0:network" >> cabal.project
166171 echo "allow-newer: openssl-streams-1.2.2.0:network" >> cabal.project
167172 echo "optimization: False" >> cabal.project
168− - "for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/-[^-]*$//' | (grep -vE -- '^(cookbook-basic-auth|cookbook-basic-streaming|cookbook-curl-mock|cookbook-db-postgres-pool|cookbook-db-sqlite-simple|cookbook-file-upload|cookbook-generic|cookbook-https|cookbook-pagination|cookbook-structuring-apis|cookbook-testing|cookbook-using-custom-monad|cookbook-using-free-client|servant|servant-client|servant-client-core|servant-conduit|servant-docs|servant-foreign|servant-http-streams|servant-machines|servant-pipes|servant-server|tutorial)$' || true) | sed 's/^/constraints: /' | sed 's/$/ installed/' >> cabal.project.local; done"
173+ - "for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/-[^-]*$//' | (grep -vE -- '^(cookbook-basic-auth|cookbook-basic-streaming|cookbook-curl-mock|cookbook-db-postgres-pool|cookbook-db-sqlite-simple|cookbook-file-upload|cookbook-generic|cookbook-https|cookbook-pagination|cookbook-structuring-apis|cookbook-testing|cookbook-using-custom-monad|cookbook-using-free-client|servant|servant-client|servant-client-core|servant-conduit|servant-docs|servant-foreign|servant-http-streams|servant-jsaddle|servant-machines|servant-pipes|servant-server|tutorial)$' || true) | sed 's/^/constraints: /' | sed 's/$/ installed/' >> cabal.project.local; done"
169174 - cat cabal.project || true
170175 - cat cabal.project.local || true
171176 - if [ -f "servant/configure.ac" ]; then (cd "servant" && autoreconf -i); fi
172177 - if [ -f "servant-client/configure.ac" ]; then (cd "servant-client" && autoreconf -i); fi
178+ - if [ -f "servant-jsaddle/configure.ac" ]; then (cd "servant-jsaddle" && autoreconf -i); fi
173179 - if [ -f "servant-client-core/configure.ac" ]; then (cd "servant-client-core" && autoreconf -i); fi
174180 - if [ -f "servant-http-streams/configure.ac" ]; then (cd "servant-http-streams" && autoreconf -i); fi
175181 - if [ -f "servant-docs/configure.ac" ]; then (cd "servant-docs" && autoreconf -i); fi
@@ -209,6 +215,7 @@ script:
209215 - find . -maxdepth 1 -type f -name '*.tar.gz' -exec rm '{}' \;
210216 - PKGDIR_servant="$(find . -maxdepth 1 -type d -regex '.*/servant-[0-9.]*')"
211217 - PKGDIR_servant_client="$(find . -maxdepth 1 -type d -regex '.*/servant-client-[0-9.]*')"
218+ - PKGDIR_servant_jsaddle="$(find . -maxdepth 1 -type d -regex '.*/servant-jsaddle-[0-9.]*')"
212219 - PKGDIR_servant_client_core="$(find . -maxdepth 1 -type d -regex '.*/servant-client-core-[0-9.]*')"
213220 - PKGDIR_servant_http_streams="$(find . -maxdepth 1 -type d -regex '.*/servant-http-streams-[0-9.]*')"
214221 - PKGDIR_servant_docs="$(find . -maxdepth 1 -type d -regex '.*/servant-docs-[0-9.]*')"
@@ -237,6 +244,7 @@ script:
237244 - |
238245 echo "packages: ${PKGDIR_servant}" >> cabal.project
239246 if ! $GHCJS ; then echo "packages: ${PKGDIR_servant_client}" >> cabal.project ; fi
247+ echo "packages: ${PKGDIR_servant_jsaddle}" >> cabal.project
240248 echo "packages: ${PKGDIR_servant_client_core}" >> cabal.project
241249 if ! $GHCJS ; then echo "packages: ${PKGDIR_servant_http_streams}" >> cabal.project ; fi
242250 if ! $GHCJS ; then echo "packages: ${PKGDIR_servant_docs}" >> cabal.project ; fi
@@ -274,7 +282,7 @@ script:
274282 echo "allow-newer: io-streams-1.5.1.0:network" >> cabal.project
275283 echo "allow-newer: openssl-streams-1.2.2.0:network" >> cabal.project
276284 echo "optimization: False" >> cabal.project
277− - "for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/-[^-]*$//' | (grep -vE -- '^(cookbook-basic-auth|cookbook-basic-streaming|cookbook-curl-mock|cookbook-db-postgres-pool|cookbook-db-sqlite-simple|cookbook-file-upload|cookbook-generic|cookbook-https|cookbook-pagination|cookbook-structuring-apis|cookbook-testing|cookbook-using-custom-monad|cookbook-using-free-client|servant|servant-client|servant-client-core|servant-conduit|servant-docs|servant-foreign|servant-http-streams|servant-machines|servant-pipes|servant-server|tutorial)$' || true) | sed 's/^/constraints: /' | sed 's/$/ installed/' >> cabal.project.local; done"
285+ - "for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/-[^-]*$//' | (grep -vE -- '^(cookbook-basic-auth|cookbook-basic-streaming|cookbook-curl-mock|cookbook-db-postgres-pool|cookbook-db-sqlite-simple|cookbook-file-upload|cookbook-generic|cookbook-https|cookbook-pagination|cookbook-structuring-apis|cookbook-testing|cookbook-using-custom-monad|cookbook-using-free-client|servant|servant-client|servant-client-core|servant-conduit|servant-docs|servant-foreign|servant-http-streams|servant-jsaddle|servant-machines|servant-pipes|servant-server|tutorial)$' || true) | sed 's/^/constraints: /' | sed 's/$/ installed/' >> cabal.project.local; done"
278286 - cat cabal.project || true
279287 - cat cabal.project.local || true
280288 - echo -en 'travis_fold:end:unpack\\r'
@@ -285,6 +293,7 @@ script:
285293 - echo -en 'travis_fold:end:build-everything\\r'
286294 # Testing...
287295 - if ! $GHCJS ; then ${CABAL} v2-test $WITHCOMPILER ${TEST} ${BENCH} all | color_cabal_output ; fi
296+ - if $GHCJS ; then for testexe in $(cabal-plan list-bins '*:test:*' | awk '{ print $2 }'); do echo $testexe; nodejs ${testexe}.jsexe/all.js; done ; fi
288297 # haddock...
289298 - echo 'haddock...' && echo -en 'travis_fold:start:haddock\\r'
290299 - if ! $GHCJS ; then ${CABAL} v2-haddock $WITHCOMPILER --with-haddock $HADDOCK ${TEST} ${BENCH} all | color_cabal_output ; fi
Makefile+1−1
@@ -9,7 +9,7 @@ really-all :
99 $(MAKE) build-ghc
1010 $(MAKE) build-ghc HC=ghc-8.0.2
1111 $(MAKE) build-ghc HC=ghc-8.2.2
12− $(MAKE) build-ghc HC=ghc-8.6.3
12+ $(MAKE) build-ghc HC=ghc-8.6.5
1313 $(MAKE) build-ghcjs
1414
1515build-ghc :
cabal.ghcjs.project+2−1
@@ -3,7 +3,8 @@
33packages:
44 servant/
55 servant-client-core/
6− servant-client-ghcjs/
6+ servant-jsaddle/
77
88-- we need to tell cabal we are using GHCJS
99compiler: ghcjs
10+tests: True
cabal.haskell-ci+2
@@ -1,6 +1,8 @@
11folds: all-but-test
22branches: master
33jobs-selection: any
4+google-chrome: True
5+ghcjs-tests: True
46
57-- https://github.com/haskell/cabal/issues/6176
68ghcjs-tools: hspec-discover
cabal.project+3
@@ -1,6 +1,7 @@
11packages:
22 servant/
33 servant-client/
4+ servant-jsaddle/
45 servant-client-core/
56 servant-http-streams/
67 servant-docs/
@@ -32,6 +33,8 @@ packages:
3233 doc/cookbook/using-free-client
3334 -- doc/cookbook/open-id-connect
3435
36+
37+
3538tests: True
3639optimization: False
3740-- reorder-goals: True
screenshot.pngnew file

Binary file not shown.

servant-jsaddle/CHANGELOG.mdnew file+4
@@ -0,0 +1,4 @@
1+X.Y
2+----
3+
4+Initial release
servant-jsaddle/LICENSEnew file+30
@@ -0,0 +1,30 @@
1+Copyright (c) 2014-2016, Zalora South East Asia Pte Ltd, Servant Contributors
2+
3+All rights reserved.
4+
5+Redistribution and use in source and binary forms, with or without
6+modification, are permitted provided that the following conditions are met:
7+
8+ * Redistributions of source code must retain the above copyright
9+ notice, this list of conditions and the following disclaimer.
10+
11+ * Redistributions in binary form must reproduce the above
12+ copyright notice, this list of conditions and the following
13+ disclaimer in the documentation and/or other materials provided
14+ with the distribution.
15+
16+ * Neither the name of Zalora South East Asia Pte Ltd nor the names of other
17+ contributors may be used to endorse or promote products derived
18+ from this software without specific prior written permission.
19+
20+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
servant-jsaddle/README.mdnew file+15
@@ -0,0 +1,15 @@
1+# `servant-client-jsaddle`
2+
3+This is a an implementation of the `servant-client-core` API on top of `jsaddle`, a framework that lets you write Haskell programs that compile to javascript to run in a browser or compile to native code that connects to a browser.
4+
5+It is similar to `servant-client-ghcjs`, except it supports native compilation and native GHCi. It even reuses some of the logic from `servant-client-ghcjs`.
6+
7+# Build
8+
9+This package comes with a test suite that depends on `jsaddle-webkit2gtk`. You may want to skip that because of the heavy dependency footprint.
10+
11+ cabal new-build --allow-newer=aeson,http-types --disable-tests
12+
13+# Usage
14+
15+TBD. Similar to `servant-client` and `servant-client-ghcjs`.
servant-jsaddle/Setup.hsnew file+2
@@ -0,0 +1,2 @@
1+import Distribution.Simple
2+main = defaultMain
servant-jsaddle/servant-jsaddle.cabalnew file+125
@@ -0,0 +1,125 @@
1+name: servant-jsaddle
2+version: 0.16
3+synopsis:
4+ automatic derivation of querying functions for servant webservices for jsaddle
5+
6+description:
7+ This library lets you automatically derive Haskell functions that
8+ let you query each endpoint of a <http://hackage.haskell.org/package/servant servant> webservice.
9+ .
10+ See <http://haskell-servant.readthedocs.org/en/stable/tutorial/Client.html the client section of the tutorial>.
11+ .
12+ <https://github.com/haskell-servant/servant/blob/master/servant-client/CHANGELOG.md CHANGELOG>
13+
14+license: BSD3
15+license-file: LICENSE
16+author: Servant Contributors
17+maintainer: haskell-servant-maintainers@googlegroups.com
18+copyright:
19+ 2014-2016 Zalora South East Asia Pte Ltd, 2016-2017 Servant Contributors
20+
21+category: Servant, Web
22+build-type: Simple
23+cabal-version: >=1.10
24+tested-with:
25+ GHC ==8.0.2 || ==8.2.2 || ==8.4.4 || ==8.6.5
26+ , GHCJS ==8.4
27+
28+homepage: http://haskell-servant.readthedocs.org/
29+bug-reports: http://github.com/haskell-servant/servant/issues
30+extra-source-files:
31+ CHANGELOG.md
32+ README.md
33+
34+source-repository head
35+ type: git
36+ location: http://github.com/haskell-servant/servant.git
37+
38+library
39+ default-language: Haskell2010
40+ hs-source-dirs: src
41+ ghc-options: -Wall
42+ exposed-modules:
43+ Servant.Client.Internal.JSaddleXhrClient
44+ Servant.Client.JSaddle
45+
46+ -- Bundled with GHC: Lower bound to not force re-installs
47+ -- text and mtl are bundled starting with GHC-8.4
48+ build-depends:
49+ base >=4.9 && <4.13
50+ , bytestring >=0.10.8.1 && <0.11
51+ , containers >=0.5.7.1 && <0.7
52+ , mtl >=2.2.2 && <2.3
53+ , text >=1.2.3.0 && <1.3
54+ , transformers >=0.5.2.0 && <0.6
55+
56+ if impl(ghcjs -any)
57+ build-depends: ghcjs-base
58+
59+ -- Servant dependencies.
60+ -- Strict dependency on `servant-client-core` as we re-export things.
61+ build-depends: servant-client-core >=0.16 && <0.16.1
62+ build-depends:
63+ base-compat >=0.10.5 && <0.11
64+ , case-insensitive >=1.2.0.0 && <1.3
65+ , exceptions >=0.10.0 && <0.11
66+ , ghcjs-dom
67+ , http-media >=0.7.1.3 && <0.9
68+ , http-types >=0.12.2 && <0.13
69+ , jsaddle >=0.9.6.0 && <0.10
70+ , monad-control >=1.0.2.3 && <1.1
71+ , semigroupoids >=5.3.1 && <5.4
72+ , string-conversions >=0.3 && <0.5
73+ , transformers-base >=0.4.4 && <0.5
74+
75+ if impl(ghc >=8.0)
76+ ghc-options: -Wno-redundant-constraints
77+
78+test-suite spec
79+ type: exitcode-stdio-1.0
80+ ghc-options: -Wall
81+ default-language: Haskell2010
82+ hs-source-dirs: test
83+ main-is: Spec.hs
84+
85+ if impl(ghcjs -any)
86+ build-depends:
87+ base
88+ , servant-jsaddle
89+
90+ else
91+ other-modules: Servant.Client.JSaddleSpec
92+
93+ -- Dependencies inherited from the library. No need to specify bounds.
94+ build-depends:
95+ base
96+ , bytestring
97+ , containers
98+ , exceptions
99+ , ghcjs-dom
100+ , http-media
101+ , http-types
102+ , jsaddle
103+ , mtl
104+ , process
105+ , semigroupoids
106+ , servant
107+ , servant-client-core
108+ , servant-jsaddle
109+ , servant-server
110+ , string-conversions
111+ , text
112+ , wai
113+ , wai-cors
114+ , wai-extra
115+ , warp
116+ , websockets
117+
118+ -- Additonal dependencies
119+ build-depends:
120+ aeson
121+ , hspec
122+ , jsaddle-warp
123+ , QuickCheck
124+
125+ build-tool-depends: hspec-discover:hspec-discover >=2.4.4 && <2.5
servant-jsaddle/src/Servant/Client/Internal/JSaddleXhrClient.hsnew file+311
@@ -0,0 +1,311 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DeriveGeneric #-}
3+{-# LANGUAGE FlexibleContexts #-}
4+{-# LANGUAGE FlexibleInstances #-}
5+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
6+{-# LANGUAGE MultiParamTypeClasses #-}
7+{-# LANGUAGE OverloadedStrings #-}
8+{-# LANGUAGE ScopedTypeVariables #-}
9+{-# LANGUAGE StandaloneDeriving #-}
10+{-# LANGUAGE TypeFamilies #-}
11+{-# LANGUAGE UndecidableInstances #-}
12+{-# LANGUAGE ViewPatterns #-}
13+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
14+
15+module Servant.Client.Internal.JSaddleXhrClient where
16+
17+import Prelude ()
18+import Prelude.Compat
19+
20+import Control.Concurrent
21+ (MVar, newEmptyMVar, takeMVar, tryPutMVar)
22+import Control.Exception
23+ (Exception, toException)
24+import Control.Monad
25+ (forM_, unless, void)
26+import Control.Monad.Catch
27+ (MonadCatch, MonadThrow, catch)
28+import Control.Monad.Error.Class
29+ (MonadError (..))
30+import Control.Monad.IO.Class
31+ (MonadIO (..))
32+import Control.Monad.Reader
33+ (MonadReader, ReaderT, asks, runReaderT)
34+import Control.Monad.Trans.Except
35+ (ExceptT, runExceptT)
36+import Data.Bifunctor
37+ (bimap, first, second)
38+import Data.ByteString.Builder
39+ (toLazyByteString)
40+import qualified Data.ByteString.Char8 as BS
41+import qualified Data.ByteString.Lazy as BSL
42+import qualified Data.ByteString.Lazy as L
43+import Data.CaseInsensitive
44+ (mk, original)
45+import Data.Char
46+ (isSpace)
47+import Data.Foldable
48+ (toList)
49+import Data.Functor.Alt
50+ (Alt (..))
51+import Data.Proxy
52+ (Proxy (..))
53+import qualified Data.Sequence as Seq
54+import Data.String.Conversions
55+ (cs)
56+import qualified Data.Text.Encoding as T
57+import qualified Data.Text.Encoding.Error as T
58+import GHC.Generics
59+import qualified GHCJS.Buffer as Buffer
60+import qualified GHCJS.DOM
61+import qualified GHCJS.DOM.EventM as JSDOM
62+import qualified GHCJS.DOM.Location as Location
63+import GHCJS.DOM.Types
64+ (DOM, DOMContext, askDOM, runDOM)
65+import qualified GHCJS.DOM.Types as JS
66+import qualified GHCJS.DOM.Window as Window
67+import qualified GHCJS.DOM.XMLHttpRequest as JS
68+import qualified JavaScript.TypedArray.ArrayBuffer as ArrayBuffer
69+import qualified Language.Javascript.JSaddle.Types as JSaddle
70+import Network.HTTP.Media
71+ (renderHeader)
72+import Network.HTTP.Types
73+ (ResponseHeaders, http11, mkStatus, renderQuery, statusCode)
74+import System.IO
75+ (hPutStrLn, stderr)
76+
77+import Servant.Client.Core
78+
79+-- Note: assuming encoding UTF-8
80+
81+data ClientEnv
82+ = ClientEnv
83+ { baseUrl :: BaseUrl
84+ -- | Modify the XMLHttpRequest at will, right before sending.
85+ , fixUpXhr :: JS.XMLHttpRequest -> DOM ()
86+ }
87+
88+data JSaddleConnectionError = JSaddleConnectionError
89+ deriving (Eq, Show)
90+
91+instance Exception JSaddleConnectionError
92+
93+-- | Default 'ClientEnv'
94+mkClientEnv :: BaseUrl -> ClientEnv
95+mkClientEnv burl = ClientEnv burl (const (pure ()))
96+
97+instance Show ClientEnv where
98+ showsPrec prec (ClientEnv burl _) =
99+ showParen (prec >= 11)
100+ ( showString "ClientEnv {"
101+ . showString "baseUrl = "
102+ . showsPrec 0 burl
103+ . showString ", fixUpXhr = <function>"
104+ . showString "}"
105+ )
106+
107+client :: HasClient ClientM api => Proxy api -> Client ClientM api
108+client api = api `clientIn` (Proxy :: Proxy ClientM)
109+
110+newtype ClientM a = ClientM
111+ { fromClientM :: ReaderT ClientEnv (ExceptT ClientError DOM) a }
112+ deriving ( Functor, Applicative, Monad, MonadIO, Generic
113+ , MonadReader ClientEnv, MonadError ClientError)
114+deriving instance MonadThrow DOM => MonadThrow ClientM
115+deriving instance MonadCatch DOM => MonadCatch ClientM
116+
117+-- | Try clients in order, last error is preserved.
118+instance Alt ClientM where
119+ a <!> b = a `catchError` const b
120+
121+instance RunClient ClientM where
122+ throwClientError = throwError
123+ runRequest r = do
124+ d <- ClientM askDOM
125+ performRequest d r
126+
127+runClientM :: ClientM a -> ClientEnv -> DOM (Either ClientError a)
128+runClientM cm env = runExceptT $ flip runReaderT env $ fromClientM cm
129+
130+runClientM' :: ClientM a -> DOM (Either ClientError a)
131+runClientM' m = do
132+ burl <- getDefaultBaseUrl
133+ runClientM m (mkClientEnv burl)
134+
135+getDefaultBaseUrl :: DOM BaseUrl
136+getDefaultBaseUrl = do
137+ win <- GHCJS.DOM.currentWindow >>= \mw -> case mw of
138+ Just x -> pure x
139+ Nothing -> fail "Can not determine default base url without window."
140+ curLoc <- Window.getLocation win
141+
142+ protocolStr <- Location.getProtocol curLoc
143+ portStr <- Location.getPort curLoc
144+ hostname <- Location.getHostname curLoc
145+
146+ let protocol
147+ | (protocolStr :: JS.JSString) == "https:"
148+ = Https
149+ | otherwise = Http
150+
151+ port :: Int
152+ port | null portStr = case protocol of
153+ Http -> 80
154+ Https -> 443
155+ | otherwise = read portStr
156+
157+ pure (BaseUrl protocol hostname port "")
158+
159+performRequest :: DOMContext -> Request -> ClientM Response
160+performRequest domc req = do
161+ xhr <- JS.newXMLHttpRequest `runDOM` domc
162+ burl <- asks baseUrl
163+ fixUp <- asks fixUpXhr
164+ performXhr xhr burl req fixUp `runDOM` domc
165+ resp <- toResponse domc xhr
166+
167+ let status = statusCode (responseStatusCode resp)
168+ unless (status >= 200 && status < 300) $
169+ throwError $ mkFailureResponse burl req resp
170+
171+ pure resp
172+
173+
174+-- * performing requests
175+-- Performs the xhr and blocks until the response was received
176+performXhr :: JS.XMLHttpRequest -> BaseUrl -> Request -> (JS.XMLHttpRequest -> DOM ()) -> DOM ()
177+performXhr xhr burl request fixUp = do
178+
179+ let username, password :: Maybe JS.JSString
180+ username = Nothing; password = Nothing
181+
182+ JS.open xhr (decodeUtf8Lenient $ requestMethod request) (toUrl burl request) True username password
183+ setHeaders xhr request
184+ fixUp xhr
185+
186+ waiter <- liftIO $ newEmptyMVar
187+
188+ cleanup <- JSDOM.on xhr JS.readyStateChange $ do
189+ state <- JS.getReadyState xhr
190+ case state of
191+ -- onReadyStateChange's callback can fire state 4
192+ -- (which means "request finished and response is ready")
193+ -- multiple times. By using tryPutMVar, only the first time
194+ -- state 4 is fired will cause an MVar to be put. Subsequent
195+ -- fires are ignored.
196+ 4 -> void $ liftIO $ tryPutMVar waiter ()
197+ _ -> return ()
198+
199+ sendXhr xhr (toBody request) `catch` handleXHRError waiter -- We handle any errors in `toResponse`.
200+
201+ liftIO $ takeMVar waiter
202+
203+ cleanup
204+
205+ where
206+
207+ handleXHRError :: MVar () -> JS.XHRError -> DOM ()
208+ handleXHRError waiter e = do
209+ liftIO $ hPutStrLn stderr $ "servant-client-jsaddle: exception in `sendXhr` (should get handled in response handling): " <> show e
210+ void $ liftIO $ tryPutMVar waiter ()
211+
212+
213+toUrl :: BaseUrl -> Request -> JS.JSString
214+toUrl burl request =
215+ let pathS = JS.toJSString $ decodeUtf8Lenient $ L.toStrict $ toLazyByteString $
216+ requestPath request
217+ queryS =
218+ JS.toJSString $ decodeUtf8Lenient $
219+ renderQuery True $
220+ toList $
221+ requestQueryString request
222+ in JS.toJSString (showBaseUrl burl) <> pathS <> queryS :: JS.JSString
223+
224+setHeaders :: JS.XMLHttpRequest -> Request -> DOM ()
225+setHeaders xhr request = do
226+ forM_ (toList $ requestAccept request) $ \mediaType -> -- FIXME review
227+ JS.setRequestHeader
228+ xhr
229+ ("Accept" :: JS.JSString)
230+ (decodeUtf8Lenient $ renderHeader mediaType)
231+
232+ forM_ (requestBody request) $ \(_, mediaType) ->
233+ JS.setRequestHeader
234+ xhr
235+ ("Content-Type" :: JS.JSString)
236+ (decodeUtf8Lenient $ renderHeader mediaType)
237+
238+ forM_ (toList $ requestHeaders request) $ \(key, value) ->
239+ JS.setRequestHeader xhr (decodeUtf8Lenient $ original key) (decodeUtf8Lenient value)
240+
241+-- ArrayBufferView is a type that only exists in the spec and covers many concrete types.
242+castMutableArrayBufferToArrayBufferView :: ArrayBuffer.MutableArrayBuffer -> DOM JS.ArrayBufferView
243+castMutableArrayBufferToArrayBufferView x = JS.liftJSM $ do
244+ JS.fromJSValUnchecked $ JS.pToJSVal x
245+
246+mkFailureResponse :: BaseUrl -> Request -> ResponseF BSL.ByteString -> ClientError
247+mkFailureResponse burl request =
248+ FailureResponse (bimap (const ()) f request)
249+ where
250+ f b = (burl, BSL.toStrict $ toLazyByteString b)
251+
252+sendXhr :: JS.XMLHttpRequest -> Maybe L.ByteString -> DOM ()
253+sendXhr xhr Nothing = JS.send xhr
254+sendXhr xhr (Just body) = do
255+ -- Reason for copy: hopefully offset will be 0 and length b == len
256+ -- FIXME: use a typed array constructor that accepts offset and length and skip the copy
257+ (b, _offset, _len) <- JSaddle.ghcjsPure $ Buffer.fromByteString $ BS.copy $ L.toStrict body
258+ b' <- Buffer.thaw b
259+ b'' <- JSaddle.ghcjsPure $ Buffer.getArrayBuffer b'
260+ JS.sendArrayBuffer xhr =<< castMutableArrayBufferToArrayBufferView b''
261+
262+toBody :: Request -> Maybe L.ByteString
263+toBody request = case requestBody request of
264+ Nothing -> Nothing
265+ Just (RequestBodyLBS "", _) -> Nothing
266+ Just (RequestBodyLBS x, _) -> Just x
267+ Just (RequestBodyBS "", _) -> Nothing
268+ Just (RequestBodyBS x, _) -> Just $ L.fromStrict x
269+ Just (RequestBodySource _, _) -> error "RequestBodySource isn't supported"
270+
271+-- * inspecting the xhr response
272+
273+-- This function is only supposed to handle 'ConnectionError's. Other
274+-- 'ClientError's are created in Servant.Client.Req.
275+toResponse :: DOMContext -> JS.XMLHttpRequest -> ClientM Response
276+toResponse domc xhr = do
277+ let inDom :: DOM a -> ClientM a
278+ inDom = flip runDOM domc
279+ status <- inDom $ JS.getStatus xhr
280+ case status of
281+ 0 -> throwError $ ConnectionError $ toException JSaddleConnectionError
282+ _ -> inDom $ do
283+ statusText <- BS.pack <$> JS.getStatusText xhr
284+ headers <- parseHeaders <$> JS.getAllResponseHeaders xhr
285+ responseText <- maybe "" (L.fromStrict . BS.pack) <$> JS.getResponseText xhr -- FIXME: Text/Binary? Performance? Test?
286+ pure Response
287+ { responseStatusCode = mkStatus (fromIntegral status) statusText
288+ , responseBody = responseText
289+ , responseHeaders = Seq.fromList headers
290+ , responseHttpVersion = http11 -- this is made up
291+ }
292+
293+parseHeaders :: String -> ResponseHeaders
294+parseHeaders s =
295+ (first mk . first strip . second strip . parseHeader) <$>
296+ splitOn "\r\n" (cs s)
297+ where
298+ parseHeader :: BS.ByteString -> (BS.ByteString, BS.ByteString)
299+ parseHeader h = case BS.breakSubstring ":" (cs h) of
300+ (key, BS.drop 1 -> value) -> (key, value)
301+
302+ splitOn :: BS.ByteString -> BS.ByteString -> [BS.ByteString]
303+ splitOn separator input = case BS.breakSubstring separator input of
304+ (prefix, "") -> [prefix]
305+ (prefix, rest) -> prefix : splitOn separator (BS.drop (BS.length separator) rest)
306+
307+ strip :: BS.ByteString -> BS.ByteString
308+ strip = BS.dropWhile isSpace . BS.reverse . BS.dropWhile isSpace . BS.reverse
309+
310+decodeUtf8Lenient :: BS.ByteString -> JS.JSString
311+decodeUtf8Lenient = JS.toJSString . T.decodeUtf8With T.lenientDecode
servant-jsaddle/src/Servant/Client/JSaddle.hsnew file+20
@@ -0,0 +1,20 @@
1+-- | This module provides 'client' which can automatically generate
2+-- querying functions for each endpoint just from the type representing your
3+-- API.
4+module Servant.Client.JSaddle
5+ (
6+ client
7+ , ClientM
8+ , runClientM
9+ , runClientM'
10+
11+ -- * Configuration
12+ , ClientEnv(..)
13+ , mkClientEnv
14+ , getDefaultBaseUrl
15+
16+ , module Servant.Client.Core.Reexport
17+ ) where
18+
19+import Servant.Client.Internal.JSaddleXhrClient
20+import Servant.Client.Core.Reexport
servant-jsaddle/test/Servant/Client/JSaddleSpec.hsnew file+163
@@ -0,0 +1,163 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE DeriveAnyClass #-}
3+{-# LANGUAGE DeriveGeneric #-}
4+{-# LANGUAGE OverloadedStrings #-}
5+{-# LANGUAGE TypeApplications #-}
6+{-# LANGUAGE TypeOperators #-}
7+module Servant.Client.JSaddleSpec where
8+
9+import Control.Concurrent
10+ (threadDelay)
11+import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar)
12+import Control.Monad.Trans
13+import Data.Aeson
14+import Data.ByteString
15+ (ByteString)
16+import qualified Data.ByteString as B
17+import Data.Proxy
18+import Data.String
19+import Data.Word
20+import GHC.Generics
21+import qualified GHCJS.DOM
22+import qualified GHCJS.DOM.Window as Window
23+import Language.Javascript.JSaddle.Monad
24+ (JSM)
25+import qualified Language.Javascript.JSaddle.Monad as JSaddle
26+import qualified Language.Javascript.JSaddle.Run as Run
27+import qualified Language.Javascript.JSaddle.WebSockets as WS
28+import qualified Network.HTTP.Types as Http
29+import qualified Network.Wai as Wai
30+import Network.Wai.Handler.Warp as Warp
31+import qualified System.Process as P
32+import Network.Wai.Middleware.AddHeaders
33+import Network.Wai.Middleware.Cors
34+ (simpleCors)
35+import Network.WebSockets
36+ (defaultConnectionOptions)
37+import Servant.API
38+import Servant.Client.JSaddle
39+import Servant.Server
40+import Test.Hspec
41+
42+type TestApi = ReqBody '[OctetStream] ByteString :> Post '[JSON] TestResponse
43+testApi :: Proxy TestApi
44+testApi = Proxy
45+
46+data TestResponse = TestResponse { byteList :: [Word8] }
47+ deriving (Generic, ToJSON, FromJSON, Show, Eq)
48+
49+testServer :: Server TestApi
50+testServer x = do
51+ pure . TestResponse . B.unpack $ x
52+
53+testClient :: Client ClientM TestApi
54+testClient = client testApi
55+
56+-- WARNING: approximation!
57+jsaddleFinally :: JSM b -> JSM a -> JSM a
58+jsaddleFinally handler m = JSaddle.bracket (pure ()) (const handler) (const m)
59+-- jsaddleFinally handler m = JSaddle.catch (m <* handler) (\e -> handler >> throw (e :: SomeException))
60+
61+close :: JSM ()
62+close = do
63+ mw <- GHCJS.DOM.currentWindow
64+ case mw of
65+ Just w -> do
66+ liftIO $ putStrLn "Closing window..."
67+ Window.close w
68+ Nothing -> liftIO $ putStrLn "Can't close the window!"
69+
70+
71+
72+spec :: Spec
73+spec = do
74+ describe "Servant.Client.JSaddle" $ do
75+ it "Receive a properly encoded response" $ do
76+ -- A mvar to tell promptly when we are done
77+ done <- newEmptyMVar
78+
79+ -- How this work:
80+ --
81+ -- 1. we start server warp, which serves simple API
82+ -- 2. we start client warp, which serves jsaddle running the 'action'
83+ -- 3. we run google-chrome-stable to open jsaddle page and to run the test
84+
85+ let action :: Int -> JSM ()
86+ action serverPort = do
87+ liftIO $ threadDelay $ 500 * 1000
88+ -- a mix of valid utf-8 and non-utf8 bytes
89+ let bytes = [0x01, 0xff, 0x02, 0xfe, 0x03, 0xfd, 0x00, 0x64, 0xc3, 0xbb, 0x68, 0xc3]
90+ response <- flip runClientM clientEnv $ testClient (B.pack bytes)
91+ liftIO $ print response
92+ liftIO $ response `shouldBe` Right (TestResponse bytes)
93+
94+ -- we are done.
95+ liftIO $ putMVar done ()
96+ where
97+ clientEnv = mkClientEnv BaseUrl
98+ { baseUrlScheme = Http
99+ , baseUrlHost = "localhost"
100+ , baseUrlPort = fromIntegral serverPort
101+ , baseUrlPath = "/"
102+ }
103+
104+ let serverApp :: IO Application
105+ serverApp = pure $ logRequest $ addCors $ serve testApi testServer
106+
107+ Warp.testWithApplication serverApp $ \serverPort -> do
108+
109+ let clientApp :: IO Application
110+ clientApp = WS.jsaddleOr defaultConnectionOptions (action serverPort >> Run.syncPoint) WS.jsaddleApp
111+
112+ Warp.testWithApplication (simpleCors <$> clientApp) $ \clientPort -> do
113+ putStrLn $ "server http://localhost:" ++ show serverPort
114+ putStrLn $ "client http://localhost:" ++ show clientPort
115+ putStrLn $ "google-chrome-stable --headless --disable-gpu --screenshot http://localhost:" ++ show clientPort
116+
117+ -- threadDelay $ 1000 * 1000 * 1000
118+
119+ -- Run headless chrome
120+ -- https://docs.travis-ci.com/user/gui-and-headless-browsers/#using-the-chrome-addon-in-the-headless-mode
121+ -- https://developers.google.com/web/updates/2017/04/headless-chrome
122+ hdl <- P.spawnProcess "google-chrome-stable"
123+ [ "--headless"
124+ , "--disable-gpu"
125+ , "--remote-debugging-port=9222" -- TODO: bind to random port
126+ , "http://localhost:" ++ show clientPort
127+ ]
128+
129+ -- wait for test to run.
130+ takeMVar done
131+
132+ -- kill chrome
133+ P.terminateProcess hdl
134+
135+-------------------------------------------------------------------------------
136+-- Logger middleware
137+-------------------------------------------------------------------------------
138+
139+logRequest :: Wai.Middleware
140+logRequest app request respond = do
141+ putStrLn "Request"
142+ print request
143+ app request $ \response -> do
144+ putStrLn "Response Headers"
145+ mapM_ print (Wai.responseHeaders response)
146+ respond response
147+
148+-------------------------------------------------------------------------------
149+-- OPTIONS
150+-------------------------------------------------------------------------------
151+
152+corsHeaders :: (IsString s1, IsString s2) => [(s1, s2)]
153+corsHeaders =
154+ [ ("Access-Control-Allow-Origin", "*")
155+ , ("Access-Control-Allow-Methods", "POST")
156+ , ("Access-Control-Allow-Headers", "content-type")
157+ ]
158+
159+addCors :: Wai.Middleware
160+addCors app request respond =
161+ if Wai.requestMethod request == "OPTIONS"
162+ then respond $ Wai.responseLBS Http.status200 corsHeaders ""
163+ else addHeaders corsHeaders app request respond
servant-jsaddle/test/Spec.hsnew file+8
@@ -0,0 +1,8 @@
1+{-# LANGUAGE CPP #-}
2+#ifdef __GHCJS__
3+module Main (main) where
4+main :: IO ()
5+main = return ()
6+#else
7+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
8+#endif