@@ -2,8 +2,8 @@
2-- from @servant@ APIs. By "backend," we mean something that concretely
2-- from @servant@ APIs. By "backend," we mean something that concretely
3-- executes the request, such as:
3-- executes the request, such as:
5−-- * The 'http-client' library
5+-- * The @http-client@ library
6−-- * The 'haxl' library
6+-- * The @haxl@ library
7-- * GHCJS via FFI
7-- * GHCJS via FFI
@@ -11,8 +11,8 @@
11-- Each backend is encapsulated in a monad that is an instance of the
11-- Each backend is encapsulated in a monad that is an instance of the
12-- 'RunClient' class.
12-- 'RunClient' class.
14−-- This library is primarily of interest to backend-writers, who are encouraged
14+-- This library is primarily of interest to backend-writers and
15−-- to re-export the parts of the
15+-- combinator-writers. For more information, see the README.md
16module Servant.Client.Core
16module Servant.Client.Core
18 -- * Client generation
18 -- * Client generation
@@ -63,536 +63,3 @@ import Servant.Client.Core.Internal.HasClient
63import Servant.Client.Core.Internal.Generic
63import Servant.Client.Core.Internal.Generic
64import Servant.Client.Core.Internal.Request
64import Servant.Client.Core.Internal.Request
65import Servant.Client.Core.Internal.RunClient
65import Servant.Client.Core.Internal.RunClient
68−import Control.Monad.Error.Class (throwError)
69−import Data.List (foldl')
70−import Data.Proxy (Proxy (Proxy))
71−import Data.String (fromString)
72−import Data.Text (pack)
73−import GHC.Exts (fromList, toList)
74−import GHC.TypeLits (KnownSymbol, symbolVal)
75−import qualified Network.HTTP.Types as H
78−import Servant.API ((:<|>) ((:<|>)), (:>),
79− AuthProtect, BasicAuth,
83− Description, EmptyAPI,
84− Header, Headers (..),
85− HttpVersion, IsSecure,
86− MimeRender (mimeRender),
87− MimeUnrender (mimeUnrender),
88− NoContent (NoContent),
89− QueryFlag, QueryParam,
93− Summary, ToHttpApiData,
101−import Servant.API.ContentTypes (contentTypes)
103−import Servant.Client.Core.Internal.Auth
104−import Servant.Client.Core.Internal.BaseUrl (BaseUrl (..),
105− InvalidBaseUrlException,
109−import Servant.Client.Core.Internal.BasicAuth
110−import Servant.Client.Core.Internal.Class
111−import Servant.Client.Core.Internal.Generic
112−import Servant.Client.Core.Internal.Request
114−-- * Accessing APIs as a Client
116−-- | 'clientIn' allows you to produce operations to query an API from a client
117−-- within a 'RunClient' monad.
119−-- > type MyApi = "books" :> Get '[JSON] [Book] -- GET /books
120−-- > :<|> "books" :> ReqBody '[JSON] Book :> Post '[JSON] Book -- POST /books
122−-- > myApi :: Proxy MyApi
125−-- > clientM :: Proxy ClientM
128−-- > getAllBooks :: ClientM [Book]
129−-- > postNewBook :: Book -> ClientM Book
130−-- > (getAllBooks :<|> postNewBook) = myApi `clientIn` clientM
131−clientIn :: HasClient m api => Proxy api -> Proxy m -> Client m api
132−clientIn p pm = clientWithRoute pm p defaultRequest
135−-- | This class lets us define how each API combinator influences the creation
136−-- of an HTTP request.
138−-- Unless you are writing a new backend for @servant-client-core@ or new
139−-- combinators that you want to support client-generation, you can ignore this
141−class RunClient m => HasClient m api where
142− type Client (m :: * -> *) (api :: *) :: *
143− clientWithRoute :: Proxy m -> Proxy api -> Request -> Client m api
146−-- | A client querying function for @a ':<|>' b@ will actually hand you
147−-- one function for querying @a@ and another one for querying @b@,
148−-- stitching them together with ':<|>', which really is just like a pair.
150−-- > type MyApi = "books" :> Get '[JSON] [Book] -- GET /books
151−-- > :<|> "books" :> ReqBody '[JSON] Book :> Post Book -- POST /books
153−-- > myApi :: Proxy MyApi
156−-- > getAllBooks :: ClientM [Book]
157−-- > postNewBook :: Book -> ClientM Book
158−-- > (getAllBooks :<|> postNewBook) = client myApi
159−instance (HasClient m a, HasClient m b) => HasClient m (a :<|> b) where
160− type Client m (a :<|> b) = Client m a :<|> Client m b
161− clientWithRoute pm Proxy req =
162− clientWithRoute pm (Proxy :: Proxy a) req :<|>
163− clientWithRoute pm (Proxy :: Proxy b) req
165−-- | Singleton type representing a client for an empty API.
166−data EmptyClient = EmptyClient deriving (Eq, Show, Bounded, Enum)
168−-- | The client for 'EmptyAPI' is simply 'EmptyClient'.
170−-- > type MyAPI = "books" :> Get '[JSON] [Book] -- GET /books
171−-- > :<|> "nothing" :> EmptyAPI
173−-- > myApi :: Proxy MyApi
176−-- > getAllBooks :: ClientM [Book]
177−-- > (getAllBooks :<|> EmptyClient) = client myApi
178−instance RunClient m => HasClient m EmptyAPI where
179− type Client m EmptyAPI = EmptyClient
180− clientWithRoute _pm Proxy _ = EmptyClient
182−-- | If you use a 'Capture' in one of your endpoints in your API,
183−-- the corresponding querying function will automatically take
184−-- an additional argument of the type specified by your 'Capture'.
185−-- That function will take care of inserting a textual representation
186−-- of this value at the right place in the request path.
188−-- You can control how values for this type are turned into
189−-- text by specifying a 'ToHttpApiData' instance for your type.
193−-- > type MyApi = "books" :> Capture "isbn" Text :> Get '[JSON] Book
195−-- > myApi :: Proxy MyApi
198−-- > getBook :: Text -> ClientM Book
199−-- > getBook = client myApi
200−-- > -- then you can just use "getBook" to query that endpoint
201−instance (KnownSymbol capture, ToHttpApiData a, HasClient m api)
202− => HasClient m (Capture capture a :> api) where
204− type Client m (Capture capture a :> api) =
207− clientWithRoute pm Proxy req val =
208− clientWithRoute pm (Proxy :: Proxy api)
209− (appendToPath p req)
211− where p = (toUrlPiece val)
213−-- | If you use a 'CaptureAll' in one of your endpoints in your API,
214−-- the corresponding querying function will automatically take an
215−-- additional argument of a list of the type specified by your
216−-- 'CaptureAll'. That function will take care of inserting a textual
217−-- representation of this value at the right place in the request
220−-- You can control how these values are turned into text by specifying
221−-- a 'ToHttpApiData' instance of your type.
225−-- > type MyAPI = "src" :> CaptureAll Text -> Get '[JSON] SourceFile
230−-- > getSourceFile :: [Text] -> ClientM SourceFile
231−-- > getSourceFile = client myApi
232−-- > -- then you can use "getSourceFile" to query that endpoint
233−instance (KnownSymbol capture, ToHttpApiData a, HasClient m sublayout)
234− => HasClient m (CaptureAll capture a :> sublayout) where
236− type Client m (CaptureAll capture a :> sublayout) =
237− [a] -> Client m sublayout
239− clientWithRoute pm Proxy req vals =
240− clientWithRoute pm (Proxy :: Proxy sublayout)
241− (foldl' (flip appendToPath) req ps)
243− where ps = map (toUrlPiece) vals
245−instance OVERLAPPABLE_
246− -- Note [Non-Empty Content Types]
247− ( RunClient m, MimeUnrender ct a, ReflectMethod method, cts' ~ (ct ': cts)
248− ) => HasClient m (Verb method status cts' a) where
249− type Client m (Verb method status cts' a) = m a
250− clientWithRoute _pm Proxy req = do
251− response <- runRequest req
252− { requestAccept = fromList $ toList accept
253− , requestMethod = method
255− response `decodedAs` (Proxy :: Proxy ct)
257− accept = contentTypes (Proxy :: Proxy ct)
258− method = reflectMethod (Proxy :: Proxy method)
260−instance OVERLAPPING_
261− ( RunClient m, ReflectMethod method
262− ) => HasClient m (Verb method status cts NoContent) where
263− type Client m (Verb method status cts NoContent)
265− clientWithRoute _pm Proxy req = do
266− _response <- runRequest req { requestMethod = method }
268− where method = reflectMethod (Proxy :: Proxy method)
270−instance OVERLAPPING_
271− -- Note [Non-Empty Content Types]
272− ( RunClient m, MimeUnrender ct a, BuildHeadersTo ls
273− , ReflectMethod method, cts' ~ (ct ': cts)
274− ) => HasClient m (Verb method status cts' (Headers ls a)) where
275− type Client m (Verb method status cts' (Headers ls a))
277− clientWithRoute _pm Proxy req = do
278− response <- runRequest req
279− { requestMethod = method
280− , requestAccept = fromList $ toList accept
282− case mimeUnrender (Proxy :: Proxy ct) $ responseBody response of
283− Left err -> throwError $ DecodeFailure (pack err) response
284− Right val -> return $ Headers
286− , getHeadersHList = buildHeadersTo . toList $ responseHeaders response
288− where method = reflectMethod (Proxy :: Proxy method)
289− accept = contentTypes (Proxy :: Proxy ct)
291−instance OVERLAPPING_
292− ( RunClient m, BuildHeadersTo ls, ReflectMethod method
293− ) => HasClient m (Verb method status cts (Headers ls NoContent)) where
294− type Client m (Verb method status cts (Headers ls NoContent))
295− = m (Headers ls NoContent)
296− clientWithRoute _pm Proxy req = do
297− let method = reflectMethod (Proxy :: Proxy method)
298− response <- runRequest req { requestMethod = method }
299− return $ Headers { getResponse = NoContent
300− , getHeadersHList = buildHeadersTo . toList $ responseHeaders response
304−-- | If you use a 'Header' in one of your endpoints in your API,
305−-- the corresponding querying function will automatically take
306−-- an additional argument of the type specified by your 'Header',
309−-- That function will take care of encoding this argument as Text
310−-- in the request headers.
312−-- All you need is for your type to have a 'ToHttpApiData' instance.
316−-- > newtype Referer = Referer { referrer :: Text }
317−-- > deriving (Eq, Show, Generic, ToHttpApiData)
319−-- > -- GET /view-my-referer
320−-- > type MyApi = "view-my-referer" :> Header "Referer" Referer :> Get '[JSON] Referer
322−-- > myApi :: Proxy MyApi
325−-- > viewReferer :: Maybe Referer -> ClientM Book
326−-- > viewReferer = client myApi
327−-- > -- then you can just use "viewRefer" to query that endpoint
328−-- > -- specifying Nothing or e.g Just "http://haskell.org/" as arguments
329−instance (KnownSymbol sym, ToHttpApiData a, HasClient m api)
330− => HasClient m (Header sym a :> api) where
332− type Client m (Header sym a :> api) =
333− Maybe a -> Client m api
335− clientWithRoute pm Proxy req mval =
336− clientWithRoute pm (Proxy :: Proxy api)
338− (\value -> addHeader hname value req)
342− where hname = fromString $ symbolVal (Proxy :: Proxy sym)
344−-- | Using a 'HttpVersion' combinator in your API doesn't affect the client
346−instance HasClient m api
347− => HasClient m (HttpVersion :> api) where
349− type Client m (HttpVersion :> api) =
352− clientWithRoute pm Proxy =
353− clientWithRoute pm (Proxy :: Proxy api)
355−-- | Ignore @'Summary'@ in client functions.
356−instance HasClient m api => HasClient m (Summary desc :> api) where
357− type Client m (Summary desc :> api) = Client m api
359− clientWithRoute pm _ = clientWithRoute pm (Proxy :: Proxy api)
361−-- | Ignore @'Description'@ in client functions.
362−instance HasClient m api => HasClient m (Description desc :> api) where
363− type Client m (Description desc :> api) = Client m api
365− clientWithRoute pm _ = clientWithRoute pm (Proxy :: Proxy api)
367−-- | If you use a 'QueryParam' in one of your endpoints in your API,
368−-- the corresponding querying function will automatically take
369−-- an additional argument of the type specified by your 'QueryParam',
370−-- enclosed in Maybe.
372−-- If you give Nothing, nothing will be added to the query string.
374−-- If you give a non-'Nothing' value, this function will take care
375−-- of inserting a textual representation of this value in the query string.
377−-- You can control how values for your type are turned into
378−-- text by specifying a 'ToHttpApiData' instance for your type.
382−-- > type MyApi = "books" :> QueryParam "author" Text :> Get '[JSON] [Book]
384−-- > myApi :: Proxy MyApi
387−-- > getBooksBy :: Maybe Text -> ClientM [Book]
388−-- > getBooksBy = client myApi
389−-- > -- then you can just use "getBooksBy" to query that endpoint.
390−-- > -- 'getBooksBy Nothing' for all books
391−-- > -- 'getBooksBy (Just "Isaac Asimov")' to get all books by Isaac Asimov
392−instance (KnownSymbol sym, ToHttpApiData a, HasClient m api)
393− => HasClient m (QueryParam sym a :> api) where
395− type Client m (QueryParam sym a :> api) =
396− Maybe a -> Client m api
398− -- if mparam = Nothing, we don't add it to the query string
399− clientWithRoute pm Proxy req mparam =
400− clientWithRoute pm (Proxy :: Proxy api)
402− (flip (appendToQueryString pname) req . Just)
406− where pname = pack $ symbolVal (Proxy :: Proxy sym)
407− mparamText = fmap toQueryParam mparam
409−-- | If you use a 'QueryParams' in one of your endpoints in your API,
410−-- the corresponding querying function will automatically take
411−-- an additional argument, a list of values of the type specified
412−-- by your 'QueryParams'.
414−-- If you give an empty list, nothing will be added to the query string.
416−-- Otherwise, this function will take care
417−-- of inserting a textual representation of your values in the query string,
418−-- under the same query string parameter name.
420−-- You can control how values for your type are turned into
421−-- text by specifying a 'ToHttpApiData' instance for your type.
425−-- > type MyApi = "books" :> QueryParams "authors" Text :> Get '[JSON] [Book]
427−-- > myApi :: Proxy MyApi
430−-- > getBooksBy :: [Text] -> ClientM [Book]
431−-- > getBooksBy = client myApi
432−-- > -- then you can just use "getBooksBy" to query that endpoint.
433−-- > -- 'getBooksBy []' for all books
434−-- > -- 'getBooksBy ["Isaac Asimov", "Robert A. Heinlein"]'
435−-- > -- to get all books by Asimov and Heinlein
436−instance (KnownSymbol sym, ToHttpApiData a, HasClient m api)
437− => HasClient m (QueryParams sym a :> api) where
439− type Client m (QueryParams sym a :> api) =
442− clientWithRoute pm Proxy req paramlist =
443− clientWithRoute pm (Proxy :: Proxy api)
444− (foldl' (\ req' -> maybe req' (flip (appendToQueryString pname) req' . Just))
449− where pname = pack $ symbolVal (Proxy :: Proxy sym)
450− paramlist' = map (Just . toQueryParam) paramlist
452−-- | If you use a 'QueryFlag' in one of your endpoints in your API,
453−-- the corresponding querying function will automatically take
454−-- an additional 'Bool' argument.
456−-- If you give 'False', nothing will be added to the query string.
458−-- Otherwise, this function will insert a value-less query string
459−-- parameter under the name associated to your 'QueryFlag'.
463−-- > type MyApi = "books" :> QueryFlag "published" :> Get '[JSON] [Book]
465−-- > myApi :: Proxy MyApi
468−-- > getBooks :: Bool -> ClientM [Book]
469−-- > getBooks = client myApi
470−-- > -- then you can just use "getBooks" to query that endpoint.
471−-- > -- 'getBooksBy False' for all books
472−-- > -- 'getBooksBy True' to only get _already published_ books
473−instance (KnownSymbol sym, HasClient m api)
474− => HasClient m (QueryFlag sym :> api) where
476− type Client m (QueryFlag sym :> api) =
477− Bool -> Client m api
479− clientWithRoute pm Proxy req flag =
480− clientWithRoute pm (Proxy :: Proxy api)
482− then appendToQueryString paramname Nothing req
486− where paramname = pack $ symbolVal (Proxy :: Proxy sym)
489−-- | Pick a 'Method' and specify where the server you want to query is. You get
490−-- back the full `Response`.
491−instance RunClient m => HasClient m Raw where
493− = H.Method -> m Response
495− clientWithRoute :: Proxy m -> Proxy Raw -> Request -> Client m Raw
496− clientWithRoute _pm Proxy req httpMethod = do
497− runRequest req { requestMethod = httpMethod }
499−-- | If you use a 'ReqBody' in one of your endpoints in your API,
500−-- the corresponding querying function will automatically take
501−-- an additional argument of the type specified by your 'ReqBody'.
502−-- That function will take care of encoding this argument as JSON and
503−-- of using it as the request body.
505−-- All you need is for your type to have a 'ToJSON' instance.
509−-- > type MyApi = "books" :> ReqBody '[JSON] Book :> Post '[JSON] Book
511−-- > myApi :: Proxy MyApi
514−-- > addBook :: Book -> ClientM Book
515−-- > addBook = client myApi
516−-- > -- then you can just use "addBook" to query that endpoint
517−instance (MimeRender ct a, HasClient m api)
518− => HasClient m (ReqBody (ct ': cts) a :> api) where
520− type Client m (ReqBody (ct ': cts) a :> api) =
523− clientWithRoute pm Proxy req body =
524− clientWithRoute pm (Proxy :: Proxy api)
525− (let ctProxy = Proxy :: Proxy ct
526− in setRequestBodyLBS (mimeRender ctProxy body)
527− -- We use first contentType from the Accept list
528− (contentType ctProxy)
532−-- | Make the querying function append @path@ to the request path.
533−instance (KnownSymbol path, HasClient m api) => HasClient m (path :> api) where
534− type Client m (path :> api) = Client m api
536− clientWithRoute pm Proxy req =
537− clientWithRoute pm (Proxy :: Proxy api)
538− (appendToPath p req)
540− where p = pack $ symbolVal (Proxy :: Proxy path)
542−instance HasClient m api => HasClient m (Vault :> api) where
543− type Client m (Vault :> api) = Client m api
545− clientWithRoute pm Proxy req =
546− clientWithRoute pm (Proxy :: Proxy api) req
548−instance HasClient m api => HasClient m (RemoteHost :> api) where
549− type Client m (RemoteHost :> api) = Client m api
551− clientWithRoute pm Proxy req =
552− clientWithRoute pm (Proxy :: Proxy api) req
554−instance HasClient m api => HasClient m (IsSecure :> api) where
555− type Client m (IsSecure :> api) = Client m api
557− clientWithRoute pm Proxy req =
558− clientWithRoute pm (Proxy :: Proxy api) req
560−instance HasClient m subapi =>
561− HasClient m (WithNamedContext name context subapi) where
563− type Client m (WithNamedContext name context subapi) = Client m subapi
564− clientWithRoute pm Proxy = clientWithRoute pm (Proxy :: Proxy subapi)
566−instance ( HasClient m api
567− ) => HasClient m (AuthProtect tag :> api) where
568− type Client m (AuthProtect tag :> api)
569− = AuthenticateReq (AuthProtect tag) -> Client m api
571− clientWithRoute pm Proxy req (AuthenticateReq (val,func)) =
572− clientWithRoute pm (Proxy :: Proxy api) (func val req)
574−-- * Basic Authentication
576−instance HasClient m api => HasClient m (BasicAuth realm usr :> api) where
577− type Client m (BasicAuth realm usr :> api) = BasicAuthData -> Client m api
579− clientWithRoute pm Proxy req val =
580− clientWithRoute pm (Proxy :: Proxy api) (basicAuthReq val req)
583−{- Note [Non-Empty Content Types]
584−~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
587− instance (..., cts' ~ (ct ': cts)) => ... cts' ...
589−It may seem to make more sense to have:
591− instance (...) => ... (ct ': cts) ...
593−But this means that if another instance exists that does *not* require
594−non-empty lists, but is otherwise more specific, no instance will be overall
595−more specific. This in turn generally means adding yet another instance (one
596−for empty and one for non-empty lists).