@@ -3,6 +3,7 @@
3{-# LANGUAGE ViewPatterns #-}
3{-# LANGUAGE ViewPatterns #-}
4{-# LANGUAGE DeriveGeneric #-}
4{-# LANGUAGE DeriveGeneric #-}
5{-# LANGUAGE TypeOperators #-}
5{-# LANGUAGE TypeOperators #-}
6+{-# LANGUAGE RecordWildCards #-}
6{-# LANGUAGE OverloadedStrings #-}
7{-# LANGUAGE OverloadedStrings #-}
7{-# OPTIONS_GHC -fcontext-stack=25 #-}
8{-# OPTIONS_GHC -fcontext-stack=25 #-}
8module Servant.ClientSpec where
9module Servant.ClientSpec where
@@ -14,9 +15,11 @@ import Data.Aeson
14import Data.ByteString.Lazy (ByteString)
15import Data.ByteString.Lazy (ByteString)
15import Data.Char
16import Data.Char
16import Data.Foldable (forM_)
17import Data.Foldable (forM_)
17import Data.Proxy
19import Data.Proxy
18−import Data.Typeable
20+import qualified Data.Text as T
19import GHC.Generics
21import GHC.Generics
22+import Network.HTTP.Media
20import Network.HTTP.Types
23import Network.HTTP.Types
21import Network.Socket
24import Network.Socket
22import Network.Wai
25import Network.Wai
@@ -26,6 +29,7 @@ import Test.Hspec.QuickCheck
26import Test.QuickCheck
29import Test.QuickCheck
28import Servant.API
31import Servant.API
32+import Servant.API.ContentTypes
29import Servant.Client
33import Servant.Client
30import Servant.Server
34import Servant.Server
@@ -40,28 +44,43 @@ data Person = Person {
40instance ToJSON Person
44instance ToJSON Person
41instance FromJSON Person
45instance FromJSON Person
47+instance ToFormUrlEncoded Person where
48+ toFormUrlEncoded Person{..} =
49+ [("name", T.pack name), ("age", T.pack (show age))]
51+lookupEither :: (Show a, Eq a) => a -> [(a,b)] -> Either String b
52+lookupEither x xs = do
53+ maybe (Left $ "could not find key " <> show x) return $ lookup x xs
55+instance FromFormUrlEncoded Person where
56+ fromFormUrlEncoded xs = do
57+ n <- lookupEither "name" xs
58+ a <- lookupEither "age" xs
59+ return $ Person (T.unpack n) (read $ T.unpack a)
43alice :: Person
62alice :: Person
44alice = Person "Alice" 42
63alice = Person "Alice" 42
47− "get" :> Get Person
66+ "get" :> Get '[JSON] Person
48 :<|> "delete" :> Delete
67 :<|> "delete" :> Delete
49− :<|> "capture" :> Capture "name" String :> Get Person
68+ :<|> "capture" :> Capture "name" String :> Get '[JSON,FormUrlEncoded] Person
50− :<|> "body" :> ReqBody Person :> Post Person
69+ :<|> "body" :> ReqBody '[FormUrlEncoded,JSON] Person :> Post '[JSON] Person
51− :<|> "param" :> QueryParam "name" String :> Get Person
70+ :<|> "param" :> QueryParam "name" String :> Get '[FormUrlEncoded,JSON] Person
52− :<|> "params" :> QueryParams "names" String :> Get [Person]
71+ :<|> "params" :> QueryParams "names" String :> Get '[JSON] [Person]
53− :<|> "flag" :> QueryFlag "flag" :> Get Bool
72+ :<|> "flag" :> QueryFlag "flag" :> Get '[JSON] Bool
54− :<|> "matrixparam" :> MatrixParam "name" String :> Get Person
73+ :<|> "matrixparam" :> MatrixParam "name" String :> Get '[JSON] Person
55− :<|> "matrixparams" :> MatrixParams "name" String :> Get [Person]
74+ :<|> "matrixparams" :> MatrixParams "name" String :> Get '[JSON] [Person]
56− :<|> "matrixflag" :> MatrixFlag "flag" :> Get Bool
75+ :<|> "matrixflag" :> MatrixFlag "flag" :> Get '[JSON] Bool
57 :<|> "rawSuccess" :> Raw
76 :<|> "rawSuccess" :> Raw
58 :<|> "rawFailure" :> Raw
77 :<|> "rawFailure" :> Raw
59 :<|> "multiple" :>
78 :<|> "multiple" :>
60 Capture "first" String :>
79 Capture "first" String :>
61 QueryParam "second" Int :>
80 QueryParam "second" Int :>
62 QueryFlag "third" :>
81 QueryFlag "third" :>
63− ReqBody [(String, [Rational])] :>
82+ ReqBody '[JSON] [(String, [Rational])] :>
64− Get (String, Maybe Int, Bool, [(String, [Rational])])
83+ Get '[JSON] (String, Maybe Int, Bool, [(String, [Rational])])
65api :: Proxy Api
84api :: Proxy Api
66api = Proxy
85api = Proxy
@@ -101,8 +120,8 @@ getQueryFlag :: Bool -> BaseUrl -> EitherT String IO Bool
101getMatrixParam :: Maybe String -> BaseUrl -> EitherT String IO Person
120getMatrixParam :: Maybe String -> BaseUrl -> EitherT String IO Person
102getMatrixParams :: [String] -> BaseUrl -> EitherT String IO [Person]
121getMatrixParams :: [String] -> BaseUrl -> EitherT String IO [Person]
103getMatrixFlag :: Bool -> BaseUrl -> EitherT String IO Bool
122getMatrixFlag :: Bool -> BaseUrl -> EitherT String IO Bool
104−getRawSuccess :: Method -> BaseUrl -> EitherT String IO (Int, ByteString)
123+getRawSuccess :: Method -> BaseUrl -> EitherT String IO (Int, ByteString, MediaType)
105−getRawFailure :: Method -> BaseUrl -> EitherT String IO (Int, ByteString)
124+getRawFailure :: Method -> BaseUrl -> EitherT String IO (Int, ByteString, MediaType)
106getMultiple :: String -> Maybe Int -> Bool -> [(String, [Rational])]
125getMultiple :: String -> Maybe Int -> Bool -> [(String, [Rational])]
107 -> BaseUrl
126 -> BaseUrl
108 -> EitherT String IO (String, Maybe Int, Bool, [(String, [Rational])])
127 -> EitherT String IO (String, Maybe Int, Bool, [(String, [Rational])])
@@ -151,6 +170,7 @@ spec = do
151 it (show flag) $ withServer $ \ host -> do
170 it (show flag) $ withServer $ \ host -> do
152 runEitherT (getQueryFlag flag host) `shouldReturn` Right flag
171 runEitherT (getQueryFlag flag host) `shouldReturn` Right flag
154 it "Servant.API.MatrixParam" $ withServer $ \ host -> do
174 it "Servant.API.MatrixParam" $ withServer $ \ host -> do
155 runEitherT (getMatrixParam (Just "alice") host) `shouldReturn` Right alice
175 runEitherT (getMatrixParam (Just "alice") host) `shouldReturn` Right alice
156 Left result <- runEitherT (getMatrixParam (Just "bob") host)
176 Left result <- runEitherT (getMatrixParam (Just "bob") host)
@@ -165,12 +185,13 @@ spec = do
165 forM_ [False, True] $ \ flag ->
185 forM_ [False, True] $ \ flag ->
166 it (show flag) $ withServer $ \ host -> do
186 it (show flag) $ withServer $ \ host -> do
167 runEitherT (getMatrixFlag flag host) `shouldReturn` Right flag
187 runEitherT (getMatrixFlag flag host) `shouldReturn` Right flag
169 it "Servant.API.Raw on success" $ withServer $ \ host -> do
190 it "Servant.API.Raw on success" $ withServer $ \ host -> do
170− runEitherT (getRawSuccess methodGet host) `shouldReturn` Right (200, "rawSuccess")
191+ runEitherT (getRawSuccess methodGet host) `shouldReturn` Right (200, "rawSuccess", "application"//"octet-stream")
172 it "Servant.API.Raw on failure" $ withServer $ \ host -> do
193 it "Servant.API.Raw on failure" $ withServer $ \ host -> do
173− runEitherT (getRawFailure methodGet host) `shouldReturn` Right (400, "rawFailure")
194+ runEitherT (getRawFailure methodGet host) `shouldReturn` Right (400, "rawFailure", "application"//"octet-stream")
175 modifyMaxSuccess (const 20) $ do
196 modifyMaxSuccess (const 20) $ do
176 it "works for a combination of Capture, QueryParam, QueryFlag and ReqBody" $
197 it "works for a combination of Capture, QueryParam, QueryFlag and ReqBody" $
@@ -183,9 +204,9 @@ spec = do
185 context "client correctly handles error status codes" $ do
206 context "client correctly handles error status codes" $ do
186− let test :: WrappedApi -> Spec
207+ let test :: (WrappedApi, String) -> Spec
187− test (WrappedApi api) =
208+ test (WrappedApi api, desc) =
188− it (show (typeOf api)) $
209+ it desc $
189 withWaiDaemon (return (serve api (left (500, "error message")))) $
210 withWaiDaemon (return (serve api (left (500, "error message")))) $
190 \ host -> do
211 \ host -> do
191 let getResponse :: BaseUrl -> EitherT String IO ()
212 let getResponse :: BaseUrl -> EitherT String IO ()
@@ -193,16 +214,15 @@ spec = do
193 Left result <- runEitherT (getResponse host)
214 Left result <- runEitherT (getResponse host)
194 result `shouldContain` "error message"
215 result `shouldContain` "error message"
195 mapM_ test $
216 mapM_ test $
196− (WrappedApi (Proxy :: Proxy Delete)) :
217+ (WrappedApi (Proxy :: Proxy Delete), "Delete") :
197− (WrappedApi (Proxy :: Proxy (Get ()))) :
218+ (WrappedApi (Proxy :: Proxy (Get '[JSON] ())), "Delete") :
198− (WrappedApi (Proxy :: Proxy (Post ()))) :
219+ (WrappedApi (Proxy :: Proxy (Post '[JSON] ())), "Delete") :
199− (WrappedApi (Proxy :: Proxy (Put ()))) :
220+ (WrappedApi (Proxy :: Proxy (Put '[JSON] ())), "Delete") :
202data WrappedApi where
223data WrappedApi where
203 WrappedApi :: (HasServer api, Server api ~ EitherT (Int, String) IO a,
224 WrappedApi :: (HasServer api, Server api ~ EitherT (Int, String) IO a,
204− HasClient api, Client api ~ (BaseUrl -> EitherT String IO ()),
225+ HasClient api, Client api ~ (BaseUrl -> EitherT String IO ())) =>
206 Proxy api -> WrappedApi
226 Proxy api -> WrappedApi