git.iko.soy/typeable/servant
on master help sign in
Merge remote-tracking branch 'servant-client/prepare-merge' into merge
commit1eedad4073da3f1836d24d521a6c85fddfcb839e authorJulian K. Arni <jkarni@gmail.com> dateMon 20 Apr 2015, 11:23 (11 years ago) parent29a54fe45f3585
12 files changed+1365
servant-client/CHANGELOG.md+15 servant-client/LICENSE+30 servant-client/README.md+23 servant-client/Setup.hs+2 servant-client/docs.sh+52 servant-client/servant-client.cabal+85 servant-client/src/Servant/Client.hs+504 servant-client/src/Servant/Common/BaseUrl.hs+55 servant-client/src/Servant/Common/Req.hs+184 servant-client/test/Servant/ClientSpec.hs+345 servant-client/test/Servant/Common/BaseUrlSpec.hs+69 servant-client/test/Spec.hs+1
servant-client/CHANGELOG.mdnew file+15
@@ -0,0 +1,15 @@
1+0.3
2+---
3+* Support content-type aware combinators and `Accept`/`Content-type` headers
4+* Added a lot of tests
5+* Support multiple concurrent threads
6+* Use `ServantError` to report Errors instead of `String`
7+* Make the clients for `Raw` endpoints return the whole `Response` value (to be able to access response headers for example)
8+* Support for PATCH
9+* Make () instances expect No Content status code, and not try to decode body.
10+* `Canonicalize` API types before generating client functions for them
11+
12+0.2.2
13+-----
14+* Add TLS support
15+* Add matrix parameter support
servant-client/LICENSEnew file+30
@@ -0,0 +1,30 @@
1+Copyright (c) 2014, Zalora South East Asia Pte Ltd
2+
3+All rights reserved.
4+
5+Redistribution and use in source and binary forms, with or without
6+modification, are permitted provided that the following conditions are met:
7+
8+ * Redistributions of source code must retain the above copyright
9+ notice, this list of conditions and the following disclaimer.
10+
11+ * Redistributions in binary form must reproduce the above
12+ copyright notice, this list of conditions and the following
13+ disclaimer in the documentation and/or other materials provided
14+ with the distribution.
15+
16+ * Neither the name of Zalora South East Asia Pte Ltd nor the names of other
17+ contributors may be used to endorse or promote products derived
18+ from this software without specific prior written permission.
19+
20+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
servant-client/README.mdnew file+23
@@ -0,0 +1,23 @@
1+# servant-client
2+
3+[![Build Status](https://secure.travis-ci.org/haskell-servant/servant-client.svg)](http://travis-ci.org/haskell-servant/servant-client)
4+[![Coverage Status](https://coveralls.io/repos/haskell-servant/servant-client/badge.svg)](https://coveralls.io/r/haskell-servant/servant-client)
5+
6+![servant](https://raw.githubusercontent.com/haskell-servant/servant/master/servant.png)
7+
8+This library lets you automatically derive Haskell functions that let you query each endpoint of a *servant* webservice.
9+
10+## Example
11+
12+``` haskell
13+type MyApi = "books" :> Get [Book] -- GET /books
14+ :<|> "books" :> ReqBody Book :> Post Book -- POST /books
15+
16+myApi :: Proxy MyApi
17+myApi = Proxy
18+
19+getAllBooks :: BaseUrl -> EitherT String IO [Book]
20+postNewBook :: Book -> BaseUrl -> EitherT String IO Book
21+-- 'client' allows you to produce operations to query an API from a client.
22+(getAllBooks :<|> postNewBook) = client myApi
23+```
servant-client/Setup.hsnew file+2
@@ -0,0 +1,2 @@
1+import Distribution.Simple
2+main = defaultMain
servant-client/docs.shnew file+52
@@ -0,0 +1,52 @@
1+SERVANT_DIR=/tmp/servant-client-gh-pages
2+
3+# Make a temporary clone
4+
5+rm -rf $SERVANT_DIR
6+
7+git clone . $SERVANT_DIR
8+
9+cd $SERVANT_DIR
10+
11+# Make sure to pull the latest
12+
13+git remote add haskell-servant git@github.com:haskell-servant/servant-client.git
14+
15+git fetch haskell-servant
16+
17+git reset --hard haskell-servant/gh-pages
18+
19+# Clear everything away
20+
21+git rm -rf $SERVANT_DIR/*
22+
23+# Switch back and build the haddocks
24+
25+cd -
26+
27+cabal configure --builddir=$SERVANT_DIR
28+
29+cabal haddock --hoogle --hyperlink-source --html-location='https://hackage.haskell.org/package/$pkg-$version/docs' --builddir=$SERVANT_DIR
30+
31+commit_hash=$(git rev-parse HEAD)
32+
33+# Move the HTML docs to the root
34+
35+cd $SERVANT_DIR
36+
37+rm *
38+rm -rf build
39+mv doc/html/servant-client/* .
40+rm -r doc/
41+
42+# Add everything
43+
44+git add .
45+
46+git commit -m "Built from $commit_hash"
47+
48+# Push to update the pages
49+
50+git push haskell-servant HEAD:gh-pages
51+
52+rm -rf $SERVANT_DIR
servant-client/servant-client.cabalnew file+85
@@ -0,0 +1,85 @@
1+name: servant-client
2+version: 0.2.2
3+synopsis: automatical derivation of querying functions for servant webservices
4+description:
5+ This library lets you derive automatically Haskell functions that
6+ let you query each endpoint of a <http://hackage.haskell.org/package/servant servant> webservice.
7+ .
8+ Example below.
9+ .
10+ > type MyApi = "books" :> Get [Book] -- GET /books
11+ > :<|> "books" :> ReqBody Book :> Post Book -- POST /books
12+ >
13+ > myApi :: Proxy MyApi
14+ > myApi = Proxy
15+ >
16+ > getAllBooks :: BaseUrl -> EitherT String IO [Book]
17+ > postNewBook :: Book -> BaseUrl -> EitherT String IO Book
18+ > (getAllBooks :<|> postNewBook) = client myApi
19+license: BSD3
20+license-file: LICENSE
21+author: Alp Mestanogullari, Sönke Hahn, Julian K. Arni
22+maintainer: alpmestan@gmail.com
23+copyright: 2014 Zalora South East Asia Pte Ltd
24+category: Web
25+build-type: Simple
26+cabal-version: >=1.10
27+tested-with: GHC >= 7.8
28+homepage: http://haskell-servant.github.io/
29+Bug-reports: http://github.com/haskell-servant/servant-client/issues
30+source-repository head
31+ type: git
32+ location: http://github.com/haskell-servant/servant-client.git
33+
34+library
35+ exposed-modules:
36+ Servant.Client
37+ Servant.Common.BaseUrl
38+ Servant.Common.Req
39+ build-depends:
40+ base >=4.7 && <5
41+ , aeson
42+ , attoparsec
43+ , bytestring
44+ , either
45+ , exceptions
46+ , http-client
47+ , http-client-tls
48+ , http-media
49+ , http-types
50+ , network-uri >= 2.6
51+ , safe
52+ , servant >= 0.2.2
53+ , string-conversions
54+ , text
55+ , transformers
56+ hs-source-dirs: src
57+ default-language: Haskell2010
58+ ghc-options: -Wall
59+
60+test-suite spec
61+ type: exitcode-stdio-1.0
62+ ghc-options:
63+ -Wall -fno-warn-name-shadowing -fno-warn-missing-signatures
64+ default-language: Haskell2010
65+ hs-source-dirs: test
66+ main-is: Spec.hs
67+ build-depends:
68+ base == 4.*
69+ , aeson
70+ , bytestring
71+ , deepseq
72+ , either
73+ , hspec == 2.*
74+ , http-client
75+ , http-media
76+ , http-types
77+ , HUnit
78+ , network >= 2.6
79+ , QuickCheck >= 2.7
80+ , servant >= 0.2.1
81+ , servant-client
82+ , servant-server >= 0.2.1
83+ , text
84+ , wai
85+ , warp
servant-client/src/Servant/Client.hsnew file+504
@@ -0,0 +1,504 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE InstanceSigs #-}
3+{-# LANGUAGE TypeFamilies #-}
4+{-# LANGUAGE TypeOperators #-}
5+{-# LANGUAGE FlexibleContexts #-}
6+{-# LANGUAGE FlexibleInstances #-}
7+{-# LANGUAGE OverloadedStrings #-}
8+{-# LANGUAGE OverlappingInstances #-}
9+{-# LANGUAGE ScopedTypeVariables #-}
10+-- | This module provides 'client' which can automatically generate
11+-- querying functions for each endpoint just from the type representing your
12+-- API.
13+module Servant.Client
14+ ( client
15+ , HasClient(..)
16+ , Client
17+ , ServantError(..)
18+ , module Servant.Common.BaseUrl
19+ ) where
20+
21+import Control.Monad
22+import Control.Monad.Trans.Either
23+import Data.ByteString.Lazy (ByteString)
24+import Data.List
25+import Data.Proxy
26+import Data.String.Conversions
27+import Data.Text (unpack)
28+import GHC.TypeLits
29+import Network.HTTP.Client (Response)
30+import Network.HTTP.Media
31+import qualified Network.HTTP.Types as H
32+import Servant.API
33+import Servant.API.ContentTypes
34+import Servant.Common.BaseUrl
35+import Servant.Common.Req
36+
37+-- * Accessing APIs as a Client
38+
39+-- | 'client' allows you to produce operations to query an API from a client.
40+--
41+-- > type MyApi = "books" :> Get [Book] -- GET /books
42+-- > :<|> "books" :> ReqBody Book :> Post Book -- POST /books
43+-- >
44+-- > myApi :: Proxy MyApi
45+-- > myApi = Proxy
46+-- >
47+-- > getAllBooks :: BaseUrl -> EitherT String IO [Book]
48+-- > postNewBook :: Book -> BaseUrl -> EitherT String IO Book
49+-- > (getAllBooks :<|> postNewBook) = client myApi
50+client :: HasClient (Canonicalize layout) => Proxy layout -> Client layout
51+client p = clientWithRoute (canonicalize p) defReq
52+
53+-- | This class lets us define how each API combinator
54+-- influences the creation of an HTTP request. It's mostly
55+-- an internal class, you can just use 'client'.
56+class HasClient layout where
57+ type Client' layout :: *
58+ clientWithRoute :: Proxy layout -> Req -> Client' layout
59+
60+type Client layout = Client' (Canonicalize layout)
61+
62+-- | A client querying function for @a ':<|>' b@ will actually hand you
63+-- one function for querying @a@ and another one for querying @b@,
64+-- stitching them together with ':<|>', which really is just like a pair.
65+--
66+-- > type MyApi = "books" :> Get [Book] -- GET /books
67+-- > :<|> "books" :> ReqBody Book :> Post Book -- POST /books
68+-- >
69+-- > myApi :: Proxy MyApi
70+-- > myApi = Proxy
71+-- >
72+-- > getAllBooks :: BaseUrl -> EitherT String IO [Book]
73+-- > postNewBook :: Book -> BaseUrl -> EitherT String IO Book
74+-- > (getAllBooks :<|> postNewBook) = client myApi
75+instance (HasClient a, HasClient b) => HasClient (a :<|> b) where
76+ type Client' (a :<|> b) = Client' a :<|> Client' b
77+ clientWithRoute Proxy req =
78+ clientWithRoute (Proxy :: Proxy a) req :<|>
79+ clientWithRoute (Proxy :: Proxy b) req
80+
81+-- | If you use a 'Capture' in one of your endpoints in your API,
82+-- the corresponding querying function will automatically take
83+-- an additional argument of the type specified by your 'Capture'.
84+-- That function will take care of inserting a textual representation
85+-- of this value at the right place in the request path.
86+--
87+-- You can control how values for this type are turned into
88+-- text by specifying a 'ToText' instance for your type.
89+--
90+-- Example:
91+--
92+-- > type MyApi = "books" :> Capture "isbn" Text :> Get Book
93+-- >
94+-- > myApi :: Proxy MyApi
95+-- > myApi = Proxy
96+-- >
97+-- > getBook :: Text -> BaseUrl -> EitherT String IO Book
98+-- > getBook = client myApi
99+-- > -- then you can just use "getBook" to query that endpoint
100+instance (KnownSymbol capture, ToText a, HasClient sublayout)
101+ => HasClient (Capture capture a :> sublayout) where
102+
103+ type Client' (Capture capture a :> sublayout) =
104+ a -> Client' sublayout
105+
106+ clientWithRoute Proxy req val =
107+ clientWithRoute (Proxy :: Proxy sublayout) $
108+ appendToPath p req
109+
110+ where p = unpack (toText val)
111+
112+-- | If you have a 'Delete' endpoint in your API, the client
113+-- side querying function that is created when calling 'client'
114+-- will just require an argument that specifies the scheme, host
115+-- and port to send the request to.
116+instance HasClient Delete where
117+ type Client' Delete = BaseUrl -> EitherT ServantError IO ()
118+
119+ clientWithRoute Proxy req host =
120+ void $ performRequest H.methodDelete req (`elem` [200, 202, 204]) host
121+
122+-- | If you have a 'Get' endpoint in your API, the client
123+-- side querying function that is created when calling 'client'
124+-- will just require an argument that specifies the scheme, host
125+-- and port to send the request to.
126+instance (MimeUnrender ct result) => HasClient (Get (ct ': cts) result) where
127+ type Client' (Get (ct ': cts) result) = BaseUrl -> EitherT ServantError IO result
128+ clientWithRoute Proxy req host =
129+ performRequestCT (Proxy :: Proxy ct) H.methodGet req [200, 203] host
130+
131+-- | If you have a 'Get xs ()' endpoint, the client expects a 204 No Content
132+-- HTTP header.
133+instance HasClient (Get (ct ': cts) ()) where
134+ type Client' (Get (ct ': cts) ()) = BaseUrl -> EitherT ServantError IO ()
135+ clientWithRoute Proxy req host =
136+ performRequestNoBody H.methodGet req [204] host
137+
138+-- | If you use a 'Header' in one of your endpoints in your API,
139+-- the corresponding querying function will automatically take
140+-- an additional argument of the type specified by your 'Header',
141+-- wrapped in Maybe.
142+--
143+-- That function will take care of encoding this argument as Text
144+-- in the request headers.
145+--
146+-- All you need is for your type to have a 'ToText' instance.
147+--
148+-- Example:
149+--
150+-- > newtype Referer = Referer Text
151+-- > deriving (Eq, Show, FromText, ToText)
152+-- >
153+-- > -- GET /view-my-referer
154+-- > type MyApi = "view-my-referer" :> Header "Referer" Referer :> Get Referer
155+-- >
156+-- > myApi :: Proxy MyApi
157+-- > myApi = Proxy
158+-- >
159+-- > viewReferer :: Maybe Referer -> BaseUrl -> EitherT String IO Book
160+-- > viewReferer = client myApi
161+-- > -- then you can just use "viewRefer" to query that endpoint
162+-- > -- specifying Nothing or Just "http://haskell.org/" as arguments
163+instance (KnownSymbol sym, ToText a, HasClient sublayout)
164+ => HasClient (Header sym a :> sublayout) where
165+
166+ type Client' (Header sym a :> sublayout) =
167+ Maybe a -> Client' sublayout
168+
169+ clientWithRoute Proxy req mval =
170+ clientWithRoute (Proxy :: Proxy sublayout) $
171+ maybe req (\value -> Servant.Common.Req.addHeader hname value req) mval
172+
173+ where hname = symbolVal (Proxy :: Proxy sym)
174+
175+-- | If you have a 'Post' endpoint in your API, the client
176+-- side querying function that is created when calling 'client'
177+-- will just require an argument that specifies the scheme, host
178+-- and port to send the request to.
179+instance (MimeUnrender ct a) => HasClient (Post (ct ': cts) a) where
180+ type Client' (Post (ct ': cts) a) = BaseUrl -> EitherT ServantError IO a
181+
182+ clientWithRoute Proxy req uri =
183+ performRequestCT (Proxy :: Proxy ct) H.methodPost req [200,201] uri
184+
185+-- | If you have a 'Post xs ()' endpoint, the client expects a 204 No Content
186+-- HTTP header.
187+instance HasClient (Post (ct ': cts) ()) where
188+ type Client' (Post (ct ': cts) ()) = BaseUrl -> EitherT ServantError IO ()
189+ clientWithRoute Proxy req host =
190+ void $ performRequestNoBody H.methodPost req [204] host
191+
192+-- | If you have a 'Put' endpoint in your API, the client
193+-- side querying function that is created when calling 'client'
194+-- will just require an argument that specifies the scheme, host
195+-- and port to send the request to.
196+instance (MimeUnrender ct a) => HasClient (Put (ct ': cts) a) where
197+ type Client' (Put (ct ': cts) a) = BaseUrl -> EitherT ServantError IO a
198+
199+ clientWithRoute Proxy req host =
200+ performRequestCT (Proxy :: Proxy ct) H.methodPut req [200,201] host
201+
202+-- | If you have a 'Put xs ()' endpoint, the client expects a 204 No Content
203+-- HTTP header.
204+instance HasClient (Put (ct ': cts) ()) where
205+ type Client' (Put (ct ': cts) ()) = BaseUrl -> EitherT ServantError IO ()
206+ clientWithRoute Proxy req host =
207+ void $ performRequestNoBody H.methodPut req [204] host
208+
209+-- | If you have a 'Patch' endpoint in your API, the client
210+-- side querying function that is created when calling 'client'
211+-- will just require an argument that specifies the scheme, host
212+-- and port to send the request to.
213+instance (MimeUnrender ct a) => HasClient (Patch (ct ': cts) a) where
214+ type Client' (Patch (ct ': cts) a) = BaseUrl -> EitherT ServantError IO a
215+
216+ clientWithRoute Proxy req host =
217+ performRequestCT (Proxy :: Proxy ct) H.methodPatch req [200,201] host
218+
219+-- | If you have a 'Patch xs ()' endpoint, the client expects a 204 No Content
220+-- HTTP header.
221+instance HasClient (Patch (ct ': cts) ()) where
222+ type Client' (Patch (ct ': cts) ()) = BaseUrl -> EitherT ServantError IO ()
223+ clientWithRoute Proxy req host =
224+ void $ performRequestNoBody H.methodPatch req [204] host
225+
226+-- | If you use a 'QueryParam' in one of your endpoints in your API,
227+-- the corresponding querying function will automatically take
228+-- an additional argument of the type specified by your 'QueryParam',
229+-- enclosed in Maybe.
230+--
231+-- If you give Nothing, nothing will be added to the query string.
232+--
233+-- If you give a non-'Nothing' value, this function will take care
234+-- of inserting a textual representation of this value in the query string.
235+--
236+-- You can control how values for your type are turned into
237+-- text by specifying a 'ToText' instance for your type.
238+--
239+-- Example:
240+--
241+-- > type MyApi = "books" :> QueryParam "author" Text :> Get [Book]
242+-- >
243+-- > myApi :: Proxy MyApi
244+-- > myApi = Proxy
245+-- >
246+-- > getBooksBy :: Maybe Text -> BaseUrl -> EitherT String IO [Book]
247+-- > getBooksBy = client myApi
248+-- > -- then you can just use "getBooksBy" to query that endpoint.
249+-- > -- 'getBooksBy Nothing' for all books
250+-- > -- 'getBooksBy (Just "Isaac Asimov")' to get all books by Isaac Asimov
251+instance (KnownSymbol sym, ToText a, HasClient sublayout)
252+ => HasClient (QueryParam sym a :> sublayout) where
253+
254+ type Client' (QueryParam sym a :> sublayout) =
255+ Maybe a -> Client' sublayout
256+
257+ -- if mparam = Nothing, we don't add it to the query string
258+ clientWithRoute Proxy req mparam =
259+ clientWithRoute (Proxy :: Proxy sublayout) $
260+ maybe req (flip (appendToQueryString pname) req . Just) mparamText
261+
262+ where pname = cs pname'
263+ pname' = symbolVal (Proxy :: Proxy sym)
264+ mparamText = fmap toText mparam
265+
266+-- | If you use a 'QueryParams' in one of your endpoints in your API,
267+-- the corresponding querying function will automatically take
268+-- an additional argument, a list of values of the type specified
269+-- by your 'QueryParams'.
270+--
271+-- If you give an empty list, nothing will be added to the query string.
272+--
273+-- Otherwise, this function will take care
274+-- of inserting a textual representation of your values in the query string,
275+-- under the same query string parameter name.
276+--
277+-- You can control how values for your type are turned into
278+-- text by specifying a 'ToText' instance for your type.
279+--
280+-- Example:
281+--
282+-- > type MyApi = "books" :> QueryParams "authors" Text :> Get [Book]
283+-- >
284+-- > myApi :: Proxy MyApi
285+-- > myApi = Proxy
286+-- >
287+-- > getBooksBy :: [Text] -> BaseUrl -> EitherT String IO [Book]
288+-- > getBooksBy = client myApi
289+-- > -- then you can just use "getBooksBy" to query that endpoint.
290+-- > -- 'getBooksBy []' for all books
291+-- > -- 'getBooksBy ["Isaac Asimov", "Robert A. Heinlein"]'
292+-- > -- to get all books by Asimov and Heinlein
293+instance (KnownSymbol sym, ToText a, HasClient sublayout)
294+ => HasClient (QueryParams sym a :> sublayout) where
295+
296+ type Client' (QueryParams sym a :> sublayout) =
297+ [a] -> Client' sublayout
298+
299+ clientWithRoute Proxy req paramlist =
300+ clientWithRoute (Proxy :: Proxy sublayout) $
301+ foldl' (\ req' -> maybe req' (flip (appendToQueryString pname) req' . Just)) req paramlist'
302+
303+ where pname = cs pname'
304+ pname' = symbolVal (Proxy :: Proxy sym)
305+ paramlist' = map (Just . toText) paramlist
306+
307+-- | If you use a 'QueryFlag' in one of your endpoints in your API,
308+-- the corresponding querying function will automatically take
309+-- an additional 'Bool' argument.
310+--
311+-- If you give 'False', nothing will be added to the query string.
312+--
313+-- Otherwise, this function will insert a value-less query string
314+-- parameter under the name associated to your 'QueryFlag'.
315+--
316+-- Example:
317+--
318+-- > type MyApi = "books" :> QueryFlag "published" :> Get [Book]
319+-- >
320+-- > myApi :: Proxy MyApi
321+-- > myApi = Proxy
322+-- >
323+-- > getBooks :: Bool -> BaseUrl -> EitherT String IO [Book]
324+-- > getBooks = client myApi
325+-- > -- then you can just use "getBooks" to query that endpoint.
326+-- > -- 'getBooksBy False' for all books
327+-- > -- 'getBooksBy True' to only get _already published_ books
328+instance (KnownSymbol sym, HasClient sublayout)
329+ => HasClient (QueryFlag sym :> sublayout) where
330+
331+ type Client' (QueryFlag sym :> sublayout) =
332+ Bool -> Client' sublayout
333+
334+ clientWithRoute Proxy req flag =
335+ clientWithRoute (Proxy :: Proxy sublayout) $
336+ if flag
337+ then appendToQueryString paramname Nothing req
338+ else req
339+
340+ where paramname = cs $ symbolVal (Proxy :: Proxy sym)
341+
342+-- | If you use a 'MatrixParam' in one of your endpoints in your API,
343+-- the corresponding querying function will automatically take
344+-- an additional argument of the type specified by your 'MatrixParam',
345+-- enclosed in Maybe.
346+--
347+-- If you give Nothing, nothing will be added to the query string.
348+--
349+-- If you give a non-'Nothing' value, this function will take care
350+-- of inserting a textual representation of this value in the query string.
351+--
352+-- You can control how values for your type are turned into
353+-- text by specifying a 'ToText' instance for your type.
354+--
355+-- Example:
356+--
357+-- > type MyApi = "books" :> MatrixParam "author" Text :> Get [Book]
358+-- >
359+-- > myApi :: Proxy MyApi
360+-- > myApi = Proxy
361+-- >
362+-- > getBooksBy :: Maybe Text -> BaseUrl -> EitherT String IO [Book]
363+-- > getBooksBy = client myApi
364+-- > -- then you can just use "getBooksBy" to query that endpoint.
365+-- > -- 'getBooksBy Nothing' for all books
366+-- > -- 'getBooksBy (Just "Isaac Asimov")' to get all books by Isaac Asimov
367+instance (KnownSymbol sym, ToText a, HasClient sublayout)
368+ => HasClient (MatrixParam sym a :> sublayout) where
369+
370+ type Client' (MatrixParam sym a :> sublayout) =
371+ Maybe a -> Client' sublayout
372+
373+ -- if mparam = Nothing, we don't add it to the query string
374+ clientWithRoute Proxy req mparam =
375+ clientWithRoute (Proxy :: Proxy sublayout) $
376+ maybe req (flip (appendToMatrixParams pname . Just) req) mparamText
377+
378+ where pname = symbolVal (Proxy :: Proxy sym)
379+ mparamText = fmap (cs . toText) mparam
380+
381+-- | If you use a 'MatrixParams' in one of your endpoints in your API,
382+-- the corresponding querying function will automatically take an
383+-- additional argument, a list of values of the type specified by your
384+-- 'MatrixParams'.
385+--
386+-- If you give an empty list, nothing will be added to the query string.
387+--
388+-- Otherwise, this function will take care of inserting a textual
389+-- representation of your values in the path segment string, under the
390+-- same matrix string parameter name.
391+--
392+-- You can control how values for your type are turned into text by
393+-- specifying a 'ToText' instance for your type.
394+--
395+-- Example:
396+--
397+-- > type MyApi = "books" :> MatrixParams "authors" Text :> Get [Book]
398+-- >
399+-- > myApi :: Proxy MyApi
400+-- > myApi = Proxy
401+-- >
402+-- > getBooksBy :: [Text] -> BaseUrl -> EitherT String IO [Book]
403+-- > getBooksBy = client myApi
404+-- > -- then you can just use "getBooksBy" to query that endpoint.
405+-- > -- 'getBooksBy []' for all books
406+-- > -- 'getBooksBy ["Isaac Asimov", "Robert A. Heinlein"]'
407+-- > -- to get all books by Asimov and Heinlein
408+instance (KnownSymbol sym, ToText a, HasClient sublayout)
409+ => HasClient (MatrixParams sym a :> sublayout) where
410+
411+ type Client' (MatrixParams sym a :> sublayout) =
412+ [a] -> Client' sublayout
413+
414+ clientWithRoute Proxy req paramlist =
415+ clientWithRoute (Proxy :: Proxy sublayout) $
416+ foldl' (\ req' value -> maybe req' (flip (appendToMatrixParams pname) req' . Just . cs) value) req paramlist'
417+
418+ where pname = cs pname'
419+ pname' = symbolVal (Proxy :: Proxy sym)
420+ paramlist' = map (Just . toText) paramlist
421+
422+-- | If you use a 'MatrixFlag' in one of your endpoints in your API,
423+-- the corresponding querying function will automatically take an
424+-- additional 'Bool' argument.
425+--
426+-- If you give 'False', nothing will be added to the path segment.
427+--
428+-- Otherwise, this function will insert a value-less matrix parameter
429+-- under the name associated to your 'MatrixFlag'.
430+--
431+-- Example:
432+--
433+-- > type MyApi = "books" :> MatrixFlag "published" :> Get [Book]
434+-- >
435+-- > myApi :: Proxy MyApi
436+-- > myApi = Proxy
437+-- >
438+-- > getBooks :: Bool -> BaseUrl -> EitherT String IO [Book]
439+-- > getBooks = client myApi
440+-- > -- then you can just use "getBooks" to query that endpoint.
441+-- > -- 'getBooksBy False' for all books
442+-- > -- 'getBooksBy True' to only get _already published_ books
443+instance (KnownSymbol sym, HasClient sublayout)
444+ => HasClient (MatrixFlag sym :> sublayout) where
445+
446+ type Client' (MatrixFlag sym :> sublayout) =
447+ Bool -> Client' sublayout
448+
449+ clientWithRoute Proxy req flag =
450+ clientWithRoute (Proxy :: Proxy sublayout) $
451+ if flag
452+ then appendToMatrixParams paramname Nothing req
453+ else req
454+
455+ where paramname = cs $ symbolVal (Proxy :: Proxy sym)
456+
457+-- | Pick a 'Method' and specify where the server you want to query is. You get
458+-- back the full `Response`.
459+instance HasClient Raw where
460+ type Client' Raw = H.Method -> BaseUrl -> EitherT ServantError IO (Int, ByteString, MediaType, Response ByteString)
461+
462+ clientWithRoute :: Proxy Raw -> Req -> Client' Raw
463+ clientWithRoute Proxy req httpMethod host = do
464+ performRequest httpMethod req (const True) host
465+
466+-- | If you use a 'ReqBody' in one of your endpoints in your API,
467+-- the corresponding querying function will automatically take
468+-- an additional argument of the type specified by your 'ReqBody'.
469+-- That function will take care of encoding this argument as JSON and
470+-- of using it as the request body.
471+--
472+-- All you need is for your type to have a 'ToJSON' instance.
473+--
474+-- Example:
475+--
476+-- > type MyApi = "books" :> ReqBody Book :> Post Book
477+-- >
478+-- > myApi :: Proxy MyApi
479+-- > myApi = Proxy
480+-- >
481+-- > addBook :: Book -> BaseUrl -> EitherT String IO Book
482+-- > addBook = client myApi
483+-- > -- then you can just use "addBook" to query that endpoint
484+instance (MimeRender ct a, HasClient sublayout)
485+ => HasClient (ReqBody (ct ': cts) a :> sublayout) where
486+
487+ type Client' (ReqBody (ct ': cts) a :> sublayout) =
488+ a -> Client' sublayout
489+
490+ clientWithRoute Proxy req body =
491+ clientWithRoute (Proxy :: Proxy sublayout) $ do
492+ let ctProxy = Proxy :: Proxy ct
493+ setRQBody (mimeRender ctProxy body) (contentType ctProxy) req
494+
495+-- | Make the querying function append @path@ to the request path.
496+instance (KnownSymbol path, HasClient sublayout) => HasClient (path :> sublayout) where
497+ type Client' (path :> sublayout) = Client' sublayout
498+
499+ clientWithRoute Proxy req =
500+ clientWithRoute (Proxy :: Proxy sublayout) $
501+ appendToPath p req
502+
503+ where p = symbolVal (Proxy :: Proxy path)
504+
servant-client/src/Servant/Common/BaseUrl.hsnew file+55
@@ -0,0 +1,55 @@
1+{-# LANGUAGE DeriveGeneric #-}
2+{-# LANGUAGE ViewPatterns #-}
3+module Servant.Common.BaseUrl where
4+
5+import Data.List
6+import GHC.Generics
7+import Network.URI
8+import Safe
9+import Text.Read
10+
11+-- | URI scheme to use
12+data Scheme =
13+ Http -- ^ http://
14+ | Https -- ^ https://
15+ deriving (Show, Eq, Ord, Generic)
16+
17+-- | Simple data type to represent the target of HTTP requests
18+-- for servant's automatically-generated clients.
19+data BaseUrl = BaseUrl
20+ { baseUrlScheme :: Scheme -- ^ URI scheme to use
21+ , baseUrlHost :: String -- ^ host (eg "haskell.org")
22+ , baseUrlPort :: Int -- ^ port (eg 80)
23+ } deriving (Show, Eq, Ord, Generic)
24+
25+showBaseUrl :: BaseUrl -> String
26+showBaseUrl (BaseUrl urlscheme host port) =
27+ schemeString ++ "//" ++ host ++ portString
28+ where
29+ schemeString = case urlscheme of
30+ Http -> "http:"
31+ Https -> "https:"
32+ portString = case (urlscheme, port) of
33+ (Http, 80) -> ""
34+ (Https, 443) -> ""
35+ _ -> ":" ++ show port
36+
37+parseBaseUrl :: String -> Either String BaseUrl
38+parseBaseUrl s = case parseURI (removeTrailingSlash s) of
39+ -- This is a rather hacky implementation and should be replaced with something
40+ -- implemented in attoparsec (which is already a dependency anyhow (via aeson)).
41+ Just (URI "http:" (Just (URIAuth "" host (':' : (readMaybe -> Just port)))) "" "" "") ->
42+ Right (BaseUrl Http host port)
43+ Just (URI "http:" (Just (URIAuth "" host "")) "" "" "") ->
44+ Right (BaseUrl Http host 80)
45+ Just (URI "https:" (Just (URIAuth "" host (':' : (readMaybe -> Just port)))) "" "" "") ->
46+ Right (BaseUrl Https host port)
47+ Just (URI "https:" (Just (URIAuth "" host "")) "" "" "") ->
48+ Right (BaseUrl Https host 443)
49+ _ -> if "://" `isInfixOf` s
50+ then Left ("invalid base url: " ++ s)
51+ else parseBaseUrl ("http://" ++ s)
52+ where
53+ removeTrailingSlash str = case lastMay str of
54+ Just '/' -> init str
55+ _ -> str
servant-client/src/Servant/Common/Req.hsnew file+184
@@ -0,0 +1,184 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+{-# LANGUAGE ScopedTypeVariables #-}
3+module Servant.Common.Req where
4+
5+import Control.Applicative
6+import Control.Exception
7+import Control.Monad
8+import Control.Monad.Catch (MonadThrow)
9+import Control.Monad.IO.Class
10+import Control.Monad.Trans.Either
11+import Data.ByteString.Lazy hiding (pack, filter, map, null, elem)
12+import Data.IORef
13+import Data.String
14+import Data.String.Conversions
15+import Data.Proxy
16+import Data.Text (Text)
17+import Data.Text.Encoding
18+import Network.HTTP.Client hiding (Proxy)
19+import Network.HTTP.Client.TLS
20+import Network.HTTP.Media
21+import Network.HTTP.Types
22+import Network.URI
23+import Servant.API.ContentTypes
24+import Servant.Common.BaseUrl
25+import Servant.Common.Text
26+import System.IO.Unsafe
27+
28+import qualified Network.HTTP.Client as Client
29+
30+data ServantError
31+ = FailureResponse
32+ { responseStatus :: Status
33+ , responseContentType :: MediaType
34+ , responseBody :: ByteString
35+ }
36+ | DecodeFailure
37+ { decodeError :: String
38+ , responseContentType :: MediaType
39+ , responseBody :: ByteString
40+ }
41+ | UnsupportedContentType
42+ { responseContentType :: MediaType
43+ , responseBody :: ByteString
44+ }
45+ | ConnectionError
46+ { connectionError :: HttpException
47+ }
48+ | InvalidContentTypeHeader
49+ { responseContentTypeHeader :: ByteString
50+ , responseBody :: ByteString
51+ }
52+ deriving (Show)
53+
54+data Req = Req
55+ { reqPath :: String
56+ , qs :: QueryText
57+ , reqBody :: Maybe (ByteString, MediaType)
58+ , reqAccept :: [MediaType]
59+ , headers :: [(String, Text)]
60+ }
61+
62+defReq :: Req
63+defReq = Req "" [] Nothing [] []
64+
65+appendToPath :: String -> Req -> Req
66+appendToPath p req =
67+ req { reqPath = reqPath req ++ "/" ++ p }
68+
69+appendToMatrixParams :: String
70+ -> Maybe String
71+ -> Req
72+ -> Req
73+appendToMatrixParams pname pvalue req =
74+ req { reqPath = reqPath req ++ ";" ++ pname ++ maybe "" ("=" ++) pvalue }
75+
76+appendToQueryString :: Text -- ^ param name
77+ -> Maybe Text -- ^ param value
78+ -> Req
79+ -> Req
80+appendToQueryString pname pvalue req =
81+ req { qs = qs req ++ [(pname, pvalue)]
82+ }
83+
84+addHeader :: ToText a => String -> a -> Req -> Req
85+addHeader name val req = req { headers = headers req
86+ ++ [(name, toText val)]
87+ }
88+
89+setRQBody :: ByteString -> MediaType -> Req -> Req
90+setRQBody b t req = req { reqBody = Just (b, t) }
91+
92+reqToRequest :: (Functor m, MonadThrow m) => Req -> BaseUrl -> m Request
93+reqToRequest req (BaseUrl reqScheme reqHost reqPort) =
94+ fmap (setheaders . setAccept . setrqb . setQS ) $ parseUrl url
95+
96+ where url = show $ nullURI { uriScheme = case reqScheme of
97+ Http -> "http:"
98+ Https -> "https:"
99+ , uriAuthority = Just $
100+ URIAuth { uriUserInfo = ""
101+ , uriRegName = reqHost
102+ , uriPort = ":" ++ show reqPort
103+ }
104+ , uriPath = reqPath req
105+ }
106+
107+ setrqb r = case reqBody req of
108+ Nothing -> r
109+ Just (b,t) -> r { requestBody = RequestBodyLBS b
110+ , requestHeaders = requestHeaders r
111+ ++ [(hContentType, cs . show $ t)] }
112+ setQS = setQueryString $ queryTextToQuery (qs req)
113+ setheaders r = r { requestHeaders = requestHeaders r
114+ <> fmap toProperHeader (headers req) }
115+ setAccept r = r { requestHeaders = filter ((/= "Accept") . fst) (requestHeaders r)
116+ <> [("Accept", renderHeader $ reqAccept req)
117+ | not . null . reqAccept $ req] }
118+ toProperHeader (name, val) =
119+ (fromString name, encodeUtf8 val)
120+
121+
122+-- * performing requests
123+
124+{-# NOINLINE __manager #-}
125+__manager :: IORef Manager
126+__manager = unsafePerformIO (newManager tlsManagerSettings >>= newIORef)
127+
128+__withGlobalManager :: (Manager -> IO a) -> IO a
129+__withGlobalManager action = readIORef __manager >>= action
130+
131+
132+displayHttpRequest :: Method -> String
133+displayHttpRequest httpmethod = "HTTP " ++ cs httpmethod ++ " request"
134+
135+
136+performRequest :: Method -> Req -> (Int -> Bool) -> BaseUrl -> EitherT ServantError IO (Int, ByteString, MediaType, Response ByteString)
137+performRequest reqMethod req isWantedStatus reqHost = do
138+ partialRequest <- liftIO $ reqToRequest req reqHost
139+
140+ let request = partialRequest { Client.method = reqMethod
141+ , checkStatus = \ _status _headers _cookies -> Nothing
142+ }
143+
144+ eResponse <- liftIO $ __withGlobalManager $ \ manager ->
145+ catchHttpException $
146+ Client.httpLbs request manager
147+ case eResponse of
148+ Left err ->
149+ left $ ConnectionError err
150+
151+ Right response -> do
152+ let status = Client.responseStatus response
153+ body = Client.responseBody response
154+ status_code = statusCode status
155+ ct <- case lookup "Content-Type" $ Client.responseHeaders response of
156+ Nothing -> pure $ "application"//"octet-stream"
157+ Just t -> case parseAccept t of
158+ Nothing -> left $ InvalidContentTypeHeader (cs t) body
159+ Just t' -> pure t'
160+ unless (isWantedStatus status_code) $
161+ left $ FailureResponse status ct body
162+ return (status_code, body, ct, response)
163+
164+performRequestCT :: MimeUnrender ct result =>
165+ Proxy ct -> Method -> Req -> [Int] -> BaseUrl -> EitherT ServantError IO result
166+performRequestCT ct reqMethod req wantedStatus reqHost = do
167+ let acceptCT = contentType ct
168+ (_status, respBody, respCT, _response) <-
169+ performRequest reqMethod (req { reqAccept = [acceptCT] }) (`elem` wantedStatus) reqHost
170+ unless (matches respCT (acceptCT)) $
171+ left $ UnsupportedContentType respCT respBody
172+ either
173+ (left . (\s -> DecodeFailure s respCT respBody))
174+ return
175+ (mimeUnrender ct respBody)
176+
177+performRequestNoBody :: Method -> Req -> [Int] -> BaseUrl -> EitherT ServantError IO ()
178+performRequestNoBody reqMethod req wantedStatus reqHost = do
179+ _ <- performRequest reqMethod req (`elem` wantedStatus) reqHost
180+ return ()
181+
182+catchHttpException :: IO a -> IO (Either HttpException a)
183+catchHttpException action =
184+ catch (Right <$> action) (pure . Left)
servant-client/test/Servant/ClientSpec.hsnew file+345
@@ -0,0 +1,345 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE DeriveGeneric #-}
3+{-# LANGUAGE FlexibleContexts #-}
4+{-# LANGUAGE GADTs #-}
5+{-# LANGUAGE OverloadedStrings #-}
6+{-# LANGUAGE RecordWildCards #-}
7+{-# LANGUAGE StandaloneDeriving #-}
8+{-# LANGUAGE TypeOperators #-}
9+{-# OPTIONS_GHC -fcontext-stack=25 #-}
10+{-# OPTIONS_GHC -fno-warn-orphans #-}
11+module Servant.ClientSpec where
12+
13+import Control.Applicative
14+import qualified Control.Arrow as Arrow
15+import Control.Concurrent
16+import Control.Exception
17+import Control.Monad.Trans.Either
18+import Data.Aeson
19+import Data.ByteString.Lazy (ByteString)
20+import Data.Char
21+import Data.Foldable (forM_)
22+import Data.Monoid
23+import Data.Proxy
24+import qualified Data.Text as T
25+import GHC.Generics
26+import qualified Network.HTTP.Client as C
27+import Network.HTTP.Media
28+import Network.HTTP.Types
29+import Network.Socket
30+import Network.Wai hiding (Response)
31+import Network.Wai.Handler.Warp
32+import Test.Hspec
33+import Test.Hspec.QuickCheck
34+import Test.HUnit
35+import Test.QuickCheck
36+
37+import Servant.API
38+import Servant.Client
39+import Servant.Server
40+
41+-- * test data types
42+
43+data Person = Person {
44+ name :: String,
45+ age :: Integer
46+ }
47+ deriving (Eq, Show, Generic)
48+
49+instance ToJSON Person
50+instance FromJSON Person
51+
52+instance ToFormUrlEncoded Person where
53+ toFormUrlEncoded Person{..} =
54+ [("name", T.pack name), ("age", T.pack (show age))]
55+
56+lookupEither :: (Show a, Eq a) => a -> [(a,b)] -> Either String b
57+lookupEither x xs = do
58+ maybe (Left $ "could not find key " <> show x) return $ lookup x xs
59+
60+instance FromFormUrlEncoded Person where
61+ fromFormUrlEncoded xs = do
62+ n <- lookupEither "name" xs
63+ a <- lookupEither "age" xs
64+ return $ Person (T.unpack n) (read $ T.unpack a)
65+
66+deriving instance Eq ServantError
67+
68+instance Eq C.HttpException where
69+ a == b = show a == show b
70+
71+alice :: Person
72+alice = Person "Alice" 42
73+
74+type Api =
75+ "get" :> Get '[JSON] Person
76+ :<|> "delete" :> Delete
77+ :<|> "capture" :> Capture "name" String :> Get '[JSON,FormUrlEncoded] Person
78+ :<|> "body" :> ReqBody '[FormUrlEncoded,JSON] Person :> Post '[JSON] Person
79+ :<|> "param" :> QueryParam "name" String :> Get '[FormUrlEncoded,JSON] Person
80+ :<|> "params" :> QueryParams "names" String :> Get '[JSON] [Person]
81+ :<|> "flag" :> QueryFlag "flag" :> Get '[JSON] Bool
82+ :<|> "matrixparam" :> MatrixParam "name" String :> Get '[JSON] Person
83+ :<|> "matrixparams" :> MatrixParams "name" String :> Get '[JSON] [Person]
84+ :<|> "matrixflag" :> MatrixFlag "flag" :> Get '[JSON] Bool
85+ :<|> "rawSuccess" :> Raw
86+ :<|> "rawFailure" :> Raw
87+ :<|> "multiple" :>
88+ Capture "first" String :>
89+ QueryParam "second" Int :>
90+ QueryFlag "third" :>
91+ ReqBody '[JSON] [(String, [Rational])] :>
92+ Get '[JSON] (String, Maybe Int, Bool, [(String, [Rational])])
93+api :: Proxy Api
94+api = Proxy
95+
96+server :: Application
97+server = serve api (
98+ return alice
99+ :<|> return ()
100+ :<|> (\ name -> return $ Person name 0)
101+ :<|> return
102+ :<|> (\ name -> case name of
103+ Just "alice" -> return alice
104+ Just name -> left (400, name ++ " not found")
105+ Nothing -> left (400, "missing parameter"))
106+ :<|> (\ names -> return (zipWith Person names [0..]))
107+ :<|> return
108+ :<|> (\ name -> case name of
109+ Just "alice" -> return alice
110+ Just name -> left (400, name ++ " not found")
111+ Nothing -> left (400, "missing parameter"))
112+ :<|> (\ names -> return (zipWith Person names [0..]))
113+ :<|> return
114+ :<|> (\ _request respond -> respond $ responseLBS ok200 [] "rawSuccess")
115+ :<|> (\ _request respond -> respond $ responseLBS badRequest400 [] "rawFailure")
116+ :<|> \ a b c d -> return (a, b, c, d)
117+ )
118+
119+withServer :: (BaseUrl -> IO a) -> IO a
120+withServer action = withWaiDaemon (return server) action
121+
122+getGet :: BaseUrl -> EitherT ServantError IO Person
123+getDelete :: BaseUrl -> EitherT ServantError IO ()
124+getCapture :: String -> BaseUrl -> EitherT ServantError IO Person
125+getBody :: Person -> BaseUrl -> EitherT ServantError IO Person
126+getQueryParam :: Maybe String -> BaseUrl -> EitherT ServantError IO Person
127+getQueryParams :: [String] -> BaseUrl -> EitherT ServantError IO [Person]
128+getQueryFlag :: Bool -> BaseUrl -> EitherT ServantError IO Bool
129+getMatrixParam :: Maybe String -> BaseUrl -> EitherT ServantError IO Person
130+getMatrixParams :: [String] -> BaseUrl -> EitherT ServantError IO [Person]
131+getMatrixFlag :: Bool -> BaseUrl -> EitherT ServantError IO Bool
132+getRawSuccess :: Method -> BaseUrl -> EitherT ServantError IO (Int, ByteString, MediaType, C.Response ByteString)
133+getRawFailure :: Method -> BaseUrl -> EitherT ServantError IO (Int, ByteString, MediaType, C.Response ByteString)
134+getMultiple :: String -> Maybe Int -> Bool -> [(String, [Rational])]
135+ -> BaseUrl
136+ -> EitherT ServantError IO (String, Maybe Int, Bool, [(String, [Rational])])
137+( getGet
138+ :<|> getDelete
139+ :<|> getCapture
140+ :<|> getBody
141+ :<|> getQueryParam
142+ :<|> getQueryParams
143+ :<|> getQueryFlag
144+ :<|> getMatrixParam
145+ :<|> getMatrixParams
146+ :<|> getMatrixFlag
147+ :<|> getRawSuccess
148+ :<|> getRawFailure
149+ :<|> getMultiple)
150+ = client api
151+
152+type FailApi =
153+ "get" :> Raw
154+ :<|> "capture" :> Capture "name" String :> Raw
155+ :<|> "body" :> Raw
156+failApi :: Proxy FailApi
157+failApi = Proxy
158+
159+failServer :: Application
160+failServer = serve failApi (
161+ (\ _request respond -> respond $ responseLBS ok200 [] "")
162+ :<|> (\ _capture _request respond -> respond $ responseLBS ok200 [("content-type", "application/json")] "")
163+ :<|> (\_request respond -> respond $ responseLBS ok200 [("content-type", "fooooo")] "")
164+ )
165+
166+withFailServer :: (BaseUrl -> IO a) -> IO a
167+withFailServer action = withWaiDaemon (return failServer) action
168+
169+spec :: Spec
170+spec = do
171+ it "Servant.API.Get" $ withServer $ \ host -> do
172+ (Arrow.left show <$> runEitherT (getGet host)) `shouldReturn` Right alice
173+
174+ it "Servant.API.Delete" $ withServer $ \ host -> do
175+ (Arrow.left show <$> runEitherT (getDelete host)) `shouldReturn` Right ()
176+
177+ it "Servant.API.Capture" $ withServer $ \ host -> do
178+ (Arrow.left show <$> runEitherT (getCapture "Paula" host)) `shouldReturn` Right (Person "Paula" 0)
179+
180+ it "Servant.API.ReqBody" $ withServer $ \ host -> do
181+ let p = Person "Clara" 42
182+ (Arrow.left show <$> runEitherT (getBody p host)) `shouldReturn` Right p
183+
184+ it "Servant.API.QueryParam" $ withServer $ \ host -> do
185+ Arrow.left show <$> runEitherT (getQueryParam (Just "alice") host) `shouldReturn` Right alice
186+ Left FailureResponse{..} <- runEitherT (getQueryParam (Just "bob") host)
187+ responseStatus `shouldBe` Status 400 "bob not found"
188+
189+ it "Servant.API.QueryParam.QueryParams" $ withServer $ \ host -> do
190+ (Arrow.left show <$> runEitherT (getQueryParams [] host)) `shouldReturn` Right []
191+ (Arrow.left show <$> runEitherT (getQueryParams ["alice", "bob"] host))
192+ `shouldReturn` Right [Person "alice" 0, Person "bob" 1]
193+
194+ context "Servant.API.QueryParam.QueryFlag" $
195+ forM_ [False, True] $ \ flag ->
196+ it (show flag) $ withServer $ \ host -> do
197+ (Arrow.left show <$> runEitherT (getQueryFlag flag host)) `shouldReturn` Right flag
198+
199+ it "Servant.API.MatrixParam" $ withServer $ \ host -> do
200+ Arrow.left show <$> runEitherT (getMatrixParam (Just "alice") host) `shouldReturn` Right alice
201+ Left FailureResponse{..} <- runEitherT (getMatrixParam (Just "bob") host)
202+ responseStatus `shouldBe` Status 400 "bob not found"
203+
204+ it "Servant.API.MatrixParam.MatrixParams" $ withServer $ \ host -> do
205+ Arrow.left show <$> runEitherT (getMatrixParams [] host) `shouldReturn` Right []
206+ Arrow.left show <$> runEitherT (getMatrixParams ["alice", "bob"] host)
207+ `shouldReturn` Right [Person "alice" 0, Person "bob" 1]
208+
209+ context "Servant.API.MatrixParam.MatrixFlag" $
210+ forM_ [False, True] $ \ flag ->
211+ it (show flag) $ withServer $ \ host -> do
212+ Arrow.left show <$> runEitherT (getMatrixFlag flag host) `shouldReturn` Right flag
213+
214+ it "Servant.API.Raw on success" $ withServer $ \ host -> do
215+ res <- runEitherT (getRawSuccess methodGet host)
216+ case res of
217+ Left e -> assertFailure $ show e
218+ Right (code, body, ct, response) -> do
219+ (code, body, ct) `shouldBe` (200, "rawSuccess", "application"//"octet-stream")
220+ C.responseBody response `shouldBe` body
221+ C.responseStatus response `shouldBe` ok200
222+
223+ it "Servant.API.Raw on failure" $ withServer $ \ host -> do
224+ res <- runEitherT (getRawFailure methodGet host)
225+ case res of
226+ Left e -> assertFailure $ show e
227+ Right (code, body, ct, response) -> do
228+ (code, body, ct) `shouldBe` (400, "rawFailure", "application"//"octet-stream")
229+ C.responseBody response `shouldBe` body
230+ C.responseStatus response `shouldBe` badRequest400
231+
232+ modifyMaxSuccess (const 20) $ do
233+ it "works for a combination of Capture, QueryParam, QueryFlag and ReqBody" $
234+ property $ forAllShrink pathGen shrink $ \(NonEmpty cap) num flag body ->
235+ ioProperty $ do
236+ withServer $ \ host -> do
237+ result <- Arrow.left show <$> runEitherT (getMultiple cap num flag body host)
238+ return $
239+ result === Right (cap, num, flag, body)
240+
241+
242+ context "client correctly handles error status codes" $ do
243+ let test :: (WrappedApi, String) -> Spec
244+ test (WrappedApi api, desc) =
245+ it desc $
246+ withWaiDaemon (return (serve api (left (500, "error message")))) $
247+ \ host -> do
248+ let getResponse :: BaseUrl -> EitherT ServantError IO ()
249+ getResponse = client api
250+ Left FailureResponse{..} <- runEitherT (getResponse host)
251+ responseStatus `shouldBe` (Status 500 "error message")
252+ mapM_ test $
253+ (WrappedApi (Proxy :: Proxy Delete), "Delete") :
254+ (WrappedApi (Proxy :: Proxy (Get '[JSON] ())), "Get") :
255+ (WrappedApi (Proxy :: Proxy (Post '[JSON] ())), "Post") :
256+ (WrappedApi (Proxy :: Proxy (Put '[JSON] ())), "Put") :
257+ []
258+
259+ context "client returns errors appropriately" $ do
260+ it "reports FailureResponse" $ withFailServer $ \ host -> do
261+ Left res <- runEitherT (getDelete host)
262+ case res of
263+ FailureResponse (Status 404 "Not Found") _ _ -> return ()
264+ _ -> fail $ "expected 404 response, but got " <> show res
265+
266+ it "reports DecodeFailure" $ withFailServer $ \ host -> do
267+ Left res <- runEitherT (getCapture "foo" host)
268+ case res of
269+ DecodeFailure _ ("application/json") _ -> return ()
270+ _ -> fail $ "expected DecodeFailure, but got " <> show res
271+
272+ it "reports ConnectionError" $ do
273+ Right host <- return $ parseBaseUrl "127.0.0.1:987654"
274+ Left res <- runEitherT (getGet host)
275+ case res of
276+ ConnectionError (C.FailedConnectionException2 "127.0.0.1" 987654 False _) -> return ()
277+ _ -> fail $ "expected ConnectionError, but got " <> show res
278+
279+ it "reports UnsupportedContentType" $ withFailServer $ \ host -> do
280+ Left res <- runEitherT (getGet host)
281+ case res of
282+ UnsupportedContentType ("application/octet-stream") _ -> return ()
283+ _ -> fail $ "expected UnsupportedContentType, but got " <> show res
284+
285+ it "reports InvalidContentTypeHeader" $ withFailServer $ \ host -> do
286+ Left res <- runEitherT (getBody alice host)
287+ case res of
288+ InvalidContentTypeHeader "fooooo" _ -> return ()
289+ _ -> fail $ "expected InvalidContentTypeHeader, but got " <> show res
290+
291+data WrappedApi where
292+ WrappedApi :: (HasServer (Canonicalize api), Server api ~ EitherT (Int, String) IO a,
293+ HasClient (Canonicalize api), Client api ~ (BaseUrl -> EitherT ServantError IO ())) =>
294+ Proxy api -> WrappedApi
295+
296+
297+-- * utils
298+
299+withWaiDaemon :: IO Application -> (BaseUrl -> IO a) -> IO a
300+withWaiDaemon mkApplication action = do
301+ application <- mkApplication
302+ bracket (acquire application) free (\ (_, _, baseUrl) -> action baseUrl)
303+ where
304+ acquire application = do
305+ (notifyStart, waitForStart) <- lvar
306+ (notifyKilled, waitForKilled) <- lvar
307+ thread <- forkIO $ (do
308+ (krakenPort, socket) <- openTestSocket
309+ let settings =
310+ setPort krakenPort $ -- set here just for consistency, shouldn't be
311+ -- used (it's set in the socket)
312+ setBeforeMainLoop (notifyStart krakenPort)
313+ defaultSettings
314+ runSettingsSocket settings socket application)
315+ `finally` notifyKilled ()
316+ krakenPort <- waitForStart
317+ let baseUrl = (BaseUrl Http "localhost" 80){baseUrlPort = krakenPort}
318+ return (thread, waitForKilled, baseUrl)
319+ free (thread, waitForKilled, _) = do
320+ killThread thread
321+ waitForKilled
322+
323+ lvar :: IO (a -> IO (), IO a)
324+ lvar = do
325+ mvar <- newEmptyMVar
326+ let put = putMVar mvar
327+ wait = readMVar mvar
328+ return (put, wait)
329+
330+openTestSocket :: IO (Port, Socket)
331+openTestSocket = do
332+ s <- socket AF_INET Stream defaultProtocol
333+ localhost <- inet_addr "127.0.0.1"
334+ bind s (SockAddrInet aNY_PORT localhost)
335+ listen s 1
336+ port <- socketPort s
337+ return (fromIntegral port, s)
338+
339+pathGen :: Gen (NonEmptyList Char)
340+pathGen = fmap NonEmpty path
341+ where
342+ path = listOf1 $ elements $
343+ filter (not . (`elem` "?%[]/#;")) $
344+ filter isPrint $
345+ map chr [0..127]
servant-client/test/Servant/Common/BaseUrlSpec.hsnew file+69
@@ -0,0 +1,69 @@
1+{-# OPTIONS_GHC -fno-warn-orphans #-}
2+module Servant.Common.BaseUrlSpec where
3+
4+import Control.Applicative
5+import Control.DeepSeq
6+import Test.Hspec
7+import Test.QuickCheck
8+
9+import Servant.Common.BaseUrl
10+
11+spec :: Spec
12+spec = do
13+ describe "showBaseUrl" $ do
14+ it "shows a BaseUrl" $ do
15+ showBaseUrl (BaseUrl Http "foo.com" 80) `shouldBe` "http://foo.com"
16+
17+ it "shows a https BaseUrl" $ do
18+ showBaseUrl (BaseUrl Https "foo.com" 443) `shouldBe` "https://foo.com"
19+
20+ describe "httpBaseUrl" $ do
21+ it "allows to construct default http BaseUrls" $ do
22+ BaseUrl Http "bar" 80 `shouldBe` BaseUrl Http "bar" 80
23+
24+ describe "parseBaseUrl" $ do
25+ it "is total" $ do
26+ property $ \ string ->
27+ deepseq (fmap show (parseBaseUrl string)) True
28+
29+ it "is the inverse of showBaseUrl" $ do
30+ property $ \ baseUrl ->
31+ counterexample (showBaseUrl baseUrl) $
32+ parseBaseUrl (showBaseUrl baseUrl) ===
33+ Right baseUrl
34+
35+ it "allows trailing slashes" $ do
36+ parseBaseUrl "foo.com/" `shouldBe` Right (BaseUrl Http "foo.com" 80)
37+
38+ context "urls without scheme" $ do
39+ it "assumes http" $ do
40+ parseBaseUrl "foo.com" `shouldBe` Right (BaseUrl Http "foo.com" 80)
41+
42+ it "allows port numbers" $ do
43+ parseBaseUrl "foo.com:8080" `shouldBe` Right (BaseUrl Http "foo.com" 8080)
44+
45+ it "rejects ftp urls" $ do
46+ parseBaseUrl "ftp://foo.com" `shouldSatisfy` isLeft
47+
48+instance Arbitrary BaseUrl where
49+ arbitrary = BaseUrl <$>
50+ elements [Http, Https] <*>
51+ hostNameGen <*>
52+ portGen
53+ where
54+ -- this does not perfectly mirror the url standard, but I hope it's good
55+ -- enough.
56+ hostNameGen = do
57+ let letters = ['a' .. 'z'] ++ ['A' .. 'Z']
58+ first <- elements letters
59+ middle <- listOf1 $ elements (letters ++ ['0' .. '9'] ++ ['.', '-'])
60+ last <- elements letters
61+ return (first : middle ++ [last])
62+ portGen = frequency $
63+ (1, return 80) :
64+ (1, return 443) :
65+ (1, choose (1, 20000)) :
66+ []
67+
68+isLeft :: Either a b -> Bool
69+isLeft = either (const True) (const False)
servant-client/test/Spec.hsnew file+1
@@ -0,0 +1 @@
1+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}