Compare commits

...

8 commits

Author SHA1 Message Date
Denis Redozubov
1aa18db1c8 Various 2019-06-20 15:44:45 +03:00
Denis Redozubov
619bfaab5b Update request body handling 2019-05-17 12:20:27 +03:00
Denis Redozubov
8a76df3a71 Update to 0.16 and replace jsaddle-webgtk with jsaddle-warp 2019-05-15 18:02:24 +03:00
Oleg Grenrus
0a237018de wip-servant-client-jsaddle 2019-05-15 13:11:00 +00:00
Vaibhav Sagar
f53e4ef960 servant-client-jsaddle: bump base bounds 2019-05-15 16:08:37 +03:00
Vaibhav Sagar
d20bdf7a89 Update .travis.yml with haskell-ci 2019-05-15 16:08:37 +03:00
Robert Hensing
5fe63b9018 servant-client-jsaddle: Remove some debug printing 2019-05-15 16:08:36 +03:00
Robert Hensing
3348665a88 Add servant-client-jsaddle 2019-05-15 16:08:36 +03:00
12 changed files with 661 additions and 0 deletions

View file

@ -8,6 +8,8 @@
# #
language: c language: c
dist: xenial dist: xenial
# services:
# - xvfb
git: git:
# whether to recursively clone submodules # whether to recursively clone submodules
submodules: false submodules: false
@ -190,6 +192,7 @@ script:
echo 'packages: "servant-machines-*/*.cabal"' >> cabal.project echo 'packages: "servant-machines-*/*.cabal"' >> cabal.project
echo 'packages: "servant-conduit-*/*.cabal"' >> cabal.project echo 'packages: "servant-conduit-*/*.cabal"' >> cabal.project
echo 'packages: "servant-pipes-*/*.cabal"' >> cabal.project echo 'packages: "servant-pipes-*/*.cabal"' >> cabal.project
echo 'packages: "servant-client-jsaddle-*/*.cabal"' >> cabal.project
if [ $HCNUMVER -eq 80404 ] || [ $HCNUMVER -eq 80604 ] ; then echo 'packages: "cookbook-basic-auth-*/*.cabal"' >> cabal.project ; fi if [ $HCNUMVER -eq 80404 ] || [ $HCNUMVER -eq 80604 ] ; then echo 'packages: "cookbook-basic-auth-*/*.cabal"' >> cabal.project ; fi
if [ $HCNUMVER -eq 80404 ] || [ $HCNUMVER -eq 80604 ] ; then echo 'packages: "cookbook-curl-mock-*/*.cabal"' >> cabal.project ; fi if [ $HCNUMVER -eq 80404 ] || [ $HCNUMVER -eq 80604 ] ; then echo 'packages: "cookbook-curl-mock-*/*.cabal"' >> cabal.project ; fi
if [ $HCNUMVER -eq 80404 ] || [ $HCNUMVER -eq 80604 ] ; then echo 'packages: "cookbook-basic-streaming-*/*.cabal"' >> cabal.project ; fi if [ $HCNUMVER -eq 80404 ] || [ $HCNUMVER -eq 80604 ] ; then echo 'packages: "cookbook-basic-streaming-*/*.cabal"' >> cabal.project ; fi

View file

@ -1,6 +1,7 @@
packages: packages:
servant/ servant/
servant-client/ servant-client/
servant-client-jsaddle/
servant-client-core/ servant-client-core/
servant-http-streams/ servant-http-streams/
servant-docs/ servant-docs/
@ -52,3 +53,17 @@ allow-newer:
servant-quickcheck:servant, servant-quickcheck:servant-client, servant-quickcheck:servant-server, servant-quickcheck:servant, servant-quickcheck:servant-client, servant-quickcheck:servant-server,
servant-quickcheck:hspec, servant-quickcheck:hspec,
servant-quickcheck:http-client servant-quickcheck:http-client
-- jsaddle
allow-newer:
jsaddle:aeson,
jsaddle:exceptions,
jsaddle:lens,
jsaddle-webkit2gtk:aeson,
jsaddle-webkit2gtk:haskell-gi-base,
jsaddle-dom:base-compat,
jsaddle-dom:lens
-- Ubuntu packages:
-- libgirepository1.0-dev
-- libwebkit2gtk-4.0-dev

View file

@ -0,0 +1,3 @@
0.13
----
First version

View file

@ -0,0 +1,30 @@
Copyright (c) 2014-2016, Zalora South East Asia Pte Ltd, Servant Contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Zalora South East Asia Pte Ltd nor the names of other
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,15 @@
# `servant-client-jsaddle`
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.
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`.
# Build
This package comes with a test suite that depends on `jsaddle-webkit2gtk`. You may want to skip that because of the heavy dependency footprint.
cabal new-build --allow-newer=aeson,http-types --disable-tests
# Usage
TBD. Similar to `servant-client` and `servant-client-ghcjs`.

View file

@ -0,0 +1,2 @@
import Distribution.Simple
main = defaultMain

View file

@ -0,0 +1,39 @@
{ mkDerivation, aeson, base, bytestring, case-insensitive
, containers, exceptions, hspec, hspec-discover, http-media
, http-types, jsaddle, jsaddle-dom, jsaddle-warp, monad-control
, mtl, QuickCheck, semigroupoids, servant, servant-client-core
, stdenv, string-conversions, text, transformers
, transformers-base, wai, wai-extra, warp
}:
mkDerivation {
pname = "servant-client-jsaddle";
version = "0.16";
src = ./.;
isLibrary = true;
isExecutable = true;
libraryHaskellDepends = [
base bytestring case-insensitive containers exceptions http-media
http-types jsaddle jsaddle-dom monad-control mtl semigroupoids
servant servant-client-core string-conversions text transformers
transformers-base
];
executableHaskellDepends = [
aeson base bytestring case-insensitive containers exceptions hspec
http-media http-types jsaddle jsaddle-dom jsaddle-warp
monad-control mtl QuickCheck semigroupoids servant
servant-client-core string-conversions text
transformers transformers-base wai wai-extra warp
];
executableToolDepends = [ hspec-discover ];
testHaskellDepends = [
aeson base bytestring case-insensitive containers exceptions hspec
http-media http-types jsaddle jsaddle-dom jsaddle-warp
monad-control mtl QuickCheck semigroupoids servant
servant-client-core string-conversions text
transformers transformers-base wai wai-extra warp
];
testToolDepends = [ hspec-discover ];
homepage = "http://haskell-servant.readthedocs.org/";
description = "automatic derivation of querying functions for servant webservices for jsaddle (GHCJS, GHC + WebKit, GHC + websockets, etc)";
license = stdenv.lib.licenses.bsd3;
}

View file

@ -0,0 +1,164 @@
name: servant-client-jsaddle
version: 0.16
synopsis: automatic derivation of querying functions for servant webservices for jsaddle (GHCJS, GHC + WebKit, GHC + websockets, etc)
description:
This library lets you automatically derive Haskell functions that
let you query each endpoint of a <http://hackage.haskell.org/package/servant servant> webservice.
.
See <http://haskell-servant.readthedocs.org/en/stable/tutorial/Client.html the client section of the tutorial>.
.
<https://github.com/haskell-servant/servant/blob/master/servant-client/CHANGELOG.md CHANGELOG>
license: BSD3
license-file: LICENSE
author: Servant Contributors
maintainer: haskell-servant-maintainers@googlegroups.com
copyright: 2014-2016 Zalora South East Asia Pte Ltd, 2016-2019 Servant Contributors
category: Servant, Web
build-type: Simple
cabal-version: >=1.10
tested-with:
GHC ==8.0.2
|| ==8.2.2
|| ==8.4.4
|| ==8.6.2
homepage: http://haskell-servant.readthedocs.org/
Bug-reports: http://github.com/haskell-servant/servant/issues
extra-source-files:
CHANGELOG.md
README.md
source-repository head
type: git
location: http://github.com/haskell-servant/servant.git
library
exposed-modules:
Servant.Client.JSaddle
Servant.Client.Internal.JSaddleXhrClient
-- Bundled with GHC: Lower bound to not force re-installs
-- text and mtl are bundled starting with GHC-8.4
build-depends:
base >= 4.9 && < 4.13
, bytestring >= 0.10.8.1 && < 0.11
, containers >= 0.5.7.1 && < 0.6
, mtl >= 2.2.2 && < 2.3
, text >= 1.2.3.0 && < 1.3
, transformers >= 0.5.2.0 && < 0.6
-- GHCJS dependencies
if impl(ghcjs)
build-depends: ghcjs-base
-- Servant dependencies.
-- Strict dependency on `servant-client-core` as we re-export things.
build-depends:
servant == 0.16.*
, servant-client-core == 0.16.*
-- Other dependencies: Lower bound around what is in the latest Stackage LTS.
-- Here can be exceptions if we really need features from the newer versions.
build-depends:
case-insensitive >= 1.2.0.0 && < 1.3.0.0
, exceptions >= 0.10.0 && < 0.11
, http-media >= 0.7.1.3 && < 0.8
, http-types >= 0.12.2 && < 0.13
, jsaddle >= 0.9.5.0 && < 0.10
, jsaddle-dom >= 0.9.2.0 && < 0.10
, monad-control >= 1.0.2.3 && < 1.1
, semigroupoids >= 5.3.1 && < 5.4
, string-conversions >= 0.4.0.1 && < 0.5
, transformers-base >= 0.4.4 && < 0.5
hs-source-dirs: src
default-language: Haskell2010
ghc-options: -Wall
if impl(ghc >= 8.0)
ghc-options: -Wno-redundant-constraints
executable spec-test
type: exitcode-stdio-1.0
ghc-options: -Wall
default-language: Haskell2010
main-is: Main.hs
-- Dependencies inherited from the library. No need to specify bounds.
build-depends:
base
, aeson
, bytestring
, case-insensitive
, containers
, exceptions
, http-media
, http-types
, jsaddle
, jsaddle-warp
, jsaddle-dom
, monad-control
, mtl
, semigroupoids
, servant
, servant-client-core
, servant-client-jsaddle
, servant-server
, string-conversions
, text
, transformers
, transformers-base
, wai
, wai-extra
, warp
-- Additonal dependencies
build-depends:
hspec
, QuickCheck
build-tool-depends:
hspec-discover:hspec-discover >=2.4.4 && <2.5
test-suite spec
type: exitcode-stdio-1.0
ghc-options: -Wall
default-language: Haskell2010
hs-source-dirs: test
main-is: Spec.hs
other-modules:
Servant.Client.JsSpec
-- Dependencies inherited from the library. No need to specify bounds.
build-depends:
base
, aeson
, bytestring
, case-insensitive
, containers
, exceptions
, http-media
, http-types
, jsaddle
, jsaddle-warp
, jsaddle-dom
, monad-control
, mtl
, semigroupoids
, servant
, servant-client-core
, servant-client-jsaddle
, servant-server
, string-conversions
, text
, transformers
, transformers-base
, wai
, wai-extra
, warp
-- Additonal dependencies
build-depends:
hspec
, QuickCheck
build-tool-depends:
hspec-discover:hspec-discover >=2.4.4 && <2.5

View file

@ -0,0 +1,267 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
module Servant.Client.Internal.JSaddleXhrClient where
import Control.Concurrent
import Control.Exception
import Control.Monad
import Control.Monad.Catch (MonadCatch, MonadThrow)
import Control.Monad.Error.Class (MonadError (..))
import Control.Monad.Reader
import Control.Monad.Trans.Except
import Data.Bifunctor
import Data.ByteString.Builder (toLazyByteString)
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as L
import Data.CaseInsensitive
import Data.Char
import Data.Foldable (toList)
import Data.Functor.Alt (Alt (..))
import Data.Proxy (Proxy (..))
import qualified Data.Sequence as Seq
import Data.String.Conversions
import qualified Data.Text.Encoding as T
import qualified Data.Text.Encoding.Error as T
import GHC.Generics
import qualified GHCJS.Buffer as Buffer
import qualified JSDOM
import qualified JSDOM.Custom.XMLHttpRequest as JS
import qualified JSDOM.EventM as JSDOM
import qualified JSDOM.Generated.Location as Location
import qualified JSDOM.Generated.Window as Window
import JSDOM.Types (DOM, askDOM, runDOM, DOMContext)
import qualified JSDOM.Types as JS
import qualified JavaScript.TypedArray.ArrayBuffer as ArrayBuffer
import qualified Language.Javascript.JSaddle.Types as JSaddle
import Network.HTTP.Media (renderHeader)
import Network.HTTP.Types
import Servant.Client.Core
-- Note: assuming encoding UTF-8
data ClientEnv
= ClientEnv
{ baseUrl :: BaseUrl
-- | Modify the XMLHttpRequest at will, right before sending.
, fixUpXhr :: JS.XMLHttpRequest -> DOM ()
}
-- | Default 'ClientEnv'
mkClientEnv :: BaseUrl -> ClientEnv
mkClientEnv burl = ClientEnv burl (const (pure ()))
instance Show ClientEnv where
showsPrec prec (ClientEnv burl _) =
showParen (prec >= 11)
( showString "ClientEnv {"
. showString "baseUrl = "
. showsPrec 0 burl
. showString ", fixUpXhr = <function>"
. showString "}"
)
client :: HasClient ClientM api => Proxy api -> Client ClientM api
client api = api `clientIn` (Proxy :: Proxy ClientM)
newtype ClientM a = ClientM
{ fromClientM :: ReaderT ClientEnv (ExceptT ClientError DOM) a }
deriving ( Functor, Applicative, Monad, MonadIO, Generic
, MonadReader ClientEnv, MonadError ClientError)
deriving instance MonadThrow DOM => MonadThrow ClientM
deriving instance MonadCatch DOM => MonadCatch ClientM
-- | Try clients in order, last error is preserved.
instance Alt ClientM where
a <!> b = a `catchError` const b
instance RunClient ClientM where
throwClientError = throwError
runRequest r = do
d <- ClientM askDOM
performRequest d r
runClientM :: ClientM a -> ClientEnv -> DOM (Either ClientError a)
runClientM cm env = runExceptT $ flip runReaderT env $ fromClientM cm
runClientM' :: ClientM a -> DOM (Either ClientError a)
runClientM' m = do
burl <- getDefaultBaseUrl
runClientM m (mkClientEnv burl)
getDefaultBaseUrl :: DOM BaseUrl
getDefaultBaseUrl = do
win <- JSDOM.currentWindow >>= \mw -> case mw of
Just x -> pure x
Nothing -> fail "Can not determine default base url without window."
curLoc <- Window.getLocation win
protocolStr <- Location.getProtocol curLoc
portStr <- Location.getPort curLoc
hostname <- Location.getHostname curLoc
let protocol
| (protocolStr :: JS.JSString) == "https:"
= Https
| otherwise = Http
port :: Int
port | null portStr = case protocol of
Http -> 80
Https -> 443
| otherwise = read portStr
pure (BaseUrl protocol hostname port "")
performRequest :: DOMContext -> Request -> ClientM Response
performRequest domc req = do
xhr <- JS.newXMLHttpRequest `runDOM` domc
burl <- asks baseUrl
fixUp <- asks fixUpXhr
performXhr xhr burl req fixUp `runDOM` domc
resp <- toResponse domc xhr
let status = statusCode (responseStatusCode resp)
unless (status >= 200 && status < 300) $ do
let f b = (burl, L.toStrict $ toLazyByteString b)
throwError $ FailureResponse (bimap (const ()) f req) resp
pure resp
-- * performing requests
-- Performs the xhr and blocks until the response was received
performXhr :: JS.XMLHttpRequest -> BaseUrl -> Request -> (JS.XMLHttpRequest -> DOM ()) -> DOM ()
performXhr xhr burl request fixUp = do
let username, password :: Maybe JS.JSString
username = Nothing; password = Nothing
JS.open xhr (decodeUtf8Lenient $ requestMethod request) (toUrl burl request) True username password
setHeaders xhr request
fixUp xhr
waiter <- liftIO $ newEmptyMVar
cleanup <- JSDOM.on xhr JS.readyStateChange $ do
state <- JS.getReadyState xhr
case state of
-- onReadyStateChange's callback can fire state 4
-- (which means "request finished and response is ready")
-- multiple times. By using tryPutMVar, only the first time
-- state 4 is fired will cause an MVar to be put. Subsequent
-- fires are ignored.
4 -> void $ liftIO $ tryPutMVar waiter ()
_ -> return ()
sendXhr xhr (toBody request)
liftIO $ takeMVar waiter
cleanup
toUrl :: BaseUrl -> Request -> JS.JSString
toUrl burl request =
let pathS = JS.toJSString $ decodeUtf8Lenient $ L.toStrict $ toLazyByteString $
requestPath request
queryS =
JS.toJSString $ decodeUtf8Lenient $
renderQuery True $
toList $
requestQueryString request
in JS.toJSString (showBaseUrl burl) <> pathS <> queryS :: JS.JSString
setHeaders :: JS.XMLHttpRequest -> Request -> DOM ()
setHeaders xhr request = do
forM_ (toList $ requestAccept request) $ \mediaType -> -- FIXME review
JS.setRequestHeader
xhr
("Accept" :: JS.JSString)
(decodeUtf8Lenient $ renderHeader mediaType)
forM_ (requestBody request) $ \(_, mediaType) ->
JS.setRequestHeader
xhr
("Content-Type" :: JS.JSString)
(decodeUtf8Lenient $ renderHeader mediaType)
forM_ (toList $ requestHeaders request) $ \(key, value) ->
JS.setRequestHeader xhr (decodeUtf8Lenient $ original key) (decodeUtf8Lenient value)
-- ArrayBufferView is a type that only exists in the spec and covers many concrete types.
castMutableArrayBufferToArrayBufferView :: ArrayBuffer.MutableArrayBuffer -> DOM JS.ArrayBufferView
castMutableArrayBufferToArrayBufferView x = JS.liftJSM $ do
JS.fromJSValUnchecked $ JS.pToJSVal x
sendXhr :: JS.XMLHttpRequest -> Maybe L.ByteString -> DOM ()
sendXhr xhr Nothing = JS.send xhr
sendXhr xhr (Just body) = do
-- Reason for copy: hopefully offset will be 0 and length b == len
-- FIXME: use a typed array constructor that accepts offset and length and skip the copy
(b, _offset, _len) <- JSaddle.ghcjsPure $ Buffer.fromByteString $ BS.copy $ L.toStrict body
b' <- Buffer.thaw b
b'' <- JSaddle.ghcjsPure $ Buffer.getArrayBuffer b'
JS.sendArrayBuffer xhr =<< castMutableArrayBufferToArrayBufferView b''
toBody :: Request -> Maybe L.ByteString
toBody request = case requestBody request of
Nothing -> Nothing
Just (RequestBodyBS "" , _) -> Nothing
Just (RequestBodyBS x , _) -> Just $ L.fromStrict x
-- FIXME: not implemented
Just (RequestBodySource _ , _) -> Nothing
Just (RequestBodyLBS "" , _) -> Nothing
Just (RequestBodyLBS x , _) -> Just x
-- * inspecting the xhr response
-- This function is only supposed to handle 'ConnectionError's. Other
-- 'ClientError's are created in Servant.Client.Req.
toResponse :: DOMContext -> JS.XMLHttpRequest -> ClientM Response
toResponse domc xhr = do
let inDom :: DOM a -> ClientM a
inDom = flip runDOM domc
status <- inDom $ JS.getStatus xhr
case status of
0 -> throwError $ ConnectionError $ SomeException $ userError "connection error"
_ -> inDom $ do
statusText <- BS.pack <$> JS.getStatusText xhr
headers <- parseHeaders <$> JS.getAllResponseHeaders xhr
responseText <- maybe "" (L.fromStrict . BS.pack) <$> JS.getResponseText xhr -- FIXME: Text/Binary? Performance? Test?
pure Response
{ responseStatusCode = mkStatus (fromIntegral status) statusText
, responseBody = responseText
, responseHeaders = Seq.fromList headers
, responseHttpVersion = http11 -- this is made up
}
parseHeaders :: String -> ResponseHeaders
parseHeaders s =
(first mk . first strip . second strip . parseHeader) <$>
splitOn "\r\n" (cs s)
where
parseHeader :: BS.ByteString -> (BS.ByteString, BS.ByteString)
parseHeader h = case BS.breakSubstring ":" (cs h) of
(key, BS.drop 1 -> value) -> (key, value)
splitOn :: BS.ByteString -> BS.ByteString -> [BS.ByteString]
splitOn separator input = case BS.breakSubstring separator input of
(prefix, "") -> [prefix]
(prefix, rest) -> prefix : splitOn separator (BS.drop (BS.length separator) rest)
strip :: BS.ByteString -> BS.ByteString
strip = BS.dropWhile isSpace . BS.reverse . BS.dropWhile isSpace . BS.reverse
decodeUtf8Lenient :: BS.ByteString -> JS.JSString
decodeUtf8Lenient = JS.toJSString . T.decodeUtf8With T.lenientDecode

View file

@ -0,0 +1,20 @@
-- | This module provides 'client' which can automatically generate
-- querying functions for each endpoint just from the type representing your
-- API.
module Servant.Client.JSaddle
(
client
, ClientM
, runClientM
, runClientM'
-- * Configuration
, ClientEnv(..)
, mkClientEnv
, getDefaultBaseUrl
, module Servant.Client.Core.Reexport
) where
import Servant.Client.Internal.JSaddleXhrClient
import Servant.Client.Core.Reexport

View file

@ -0,0 +1,102 @@
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE OverloadedStrings #-}
module Servant.Client.JsSpec where
import Control.Concurrent
import Control.Monad.Trans
import Data.Aeson
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import Data.Proxy
import Data.String
import Data.Word
import Debug.Trace
import GHC.Generics
import qualified JSDOM
import qualified JSDOM.Window as Window
import Language.Javascript.JSaddle.Monad (JSM)
import qualified Language.Javascript.JSaddle.Monad as JSaddle
import qualified Language.Javascript.JSaddle.Warp as JW
import qualified Network.HTTP.Types as Http
import qualified Network.Wai as Wai
import Network.Wai.Handler.Warp as Warp
import Network.Wai.Middleware.AddHeaders
import Servant.API
import Servant.Client.Core
import Servant.Client.Internal.JSaddleXhrClient
import Servant.Server
import Test.Hspec
type TestApi = ReqBody '[OctetStream] ByteString :> Post '[JSON] TestResponse
testApi :: Proxy TestApi
testApi = Proxy
data TestResponse = TestResponse { byteList :: [Word8] }
deriving (Generic, ToJSON, FromJSON, Show, Eq)
testServer :: Server TestApi
testServer x = do
pure . TestResponse . B.unpack $ x
testClient :: Client ClientM TestApi
testClient = client testApi
-- WARNING: approximation!
jsaddleFinally :: JSM b -> JSM a -> JSM a
jsaddleFinally handler m = JSaddle.bracket (pure ()) (const handler) (const m)
-- jsaddleFinally handler m = JSaddle.catch (m <* handler) (\e -> handler >> throw (e :: SomeException))
close :: JSM ()
close = do
mw <- JSDOM.currentWindow
case mw of
Just w -> do
liftIO $ putStrLn "Closing window..."
Window.close w
Nothing -> liftIO $ putStrLn "Can't close the window!"
logRequest :: Wai.Middleware
logRequest app request respond = do
putStrLn "Request"
print request
app request (\response -> do
putStrLn "Response Headers"
print `mapM_` (Wai.responseHeaders response)
respond response)
corsHeaders :: (IsString s1, IsString s2) => [(s1, s2)]
corsHeaders = [ ("Access-Control-Allow-Origin", "null")
, ("Access-Control-Allow-Methods", "POST")
, ("Access-Control-Allow-Headers", "content-type")
]
addCors :: Wai.Middleware
addCors app request respond =
if Wai.requestMethod request == "OPTIONS"
then respond $ Wai.responseLBS Http.status200 corsHeaders ""
else addHeaders corsHeaders app request respond
spec :: Spec
spec = do
describe "Servant.Client.Js" $ do
it "Receive a properly encoded response" $ do
Warp.testWithApplication (pure $ logRequest $ addCors $ serve testApi testServer) $ \portNr -> do
let clientEnv = mkClientEnv BaseUrl { baseUrlScheme = Http
, baseUrlHost = "localhost"
, baseUrlPort = fromIntegral portNr
, baseUrlPath = "/"
}
JW.run portNr $ jsaddleFinally close $ do
liftIO $ threadDelay $ 1000 * 1000
-- a mix of valid utf-8 and non-utf8 bytes
let bytes = [0x01, 0xff, 0x02, 0xfe, 0x03, 0xfd, 0x00, 0x64, 0xc3, 0xbb, 0x68, 0xc3]
response <- flip runClientM clientEnv $ do
testClient (B.pack bytes)
liftIO $ response `shouldBe` Right (TestResponse bytes)

View file

@ -0,0 +1 @@
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}