@@ -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+