Compare commits
8 commits
master
...
wip-servan
Author | SHA1 | Date | |
---|---|---|---|
|
1aa18db1c8 | ||
|
619bfaab5b | ||
|
8a76df3a71 | ||
|
0a237018de | ||
|
f53e4ef960 | ||
|
d20bdf7a89 | ||
|
5fe63b9018 | ||
|
3348665a88 |
12 changed files with 661 additions and 0 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
3
servant-client-jsaddle/CHANGELOG.md
Normal file
3
servant-client-jsaddle/CHANGELOG.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
0.13
|
||||||
|
----
|
||||||
|
First version
|
30
servant-client-jsaddle/LICENSE
Normal file
30
servant-client-jsaddle/LICENSE
Normal 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.
|
15
servant-client-jsaddle/README.md
Normal file
15
servant-client-jsaddle/README.md
Normal 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`.
|
2
servant-client-jsaddle/Setup.hs
Normal file
2
servant-client-jsaddle/Setup.hs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import Distribution.Simple
|
||||||
|
main = defaultMain
|
39
servant-client-jsaddle/default.nix
Normal file
39
servant-client-jsaddle/default.nix
Normal 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;
|
||||||
|
}
|
164
servant-client-jsaddle/servant-client-jsaddle.cabal
Normal file
164
servant-client-jsaddle/servant-client-jsaddle.cabal
Normal 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
|
|
@ -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
|
20
servant-client-jsaddle/src/Servant/Client/JSaddle.hs
Normal file
20
servant-client-jsaddle/src/Servant/Client/JSaddle.hs
Normal 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
|
102
servant-client-jsaddle/test/Servant/Client/JsSpec.hs
Normal file
102
servant-client-jsaddle/test/Servant/Client/JsSpec.hs
Normal 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)
|
1
servant-client-jsaddle/test/Spec.hs
Normal file
1
servant-client-jsaddle/test/Spec.hs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
|
Loading…
Reference in a new issue