This commit is contained in:
Sönke Hahn 2015-11-25 17:31:58 +08:00
parent 9cc344b95b
commit d46a41662e
23 changed files with 656 additions and 111 deletions

6
.travis-ghc.sh Executable file
View file

@ -0,0 +1,6 @@
#/usr/bin/env bash
set -ev
for package in $(cat sources.txt); do
(cd $package && tinc && cabal configure --enable-tests --disable-optimization && cabal build && cabal test) || exit 1
done

12
.travis-ghcjs.sh Executable file
View file

@ -0,0 +1,12 @@
#/usr/bin/env bash
set -ev
# tinc
#cabal exec which hspec-discover
#barf
cabal install hspec-discover --prefix $HOME/.local
# export PATH=$HOME/huhu/bin:$PATH
which hspec-discover
cd servant-client
./test/ghcjs/run-tests.sh

View file

@ -5,6 +5,8 @@ language: c
env: env:
- GHCVER=7.8.4 - GHCVER=7.8.4
- GHCVER=7.10.2 - GHCVER=7.10.2
- GHCVER=7.10.2
GHCJS=true
addons: addons:
apt: apt:
@ -17,6 +19,7 @@ addons:
- libgmp-dev - libgmp-dev
install: install:
# set up tinc
- (mkdir -p $HOME/.local/bin && cd $HOME/.local/bin && wget https://zalora-public.s3.amazonaws.com/tinc && chmod +x tinc) - (mkdir -p $HOME/.local/bin && cd $HOME/.local/bin && wget https://zalora-public.s3.amazonaws.com/tinc && chmod +x tinc)
- export PATH=/opt/ghc/$GHCVER/bin:/opt/cabal/1.22/bin:$PATH - export PATH=/opt/ghc/$GHCVER/bin:/opt/cabal/1.22/bin:$PATH
- ghc --version - ghc --version
@ -24,9 +27,19 @@ install:
- travis_retry cabal update - travis_retry cabal update
- sed -i 's/^jobs:/-- jobs:/' ${HOME}/.cabal/config - sed -i 's/^jobs:/-- jobs:/' ${HOME}/.cabal/config
# set up stack (for ghcjs in servant-client)
- travis_retry curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack'
- stack --version
script: script:
- for package in $(cat sources.txt); do (cd $package && tinc && cabal configure --enable-tests --disable-optimization && cabal build && cabal test) || exit 1; done - if [ "$GHCJS" = "true" ];
then ./.travis-ghcjs.sh ;
else ./.travis-ghc.sh ;
fi
- export CASHER_TIME_OUT=500
cache: cache:
directories: directories:
- $HOME/.tinc/cache - $HOME/.tinc/cache
- $HOME/.stack
- $HOME/.ghcjs

View file

@ -28,6 +28,16 @@ library
Servant.Client Servant.Client
Servant.Common.BaseUrl Servant.Common.BaseUrl
Servant.Common.Req Servant.Common.Req
-- fixme: shouldn't be part of the public API:
Servant.Client.PerformRequest.Base
other-modules:
Servant.Client.PerformRequest
if impl(ghcjs)
other-modules:
Servant.Client.PerformRequest.GHCJS
else
other-modules:
Servant.Client.PerformRequest.GHC
build-depends: build-depends:
base >=4.7 && <5 base >=4.7 && <5
, aeson , aeson
@ -46,9 +56,14 @@ library
, text , text
, transformers , transformers
, transformers-compat , transformers-compat
, case-insensitive
if impl(ghcjs)
build-depends:
ghcjs-base
, ghcjs-prim
hs-source-dirs: src hs-source-dirs: src
default-language: Haskell2010 default-language: Haskell2010
ghc-options: -Wall ghc-options: -Wall -Werror
test-suite spec test-suite spec
type: exitcode-stdio-1.0 type: exitcode-stdio-1.0
@ -59,7 +74,16 @@ test-suite spec
main-is: Spec.hs main-is: Spec.hs
other-modules: other-modules:
Servant.ClientSpec Servant.ClientSpec
, Servant.Client.PerformRequest.BaseSpec
, Servant.Client.TestServer
, Servant.Common.BaseUrlSpec , Servant.Common.BaseUrlSpec
, Spec
if impl(ghcjs)
other-modules:
Servant.Client.TestServer.GHCJS
else
other-modules:
Servant.Client.TestServer.GHC
build-depends: build-depends:
base == 4.* base == 4.*
, transformers , transformers
@ -67,7 +91,7 @@ test-suite spec
, aeson , aeson
, bytestring , bytestring
, deepseq , deepseq
, hspec == 2.* , hspec >= 2.2.1 && < 2.3
, http-client , http-client
, http-media , http-media
, http-types , http-types
@ -80,3 +104,6 @@ test-suite spec
, text , text
, wai , wai
, warp , warp
, mockery
, safe
, process

View file

@ -39,6 +39,7 @@ import qualified Network.HTTP.Types.Header as HTTP
import Servant.API import Servant.API
import Servant.Common.BaseUrl import Servant.Common.BaseUrl
import Servant.Common.Req import Servant.Common.Req
import Servant.Client.PerformRequest (ServantError(..))
-- * Accessing APIs as a Client -- * Accessing APIs as a Client

View file

@ -0,0 +1,13 @@
{-# LANGUAGE CPP #-}
module Servant.Client.PerformRequest (
ServantError(..),
performHttpRequest,
) where
import Servant.Client.PerformRequest.Base
#ifdef __GHCJS__
import Servant.Client.PerformRequest.GHCJS
#else
import Servant.Client.PerformRequest.GHC
#endif

View file

@ -0,0 +1,63 @@
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
module Servant.Client.PerformRequest.Base where
import Control.Arrow
import Control.Exception
import qualified Data.ByteString.Char8 as BS
import Data.ByteString.Lazy
import Data.CaseInsensitive
import Data.Char
import Data.String.Conversions
import Data.Typeable
import Network.HTTP.Media
import Network.HTTP.Types
data ServantError
= FailureResponse
{ responseStatus :: Status
, responseContentType :: MediaType
, responseBody :: ByteString
}
| DecodeFailure
{ decodeError :: String
, responseContentType :: MediaType
, responseBody :: ByteString
}
| UnsupportedContentType
{ responseContentType :: MediaType
, responseBody :: ByteString
}
| InvalidContentTypeHeader
{ responseContentTypeHeader :: ByteString
, responseBody :: ByteString
}
| ConnectionError
{ connectionError :: SomeException
}
deriving (Show, Typeable)
instance Exception ServantError
parseHeaders :: String -> ResponseHeaders
parseHeaders s =
fmap (first mk) $
fmap (first strip . second strip) $
fmap 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

View file

@ -0,0 +1,29 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
module Servant.Client.PerformRequest.GHC (
ServantError(..),
performHttpRequest,
) where
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative
#endif
import Control.Exception
import qualified Data.ByteString.Lazy as LBS
import Network.HTTP.Client
import Servant.Client.PerformRequest.Base
performHttpRequest :: Manager -> Request
-> IO (Either ServantError (Response LBS.ByteString))
performHttpRequest manager request =
catchConnectionError $ httpLbs request manager
catchConnectionError :: IO a -> IO (Either ServantError a)
catchConnectionError action =
catch (Right <$> action) $ \e ->
pure . Left . ConnectionError $ SomeException (e :: HttpException)

View file

@ -0,0 +1,151 @@
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
module Servant.Client.PerformRequest.GHCJS (
ServantError(..),
performHttpRequest,
) where
import Control.Concurrent
import Control.Exception
import Control.Monad
import qualified Data.ByteString.Lazy as LBS
import Data.CaseInsensitive
import Data.String.Conversions
import GHCJS.Foreign.Callback
import GHCJS.Prim
import Network.HTTP.Client
import Network.HTTP.Client.Internal as HttpClient
import Network.HTTP.Types
import Servant.Client.PerformRequest.Base
newtype JSXMLHttpRequest = JSXMLHttpRequest JSVal
newtype JSXMLHttpRequestClass = JSXMLHttpRequestClass JSVal
performHttpRequest :: Manager -> Request -> IO (Either ServantError (Response LBS.ByteString))
performHttpRequest _ request = do
xhr <- initXhr
performXhr xhr request
toResponse xhr
-- * initialization
initXhr :: IO JSXMLHttpRequest
initXhr = do
lib <- requireXMLHttpRequestClass
newXMLHttpRequest lib
foreign import javascript unsafe
-- branching between node (for testing) and browsers
"(function () {if (typeof require !== 'undefined') { return require('xhr2'); } else { return XMLHttpRequest; };})()"
requireXMLHttpRequestClass :: IO JSXMLHttpRequestClass
foreign import javascript unsafe "new $1()"
newXMLHttpRequest :: JSXMLHttpRequestClass -> IO JSXMLHttpRequest
-- * performing requests
-- Performs the xhr and blocks until the response was received
performXhr :: JSXMLHttpRequest -> Request -> IO ()
performXhr xhr request = do
waiter <- newEmptyMVar
callback <- onReadyStateChange xhr $ do
state <- readyState xhr
case state of
4 -> putMVar waiter ()
_ -> return ()
openXhr xhr (cs $ method request) (toUrl request) True
setHeaders xhr (requestHeaders request)
sendXhr xhr (toBody request)
takeMVar waiter
releaseCallback callback
onReadyStateChange :: JSXMLHttpRequest -> IO () -> IO (Callback (IO ()))
onReadyStateChange xhr action = do
callback <- asyncCallback action
js_onReadyStateChange xhr callback
return callback
foreign import javascript safe "$1.onreadystatechange = $2;"
js_onReadyStateChange :: JSXMLHttpRequest -> Callback (IO ()) -> IO ()
foreign import javascript unsafe "$1.readyState"
readyState :: JSXMLHttpRequest -> IO Int
openXhr :: JSXMLHttpRequest -> String -> String -> Bool -> IO ()
openXhr xhr method url async =
js_openXhr xhr (toJSString method) (toJSString url) async
foreign import javascript unsafe "$1.open($2, $3, $4)"
js_openXhr :: JSXMLHttpRequest -> JSVal -> JSVal -> Bool -> IO ()
toUrl :: Request -> String
toUrl request =
let protocol = if secure request then "https" else "http"
hostS = cs $ host request
portS = show $ port request
pathS = cs $ path request
queryS = cs $ queryString request
in protocol ++ "://" ++ hostS ++ ":" ++ portS ++ pathS ++ queryS
setHeaders :: JSXMLHttpRequest -> RequestHeaders -> IO ()
setHeaders xhr headers = forM_ headers $ \ (key, value) ->
js_setRequestHeader xhr (toJSString $ cs $ original key) (toJSString $ cs value)
foreign import javascript unsafe "$1.setRequestHeader($2, $3)"
js_setRequestHeader :: JSXMLHttpRequest -> JSVal -> JSVal -> IO ()
sendXhr :: JSXMLHttpRequest -> Maybe String -> IO ()
sendXhr xhr Nothing = js_sendXhr xhr
sendXhr xhr (Just body) =
js_sendXhrWithBody xhr (toJSString body)
foreign import javascript unsafe "$1.send()"
js_sendXhr :: JSXMLHttpRequest -> IO ()
foreign import javascript unsafe "$1.send($2)"
js_sendXhrWithBody :: JSXMLHttpRequest -> JSVal -> IO ()
toBody :: Request -> Maybe String
toBody request = case requestBody request of
RequestBodyLBS "" -> Nothing
RequestBodyLBS x -> Just $ cs x
_ -> error "servant-client only uses RequestBodyLBS"
-- * inspecting the xhr response
-- This function is only supposed to handle 'ConnectionError's. Other
-- 'ServantError's are created in Servant.Client.Req.
toResponse :: JSXMLHttpRequest -> IO (Either ServantError (Response LBS.ByteString))
toResponse xhr = do
status <- getStatus xhr
case status of
0 -> return $ Left $ ConnectionError $ SomeException $ ErrorCall "connection error"
_ -> do
statusText <- cs <$> getStatusText xhr
headers <- parseHeaders <$> getAllResponseHeaders xhr
responseText <- cs <$> getResponseText xhr
return $ Right $ Response {
HttpClient.responseStatus = mkStatus status statusText,
responseVersion = http11, -- this is made up
responseHeaders = headers,
HttpClient.responseBody = responseText,
responseCookieJar = mempty,
responseClose' = ResponseClose (return ())
}
foreign import javascript unsafe "$1.status"
getStatus :: JSXMLHttpRequest -> IO Int
getStatusText :: JSXMLHttpRequest -> IO String
getStatusText = fmap fromJSString . js_statusText
foreign import javascript unsafe "$1.statusText"
js_statusText :: JSXMLHttpRequest -> IO JSVal
getAllResponseHeaders :: JSXMLHttpRequest -> IO String
getAllResponseHeaders xhr =
fromJSString <$> js_getAllResponseHeaders xhr
foreign import javascript unsafe "$1.getAllResponseHeaders()"
js_getAllResponseHeaders :: JSXMLHttpRequest -> IO JSVal
getResponseText :: JSXMLHttpRequest -> IO String
getResponseText xhr = fromJSString <$> js_responseText xhr
foreign import javascript unsafe "$1.responseText"
js_responseText :: JSXMLHttpRequest -> IO JSVal

View file

@ -18,44 +18,17 @@ import Data.String.Conversions
import Data.Proxy import Data.Proxy
import Data.Text (Text) import Data.Text (Text)
import Data.Text.Encoding import Data.Text.Encoding
import Data.Typeable
import Network.HTTP.Client hiding (Proxy, path) import Network.HTTP.Client hiding (Proxy, path)
import Network.HTTP.Media import Network.HTTP.Media
import Network.HTTP.Types import Network.HTTP.Types
import qualified Network.HTTP.Types.Header as HTTP import qualified Network.HTTP.Types.Header as HTTP
import Network.URI hiding (path) import Network.URI hiding (path)
import Servant.API.ContentTypes import Servant.API.ContentTypes
import Servant.Client.PerformRequest
import Servant.Common.BaseUrl import Servant.Common.BaseUrl
import qualified Network.HTTP.Client as Client
import Web.HttpApiData import Web.HttpApiData
data ServantError import qualified Network.HTTP.Client as Client
= FailureResponse
{ responseStatus :: Status
, responseContentType :: MediaType
, responseBody :: ByteString
}
| DecodeFailure
{ decodeError :: String
, responseContentType :: MediaType
, responseBody :: ByteString
}
| UnsupportedContentType
{ responseContentType :: MediaType
, responseBody :: ByteString
}
| InvalidContentTypeHeader
{ responseContentTypeHeader :: ByteString
, responseBody :: ByteString
}
| ConnectionError
{ connectionError :: SomeException
}
deriving (Show, Typeable)
instance Exception ServantError
data Req = Req data Req = Req
{ reqPath :: String { reqPath :: String
@ -134,7 +107,7 @@ performRequest reqMethod req reqHost manager = do
, checkStatus = \ _status _headers _cookies -> Nothing , checkStatus = \ _status _headers _cookies -> Nothing
} }
eResponse <- liftIO $ catchConnectionError $ Client.httpLbs request manager eResponse <- liftIO $ performHttpRequest manager request
case eResponse of case eResponse of
Left err -> Left err ->
throwE . ConnectionError $ SomeException err throwE . ConnectionError $ SomeException err
@ -168,8 +141,3 @@ performRequestCT ct reqMethod req reqHost manager = do
performRequestNoBody :: Method -> Req -> BaseUrl -> Manager -> ExceptT ServantError IO () performRequestNoBody :: Method -> Req -> BaseUrl -> Manager -> ExceptT ServantError IO ()
performRequestNoBody reqMethod req reqHost manager = performRequestNoBody reqMethod req reqHost manager =
void $ performRequest reqMethod req reqHost manager void $ performRequest reqMethod req reqHost manager
catchConnectionError :: IO a -> IO (Either ServantError a)
catchConnectionError action =
catch (Right <$> action) $ \e ->
pure . Left . ConnectionError $ SomeException (e :: HttpException)

View file

@ -0,0 +1,23 @@
# This file was added for being able to test ghcjs support
packages:
- location: ./.
- location: ../
subdirs:
- servant
- servant-server
extra-dep: true
- location:
git: https://github.com/hspec/hspec
commit: bd06049
subdirs:
- ./
- hspec-core
extra-dep: true
resolver: lts-3.10
extra-deps:
- hspec-expectations-0.7.2
- hspec-discover-2.2.0
- http-api-data-0.1.1.1

View file

@ -0,0 +1,25 @@
# This file was added for being able to test ghcjs support
packages:
- location: ./.
- location: ../servant
extra-dep: true
- location: ../servant-server
extra-dep: true
resolver: lts-3.10
compiler: ghcjs-0.2.0.20151029_ghc-7.10.2
compiler-check: match-exact
setup-info:
ghcjs:
source:
ghcjs-0.2.0.20151029_ghc-7.10.2:
url: "https://github.com/nrolland/ghcjs/releases/download/v0.2.0.20151029/ghcjs-0.2.0.20151029.tar.gz"
extra-deps:
- hspec-expectations-0.7.2
- hspec-discover-2.2.1
- http-api-data-0.1.1.1
- hspec-2.2.1
- hspec-core-2.2.1

View file

@ -0,0 +1,20 @@
{-# LANGUAGE OverloadedStrings #-}
module Servant.Client.PerformRequest.BaseSpec where
import Test.Hspec
import Servant.Client.PerformRequest.Base
spec :: Spec
spec = do
describe "parseHeaders" $ do
it "parses single headers" $ do
parseHeaders "key: value" `shouldBe` [("key", "value")]
it "parses multiple headers" $ do
parseHeaders "foo: bar\r\nnext: yay" `shouldBe`
[("foo", "bar"), ("next", "yay")]
it "handles colons in header values correctly" $ do
parseHeaders "foo: bar:baz" `shouldBe` [("foo", "bar:baz")]

View file

@ -0,0 +1,12 @@
{-# LANGUAGE CPP #-}
module Servant.Client.TestServer (
buildTestServer,
withTestServer,
)where
#ifdef __GHCJS__
import Servant.Client.TestServer.GHCJS
#else
import Servant.Client.TestServer.GHC
#endif

View file

@ -0,0 +1,38 @@
{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
module Servant.Client.TestServer.GHC where
import Control.Concurrent
import Control.Exception
import Network.Socket
import Network.Wai
import Network.Wai.Handler.Warp
import Servant.Common.BaseUrl
buildTestServer :: IO ()
buildTestServer = return ()
withTestServer :: Application -> String -> (BaseUrl -> IO a) -> IO a
withTestServer app _ action =
bracket (startWaiApp app) endWaiApp $ \ (_, url) ->
action url
startWaiApp :: Application -> IO (ThreadId, BaseUrl)
startWaiApp app = do
(port, socket) <- openTestSocket
let settings = setPort port $ defaultSettings
thread <- forkIO $ runSettingsSocket settings socket app
return (thread, BaseUrl Http "localhost" port "")
endWaiApp :: (ThreadId, BaseUrl) -> IO ()
endWaiApp (thread, _) = killThread thread
openTestSocket :: IO (Port, Socket)
openTestSocket = do
s <- socket AF_INET Stream defaultProtocol
localhost <- inet_addr "127.0.0.1"
bind s (SockAddrInet aNY_PORT localhost)
listen s 1
port <- socketPort s
return (fromIntegral port, s)

View file

@ -0,0 +1,34 @@
module Servant.Client.TestServer.GHCJS where
import Control.Exception
import Network.Wai
import Safe
import System.Exit
import System.IO
import System.Process
import Servant.Common.BaseUrl
buildTestServer :: IO ()
buildTestServer = do
process <- spawnProcess "./test/ghcjs/build-test-server.sh" []
ExitSuccess <- waitForProcess process
return ()
withTestServer :: Application -> String -> (BaseUrl -> IO a) -> IO a
withTestServer _ testServerName action = do
bracket start stop $ \ (port, _) -> action (BaseUrl Http "localhost" port ("/" ++ testServerName))
where
start :: IO (Int, ProcessHandle)
start = do
(Nothing, Just stdout, Nothing, process) <- createProcess $ (proc "./test/ghcjs/testServer" []) {
std_out = CreatePipe
}
line <- hGetLine stdout
case readMay line :: Maybe Int of
Nothing -> error ("unparseable port: " ++ show line)
Just port -> return (port, process)
stop (_, process) = do
terminateProcess process
waitForProcess process

View file

@ -23,12 +23,10 @@
module Servant.ClientSpec where module Servant.ClientSpec where
#if !MIN_VERSION_base(4,8,0) #if !MIN_VERSION_base(4,8,0)
import Control.Applicative ((<$>)) import Control.Applicative ((<$>), pure)
#endif #endif
import Control.Arrow (left) import Control.Arrow (left)
import Control.Concurrent (forkIO, killThread, ThreadId) import Control.Monad.Trans.Except (runExceptT, throwE)
import Control.Exception (bracket)
import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE)
import Data.Aeson import Data.Aeson
import Data.Char (chr, isPrint) import Data.Char (chr, isPrint)
import Data.Foldable (forM_) import Data.Foldable (forM_)
@ -41,9 +39,7 @@ import qualified Network.HTTP.Client as C
import Network.HTTP.Media import Network.HTTP.Media
import Network.HTTP.Types (Status (..), badRequest400, import Network.HTTP.Types (Status (..), badRequest400,
methodGet, ok200, status400) methodGet, ok200, status400)
import Network.Socket
import Network.Wai (Application, responseLBS) import Network.Wai (Application, responseLBS)
import Network.Wai.Handler.Warp
import System.IO.Unsafe (unsafePerformIO) import System.IO.Unsafe (unsafePerformIO)
import Test.Hspec import Test.Hspec
import Test.Hspec.QuickCheck import Test.Hspec.QuickCheck
@ -52,13 +48,16 @@ import Test.QuickCheck
import Servant.API import Servant.API
import Servant.Client import Servant.Client
import Servant.Client.TestServer
import Servant.Server import Servant.Server
spec :: Spec spec :: Spec
spec = describe "Servant.Client" $ do spec = do
runIO buildTestServer
describe "Servant.Client" $ do
sucessSpec sucessSpec
failSpec failSpec
wrappedApiSpec errorSpec
-- * test data types -- * test data types
@ -105,7 +104,7 @@ type Api =
QueryParam "second" Int :> QueryParam "second" Int :>
QueryFlag "third" :> QueryFlag "third" :>
ReqBody '[JSON] [(String, [Rational])] :> ReqBody '[JSON] [(String, [Rational])] :>
Get '[JSON] (String, Maybe Int, Bool, [(String, [Rational])]) Post '[JSON] (String, Maybe Int, Bool, [(String, [Rational])])
:<|> "headers" :> Get '[JSON] (Headers TestHeaders Bool) :<|> "headers" :> Get '[JSON] (Headers TestHeaders Bool)
:<|> "deleteContentType" :> Delete '[JSON] () :<|> "deleteContentType" :> Delete '[JSON] ()
api :: Proxy Api api :: Proxy Api
@ -130,11 +129,11 @@ server = serve api (
:<|> return () :<|> return ()
) )
type FailApi = type FailApi =
"get" :> Raw "get" :> Raw
:<|> "capture" :> Capture "name" String :> Raw :<|> "capture" :> Capture "name" String :> Raw
:<|> "body" :> Raw :<|> "body" :> Raw
failApi :: Proxy FailApi failApi :: Proxy FailApi
failApi = Proxy failApi = Proxy
@ -142,7 +141,7 @@ failServer :: Application
failServer = serve failApi ( failServer = serve failApi (
(\ _request respond -> respond $ responseLBS ok200 [] "") (\ _request respond -> respond $ responseLBS ok200 [] "")
:<|> (\ _capture _request respond -> respond $ responseLBS ok200 [("content-type", "application/json")] "") :<|> (\ _capture _request respond -> respond $ responseLBS ok200 [("content-type", "application/json")] "")
:<|> (\_request respond -> respond $ responseLBS ok200 [("content-type", "fooooo")] "") :<|> (\ _request respond -> respond $ responseLBS ok200 [("content-type", "fooooo")] "")
) )
{-# NOINLINE manager #-} {-# NOINLINE manager #-}
@ -150,48 +149,48 @@ manager :: C.Manager
manager = unsafePerformIO $ C.newManager C.defaultManagerSettings manager = unsafePerformIO $ C.newManager C.defaultManagerSettings
sucessSpec :: Spec sucessSpec :: Spec
sucessSpec = beforeAll (startWaiApp server) $ afterAll endWaiApp $ do sucessSpec = around (withTestServer server "server") $ do
it "Servant.API.Get" $ \(_, baseUrl) -> do it "Servant.API.Get" $ \baseUrl -> do
let getGet = getNth (Proxy :: Proxy 0) $ client api baseUrl manager let getGet = getNth (Proxy :: Proxy 0) $ client api baseUrl manager
(left show <$> runExceptT getGet) `shouldReturn` Right alice (left show <$> runExceptT getGet) `shouldReturn` Right alice
describe "Servant.API.Delete" $ do describe "Servant.API.Delete" $ do
it "allows empty content type" $ \(_, baseUrl) -> do it "allows empty content type" $ \baseUrl -> do
let getDeleteEmpty = getNth (Proxy :: Proxy 1) $ client api baseUrl manager let getDeleteEmpty = getNth (Proxy :: Proxy 1) $ client api baseUrl manager
(left show <$> runExceptT getDeleteEmpty) `shouldReturn` Right () (left show <$> runExceptT getDeleteEmpty) `shouldReturn` Right ()
it "allows content type" $ \(_, baseUrl) -> do it "allows content type" $ \baseUrl -> do
let getDeleteContentType = getLast $ client api baseUrl manager let getDeleteContentType = getLast $ client api baseUrl manager
(left show <$> runExceptT getDeleteContentType) `shouldReturn` Right () (left show <$> runExceptT getDeleteContentType) `shouldReturn` Right ()
it "Servant.API.Capture" $ \(_, baseUrl) -> do it "Servant.API.Capture" $ \baseUrl -> do
let getCapture = getNth (Proxy :: Proxy 2) $ client api baseUrl manager let getCapture = getNth (Proxy :: Proxy 2) $ client api baseUrl manager
(left show <$> runExceptT (getCapture "Paula")) `shouldReturn` Right (Person "Paula" 0) (left show <$> runExceptT (getCapture "Paula")) `shouldReturn` Right (Person "Paula" 0)
it "Servant.API.ReqBody" $ \(_, baseUrl) -> do it "Servant.API.ReqBody" $ \baseUrl -> do
let p = Person "Clara" 42 let p = Person "Clara" 42
getBody = getNth (Proxy :: Proxy 3) $ client api baseUrl manager getBody = getNth (Proxy :: Proxy 3) $ client api baseUrl manager
(left show <$> runExceptT (getBody p)) `shouldReturn` Right p (left show <$> runExceptT (getBody p)) `shouldReturn` Right p
it "Servant.API.QueryParam" $ \(_, baseUrl) -> do it "Servant.API.QueryParam" $ \baseUrl -> do
let getQueryParam = getNth (Proxy :: Proxy 4) $ client api baseUrl manager let getQueryParam = getNth (Proxy :: Proxy 4) $ client api baseUrl manager
left show <$> runExceptT (getQueryParam (Just "alice")) `shouldReturn` Right alice left show <$> runExceptT (getQueryParam (Just "alice")) `shouldReturn` Right alice
Left FailureResponse{..} <- runExceptT (getQueryParam (Just "bob")) Left FailureResponse{..} <- runExceptT (getQueryParam (Just "bob"))
responseStatus `shouldBe` Status 400 "bob not found" responseStatus `shouldBe` Status 400 "bob not found"
it "Servant.API.QueryParam.QueryParams" $ \(_, baseUrl) -> do it "Servant.API.QueryParam.QueryParams" $ \baseUrl -> do
let getQueryParams = getNth (Proxy :: Proxy 5) $ client api baseUrl manager let getQueryParams = getNth (Proxy :: Proxy 5) $ client api baseUrl manager
(left show <$> runExceptT (getQueryParams [])) `shouldReturn` Right [] (left show <$> runExceptT (getQueryParams [])) `shouldReturn` Right []
(left show <$> runExceptT (getQueryParams ["alice", "bob"])) (left show <$> runExceptT (getQueryParams ["alice", "bob"]))
`shouldReturn` Right [Person "alice" 0, Person "bob" 1] `shouldReturn` Right [Person "alice" 0, Person "bob" 1]
context "Servant.API.QueryParam.QueryFlag" $ context "Servant.API.QueryParam.QueryFlag" $
forM_ [False, True] $ \ flag -> it (show flag) $ \(_, baseUrl) -> do forM_ [False, True] $ \ flag -> it (show flag) $ \baseUrl -> do
let getQueryFlag = getNth (Proxy :: Proxy 6) $ client api baseUrl manager let getQueryFlag = getNth (Proxy :: Proxy 6) $ client api baseUrl manager
(left show <$> runExceptT (getQueryFlag flag)) `shouldReturn` Right flag (left show <$> runExceptT (getQueryFlag flag)) `shouldReturn` Right flag
it "Servant.API.Raw on success" $ \(_, baseUrl) -> do it "Servant.API.Raw on success" $ \baseUrl -> do
let getRawSuccess = getNth (Proxy :: Proxy 7) $ client api baseUrl manager let getRawSuccess = getNth (Proxy :: Proxy 7) $ client api baseUrl manager
res <- runExceptT (getRawSuccess methodGet) res <- runExceptT (getRawSuccess methodGet)
case res of case res of
@ -201,7 +200,7 @@ sucessSpec = beforeAll (startWaiApp server) $ afterAll endWaiApp $ do
C.responseBody response `shouldBe` body C.responseBody response `shouldBe` body
C.responseStatus response `shouldBe` ok200 C.responseStatus response `shouldBe` ok200
it "Servant.API.Raw should return a Left in case of failure" $ \(_, baseUrl) -> do it "Servant.API.Raw should return a Left in case of failure" $ \baseUrl -> do
let getRawFailure = getNth (Proxy :: Proxy 8) $ client api baseUrl manager let getRawFailure = getNth (Proxy :: Proxy 8) $ client api baseUrl manager
res <- runExceptT (getRawFailure methodGet) res <- runExceptT (getRawFailure methodGet)
case res of case res of
@ -210,15 +209,15 @@ sucessSpec = beforeAll (startWaiApp server) $ afterAll endWaiApp $ do
Servant.Client.responseStatus e `shouldBe` status400 Servant.Client.responseStatus e `shouldBe` status400
Servant.Client.responseBody e `shouldBe` "rawFailure" Servant.Client.responseBody e `shouldBe` "rawFailure"
it "Returns headers appropriately" $ \(_, baseUrl) -> do it "Returns headers appropriately" $ \baseUrl -> do
let getRespHeaders = getNth (Proxy :: Proxy 10) $ client api baseUrl manager let getRespHeaders = getNth (Proxy :: Proxy 10) $ client api baseUrl manager
res <- runExceptT getRespHeaders res <- runExceptT getRespHeaders
case res of case res of
Left e -> assertFailure $ show e Left e -> assertFailure $ show e
Right val -> getHeaders val `shouldBe` [("X-Example1", "1729"), ("X-Example2", "eg2")] Right val -> getHeaders val `shouldBe` [("X-Example1", "1729"), ("X-Example2", "eg2")]
modifyMaxSuccess (const 20) $ do modifyMaxSuccess (const 2) $ do
it "works for a combination of Capture, QueryParam, QueryFlag and ReqBody" $ \(_, baseUrl) -> it "works for a combination of Capture, QueryParam, QueryFlag and ReqBody" $ \baseUrl ->
let getMultiple = getNth (Proxy :: Proxy 9) $ client api baseUrl manager let getMultiple = getNth (Proxy :: Proxy 9) $ client api baseUrl manager
in property $ forAllShrink pathGen shrink $ \(NonEmpty cap) num flag body -> in property $ forAllShrink pathGen shrink $ \(NonEmpty cap) num flag body ->
ioProperty $ do ioProperty $ do
@ -226,37 +225,45 @@ sucessSpec = beforeAll (startWaiApp server) $ afterAll endWaiApp $ do
return $ return $
result === Right (cap, num, flag, body) result === Right (cap, num, flag, body)
type ErrorApi =
Delete '[JSON] () :<|>
Get '[JSON] () :<|>
Post '[JSON] () :<|>
Put '[JSON] ()
wrappedApiSpec :: Spec errorApi :: Proxy ErrorApi
wrappedApiSpec = describe "error status codes" $ do errorApi = Proxy
let serveW api = serve api $ throwE $ ServantErr 500 "error message" "" []
context "are correctly handled by the client" $ errorServer :: Application
let test :: (WrappedApi, String) -> Spec errorServer = serve errorApi $
test (WrappedApi api, desc) = err :<|> err :<|> err :<|> err
it desc $ bracket (startWaiApp $ serveW api) endWaiApp $ \(_, baseUrl) -> do where
let getResponse :: ExceptT ServantError IO () err = throwE $ ServantErr 500 "error message" "" []
getResponse = client api baseUrl manager
Left FailureResponse{..} <- runExceptT getResponse errorSpec :: Spec
responseStatus `shouldBe` (Status 500 "error message") errorSpec =
in mapM_ test $ around (withTestServer errorServer "errorServer") $ do
(WrappedApi (Proxy :: Proxy (Delete '[JSON] ())), "Delete") : describe "error status codes" $
(WrappedApi (Proxy :: Proxy (Get '[JSON] ())), "Get") : it "reports error statuses correctly" $ \baseUrl -> do
(WrappedApi (Proxy :: Proxy (Post '[JSON] ())), "Post") : let delete :<|> get :<|> post :<|> put =
(WrappedApi (Proxy :: Proxy (Put '[JSON] ())), "Put") : client errorApi baseUrl manager
[] actions = [delete, get, post, put]
forM_ actions $ \ clientAction -> do
Left FailureResponse{..} <- runExceptT clientAction
responseStatus `shouldBe` Status 500 "error message"
failSpec :: Spec failSpec :: Spec
failSpec = beforeAll (startWaiApp failServer) $ afterAll endWaiApp $ do failSpec = around (withTestServer failServer "failServer") $ do
context "client returns errors appropriately" $ do context "client returns errors appropriately" $ do
it "reports FailureResponse" $ \(_, baseUrl) -> do it "reports FailureResponse" $ \baseUrl -> do
let (_ :<|> getDeleteEmpty :<|> _) = client api baseUrl manager let (_ :<|> getDeleteEmpty :<|> _) = client api baseUrl manager
Left res <- runExceptT getDeleteEmpty Left res <- runExceptT getDeleteEmpty
case res of case res of
FailureResponse (Status 404 "Not Found") _ _ -> return () FailureResponse (Status 404 "Not Found") _ _ -> return ()
_ -> fail $ "expected 404 response, but got " <> show res _ -> fail $ "expected 404 response, but got " <> show res
it "reports DecodeFailure" $ \(_, baseUrl) -> do it "reports DecodeFailure" $ \baseUrl -> do
let (_ :<|> _ :<|> getCapture :<|> _) = client api baseUrl manager let (_ :<|> _ :<|> getCapture :<|> _) = client api baseUrl manager
Left res <- runExceptT (getCapture "foo") Left res <- runExceptT (getCapture "foo")
case res of case res of
@ -270,48 +277,23 @@ failSpec = beforeAll (startWaiApp failServer) $ afterAll endWaiApp $ do
ConnectionError _ -> return () ConnectionError _ -> return ()
_ -> fail $ "expected ConnectionError, but got " <> show res _ -> fail $ "expected ConnectionError, but got " <> show res
it "reports UnsupportedContentType" $ \(_, baseUrl) -> do it "reports UnsupportedContentType" $ \baseUrl -> do
let (getGet :<|> _ ) = client api baseUrl manager let (getGet :<|> _ ) = client api baseUrl manager
Left res <- runExceptT getGet Left res <- runExceptT getGet
case res of case res of
UnsupportedContentType ("application/octet-stream") _ -> return () UnsupportedContentType ("application/octet-stream") _ -> return ()
_ -> fail $ "expected UnsupportedContentType, but got " <> show res _ -> fail $ "expected UnsupportedContentType, but got " <> show res
it "reports InvalidContentTypeHeader" $ \(_, baseUrl) -> do it "reports InvalidContentTypeHeader" $ \baseUrl -> do
let (_ :<|> _ :<|> _ :<|> getBody :<|> _) = client api baseUrl manager let (_ :<|> _ :<|> _ :<|> getBody :<|> _) = client api baseUrl manager
Left res <- runExceptT (getBody alice) Left res <- runExceptT (getBody alice)
case res of case res of
InvalidContentTypeHeader "fooooo" _ -> return () InvalidContentTypeHeader "fooooo" _ -> return ()
_ -> fail $ "expected InvalidContentTypeHeader, but got " <> show res _ -> fail $ "expected InvalidContentTypeHeader, but got " <> show res
data WrappedApi where
WrappedApi :: (HasServer api, Server api ~ ExceptT ServantErr IO a,
HasClient api, Client api ~ ExceptT ServantError IO ()) =>
Proxy api -> WrappedApi
-- * utils -- * utils
startWaiApp :: Application -> IO (ThreadId, BaseUrl)
startWaiApp app = do
(port, socket) <- openTestSocket
let settings = setPort port $ defaultSettings
thread <- forkIO $ runSettingsSocket settings socket app
return (thread, BaseUrl Http "localhost" port "")
endWaiApp :: (ThreadId, BaseUrl) -> IO ()
endWaiApp (thread, _) = killThread thread
openTestSocket :: IO (Port, Socket)
openTestSocket = do
s <- socket AF_INET Stream defaultProtocol
localhost <- inet_addr "127.0.0.1"
bind s (SockAddrInet aNY_PORT localhost)
listen s 1
port <- socketPort s
return (fromIntegral port, s)
pathGen :: Gen (NonEmptyList Char) pathGen :: Gen (NonEmptyList Char)
pathGen = fmap NonEmpty path pathGen = fmap NonEmpty path
where where

View file

@ -0,0 +1,10 @@
#/usr/bin/env bash
set -o errexit
cd test/ghcjs
export STACK_YAML=stack-ghc.yaml
stack setup
stack build
cp $(stack exec which testServer) .

View file

@ -0,0 +1,25 @@
name: testServer
executables:
testServer:
main: testServer.hs
dependencies:
- base
- servant
- servant-client
- servant-server
- warp
- QuickCheck
- hspec
- HUnit
- http-types
- http-media
- http-client
- text
- aeson
- wai
- transformers
- network
source-dirs:
- ./
- ../

View file

@ -0,0 +1,11 @@
#/usr/bin/env bash
# this script has to be executed from the 'servant-client' directory
set -o errexit
npm install xhr2
export STACK_YAML=stack-ghcjs.yaml
stack setup
stack test

View file

@ -0,0 +1,15 @@
# This file was added for being able to test ghcjs support
packages:
- location: ./.
- location: ../../../
subdirs:
- servant
- servant-client
- servant-server
extra-dep: true
resolver: nightly-2015-10-08
extra-deps:
- http-api-data-0.1.1.1

View file

@ -0,0 +1,40 @@
-- This file has been generated from package.yaml by hpack version 0.5.4.
--
-- see: https://github.com/sol/hpack
name: testServer
version: 0.0.0
build-type: Simple
cabal-version: >= 1.10
executable testServer
main-is: testServer.hs
hs-source-dirs:
./
, ../
build-depends:
base
, servant
, servant-client
, servant-server
, warp
, QuickCheck
, hspec
, HUnit
, http-types
, http-media
, http-client
, text
, aeson
, wai
, transformers
, network
other-modules:
Servant.Client.PerformRequest.BaseSpec
Servant.Client.TestServer
Servant.Client.TestServer.GHC
Servant.Client.TestServer.GHCJS
Servant.ClientSpec
Servant.Common.BaseUrlSpec
Spec
default-language: Haskell2010

View file

@ -0,0 +1,27 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
import Network.Wai.Handler.Warp
import Servant
import System.IO
import Servant.Client.TestServer.GHC
import Servant.ClientSpec
main :: IO ()
main = do
(port, socket) <- openTestSocket
let settings =
setPort port $
setBeforeMainLoop (print port >> hFlush stdout) $
defaultSettings
runSettingsSocket settings socket $
serve testServerApi (server :<|> errorServer :<|> failServer)
type TestServerApi =
"server" :> Raw :<|>
"errorServer" :> Raw :<|>
"failServer" :> Raw
testServerApi :: Proxy TestServerApi
testServerApi = Proxy