@@ -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
18+ , module Servant.Common.BaseUrl
22+import Control.Monad.Trans.Either
23+import Data.ByteString.Lazy (ByteString)
26+import Data.String.Conversions
27+import Data.Text (unpack)
29+import Network.HTTP.Client (Response)
30+import Network.HTTP.Media
31+import qualified Network.HTTP.Types as H
33+import Servant.API.ContentTypes
34+import Servant.Common.BaseUrl
35+import Servant.Common.Req
37+-- * Accessing APIs as a Client
39+-- | 'client' allows you to produce operations to query an API from a client.
41+-- > type MyApi = "books" :> Get [Book] -- GET /books
42+-- > :<|> "books" :> ReqBody Book :> Post Book -- POST /books
44+-- > myApi :: Proxy MyApi
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
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
60+type Client layout = Client' (Canonicalize layout)
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.
66+-- > type MyApi = "books" :> Get [Book] -- GET /books
67+-- > :<|> "books" :> ReqBody Book :> Post Book -- POST /books
69+-- > myApi :: Proxy MyApi
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
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.
87+-- You can control how values for this type are turned into
88+-- text by specifying a 'ToText' instance for your type.
92+-- > type MyApi = "books" :> Capture "isbn" Text :> Get Book
94+-- > myApi :: Proxy MyApi
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
103+ type Client' (Capture capture a :> sublayout) =
104+ a -> Client' sublayout
106+ clientWithRoute Proxy req val =
107+ clientWithRoute (Proxy :: Proxy sublayout) $
110+ where p = unpack (toText val)
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 ()
119+ clientWithRoute Proxy req host =
120+ void $ performRequest H.methodDelete req (`elem` [200, 202, 204]) host
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
131+-- | If you have a 'Get xs ()' endpoint, the client expects a 204 No Content
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
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',
143+-- That function will take care of encoding this argument as Text
144+-- in the request headers.
146+-- All you need is for your type to have a 'ToText' instance.
150+-- > newtype Referer = Referer Text
151+-- > deriving (Eq, Show, FromText, ToText)
153+-- > -- GET /view-my-referer
154+-- > type MyApi = "view-my-referer" :> Header "Referer" Referer :> Get Referer
156+-- > myApi :: Proxy MyApi
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
166+ type Client' (Header sym a :> sublayout) =
167+ Maybe a -> Client' sublayout
169+ clientWithRoute Proxy req mval =
170+ clientWithRoute (Proxy :: Proxy sublayout) $
171+ maybe req (\value -> Servant.Common.Req.addHeader hname value req) mval
173+ where hname = symbolVal (Proxy :: Proxy sym)
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
182+ clientWithRoute Proxy req uri =
183+ performRequestCT (Proxy :: Proxy ct) H.methodPost req [200,201] uri
185+-- | If you have a 'Post xs ()' endpoint, the client expects a 204 No Content
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
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
199+ clientWithRoute Proxy req host =
200+ performRequestCT (Proxy :: Proxy ct) H.methodPut req [200,201] host
202+-- | If you have a 'Put xs ()' endpoint, the client expects a 204 No Content
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
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
216+ clientWithRoute Proxy req host =
217+ performRequestCT (Proxy :: Proxy ct) H.methodPatch req [200,201] host
219+-- | If you have a 'Patch xs ()' endpoint, the client expects a 204 No Content
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
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.
231+-- If you give Nothing, nothing will be added to the query string.
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.
236+-- You can control how values for your type are turned into
237+-- text by specifying a 'ToText' instance for your type.
241+-- > type MyApi = "books" :> QueryParam "author" Text :> Get [Book]
243+-- > myApi :: Proxy MyApi
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
254+ type Client' (QueryParam sym a :> sublayout) =
255+ Maybe a -> Client' sublayout
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
262+ where pname = cs pname'
263+ pname' = symbolVal (Proxy :: Proxy sym)
264+ mparamText = fmap toText mparam
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'.
271+-- If you give an empty list, nothing will be added to the query string.
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.
277+-- You can control how values for your type are turned into
278+-- text by specifying a 'ToText' instance for your type.
282+-- > type MyApi = "books" :> QueryParams "authors" Text :> Get [Book]
284+-- > myApi :: Proxy MyApi
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
296+ type Client' (QueryParams sym a :> sublayout) =
297+ [a] -> Client' sublayout
299+ clientWithRoute Proxy req paramlist =
300+ clientWithRoute (Proxy :: Proxy sublayout) $
301+ foldl' (\ req' -> maybe req' (flip (appendToQueryString pname) req' . Just)) req paramlist'
303+ where pname = cs pname'
304+ pname' = symbolVal (Proxy :: Proxy sym)
305+ paramlist' = map (Just . toText) paramlist
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.
311+-- If you give 'False', nothing will be added to the query string.
313+-- Otherwise, this function will insert a value-less query string
314+-- parameter under the name associated to your 'QueryFlag'.
318+-- > type MyApi = "books" :> QueryFlag "published" :> Get [Book]
320+-- > myApi :: Proxy MyApi
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
331+ type Client' (QueryFlag sym :> sublayout) =
332+ Bool -> Client' sublayout
334+ clientWithRoute Proxy req flag =
335+ clientWithRoute (Proxy :: Proxy sublayout) $
337+ then appendToQueryString paramname Nothing req
340+ where paramname = cs $ symbolVal (Proxy :: Proxy sym)
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.
347+-- If you give Nothing, nothing will be added to the query string.
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.
352+-- You can control how values for your type are turned into
353+-- text by specifying a 'ToText' instance for your type.
357+-- > type MyApi = "books" :> MatrixParam "author" Text :> Get [Book]
359+-- > myApi :: Proxy MyApi
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
370+ type Client' (MatrixParam sym a :> sublayout) =
371+ Maybe a -> Client' sublayout
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
378+ where pname = symbolVal (Proxy :: Proxy sym)
379+ mparamText = fmap (cs . toText) mparam
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
386+-- If you give an empty list, nothing will be added to the query string.
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.
392+-- You can control how values for your type are turned into text by
393+-- specifying a 'ToText' instance for your type.
397+-- > type MyApi = "books" :> MatrixParams "authors" Text :> Get [Book]
399+-- > myApi :: Proxy MyApi
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
411+ type Client' (MatrixParams sym a :> sublayout) =
412+ [a] -> Client' sublayout
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'
418+ where pname = cs pname'
419+ pname' = symbolVal (Proxy :: Proxy sym)
420+ paramlist' = map (Just . toText) paramlist
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.
426+-- If you give 'False', nothing will be added to the path segment.
428+-- Otherwise, this function will insert a value-less matrix parameter
429+-- under the name associated to your 'MatrixFlag'.
433+-- > type MyApi = "books" :> MatrixFlag "published" :> Get [Book]
435+-- > myApi :: Proxy MyApi
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
446+ type Client' (MatrixFlag sym :> sublayout) =
447+ Bool -> Client' sublayout
449+ clientWithRoute Proxy req flag =
450+ clientWithRoute (Proxy :: Proxy sublayout) $
452+ then appendToMatrixParams paramname Nothing req
455+ where paramname = cs $ symbolVal (Proxy :: Proxy sym)
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)
462+ clientWithRoute :: Proxy Raw -> Req -> Client' Raw
463+ clientWithRoute Proxy req httpMethod host = do
464+ performRequest httpMethod req (const True) host
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.
472+-- All you need is for your type to have a 'ToJSON' instance.
476+-- > type MyApi = "books" :> ReqBody Book :> Post Book
478+-- > myApi :: Proxy MyApi
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
487+ type Client' (ReqBody (ct ': cts) a :> sublayout) =
488+ a -> Client' sublayout
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
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
499+ clientWithRoute Proxy req =
500+ clientWithRoute (Proxy :: Proxy sublayout) $
503+ where p = symbolVal (Proxy :: Proxy path)