git.iko.soy/typeable/servant
on master help sign in
Documentation and Reexport module
commite8b9814168432237ec293f4b320703e5fd1625df authorJulian K. Arni <jkarni@gmail.com> dateWed 13 Sep 2017, 12:36 (9 years ago) parent6be78e0
10 files changed+627−38
servant-client-core/servant-client-core.cabal+3−1 servant-client-core/src/Servant/Client/Core.hs+37−23 servant-client-core/src/Servant/Client/Core/Internal/Auth.hs+5−5 servant-client-core/src/Servant/Client/Core/Internal/HasClient.hs+541 servant-client-core/src/Servant/Client/Core/Internal/Class.hs → servant-client-core/src/Servant/Client/Core/Internal/RunClient.hs+1−1 servant-client-core/src/Servant/Client/Core/Reexport.hs+30 servant-client/servant-client.cabal+1 servant-client/src/Servant/Client.hs+2−2 servant-client/src/Servant/Client/Internal/HttpClient.hs+1−2 servant-client/test/Servant/ClientSpec.hs+6−4
servant-client-core/servant-client-core.cabal+3−1
@@ -24,12 +24,14 @@ source-repository head
2424library
2525 exposed-modules:
2626 Servant.Client.Core
27+ Servant.Client.Core.Reexport
2728 Servant.Client.Core.Internal.Auth
2829 Servant.Client.Core.Internal.BaseUrl
2930 Servant.Client.Core.Internal.BasicAuth
30− Servant.Client.Core.Internal.Class
3131 Servant.Client.Core.Internal.Generic
32+ Servant.Client.Core.Internal.HasClient
3233 Servant.Client.Core.Internal.Request
34+ Servant.Client.Core.Internal.RunClient
3335 build-depends:
3436 base >= 4.7 && < 4.11
3537 , base-compat >= 0.9.1 && < 0.10
servant-client-core/src/Servant/Client/Core.hs+37−23
@@ -1,29 +1,33 @@
1−{-# LANGUAGE CPP #-}
2−{-# LANGUAGE DataKinds #-}
3−{-# LANGUAGE FlexibleContexts #-}
4−{-# LANGUAGE FlexibleInstances #-}
5−{-# LANGUAGE InstanceSigs #-}
6−{-# LANGUAGE MultiParamTypeClasses #-}
7−{-# LANGUAGE OverloadedStrings #-}
8−{-# LANGUAGE PolyKinds #-}
9−{-# LANGUAGE ScopedTypeVariables #-}
10−{-# LANGUAGE TypeFamilies #-}
11−{-# LANGUAGE TypeOperators #-}
12−{-# LANGUAGE UndecidableInstances #-}
13−
14−#include "overlapping-compat.h"
15−-- | This module provides 'client' which can automatically generate
16−-- querying functions for each endpoint just from the type representing your
17−-- API.
1+-- | This module provides backend-agnostic functionality for generating clients
2+-- from @servant@ APIs. By "backend," we mean something that concretely
3+-- executes the request, such as:
4+--
5+-- * The 'http-client' library
6+-- * The 'haxl' library
7+-- * GHCJS via FFI
8+--
9+-- etc.
10+--
11+-- Each backend is encapsulated in a monad that is an instance of the
12+-- 'RunClient' class.
13+--
14+-- This library is primarily of interest to backend-writers, who are encouraged
15+-- to re-export the parts of the
1816module Servant.Client.Core
1917 (
2018 -- * Client generation
2119 clientIn
2220 , HasClient(..)
2321
22+ -- * Request
23+ , Request(..)
24+ , defaultRequest
25+ , RequestBody(..)
26+
2427 -- * Authentication
25− , mkAuthenticateReq
26− , AuthenticateReq(..)
28+ , mkAuthenticatedRequest
29+ , basicAuthReq
30+ , AuthenticatedRequest(..)
2731 , AuthClientData
2832
2933 -- * Generic Client
@@ -33,10 +37,6 @@ module Servant.Client.Core
3337 , ServantError(..)
3438 , EmptyClient(..)
3539
36− -- * Request
37− , Request(..)
38− , defaultRequest
39− , RequestBody(..)
4040
4141 -- * Response
4242 , Response(..)
@@ -44,13 +44,26 @@ module Servant.Client.Core
4444 , module Servant.Client.Core.Internal.BaseUrl
4545
4646 -- * Writing HasClient instances
47+ -- | These functions need not be re-exported by backend libraries.
4748 , addHeader
4849 , appendToQueryString
4950 , appendToPath
5051 , setRequestBodyLBS
5152 , setRequestBody
5253 ) where
54+import Servant.Client.Core.Internal.Auth
55+import Servant.Client.Core.Internal.BaseUrl (BaseUrl (..),
56+ InvalidBaseUrlException,
57+ Scheme (..),
58+ parseBaseUrl,
59+ showBaseUrl)
60+import Servant.Client.Core.Internal.BasicAuth
61+import Servant.Client.Core.Internal.HasClient
62+import Servant.Client.Core.Internal.Generic
63+import Servant.Client.Core.Internal.Request
64+import Servant.Client.Core.Internal.RunClient
5365
66+{-
5467import Control.Monad.Error.Class (throwError)
5568import Data.List (foldl')
5669import Data.Proxy (Proxy (Proxy))
@@ -581,3 +594,4 @@ non-empty lists, but is otherwise more specific, no instance will be overall
581594more specific. This in turn generally means adding yet another instance (one
582595for empty and one for non-empty lists).
583596-}
597+-}
servant-client-core/src/Servant/Client/Core/Internal/Auth.hs+5−5
@@ -20,13 +20,13 @@ type family AuthClientData a :: *
2020-- data to a request
2121--
2222-- NOTE: THIS API IS EXPERIMENTAL AND SUBJECT TO CHANGE
23−newtype AuthenticateReq a =
24− AuthenticateReq { unAuthReq :: (AuthClientData a, AuthClientData a -> Request -> Request) }
23+newtype AuthenticatedRequest a =
24+ AuthenticatedRequest { unAuthReq :: (AuthClientData a, AuthClientData a -> Request -> Request) }
2525
2626-- | Handy helper to avoid wrapping datatypes in tuples everywhere.
2727--
2828-- NOTE: THIS API IS EXPERIMENTAL AND SUBJECT TO CHANGE
29−mkAuthenticateReq :: AuthClientData a
29+mkAuthenticatedRequest :: AuthClientData a
3030 -> (AuthClientData a -> Request -> Request)
31− -> AuthenticateReq a
32−mkAuthenticateReq val func = AuthenticateReq (val, func)
31+ -> AuthenticatedRequest a
32+mkAuthenticatedRequest val func = AuthenticatedRequest (val, func)
servant-client-core/src/Servant/Client/Core/Internal/HasClient.hsnew file+541
@@ -0,0 +1,541 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE FlexibleContexts #-}
4+{-# LANGUAGE FlexibleInstances #-}
5+{-# LANGUAGE InstanceSigs #-}
6+{-# LANGUAGE MultiParamTypeClasses #-}
7+{-# LANGUAGE OverloadedStrings #-}
8+{-# LANGUAGE PolyKinds #-}
9+{-# LANGUAGE ScopedTypeVariables #-}
10+{-# LANGUAGE TypeFamilies #-}
11+{-# LANGUAGE TypeOperators #-}
12+{-# LANGUAGE UndecidableInstances #-}
13+
14+#include "overlapping-compat.h"
15+module Servant.Client.Core.Internal.HasClient where
16+
17+
18+import Control.Monad.Error.Class (throwError)
19+import Data.List (foldl')
20+import Data.Proxy (Proxy (Proxy))
21+import Data.String (fromString)
22+import Data.Text (pack)
23+import GHC.Exts (fromList, toList)
24+import GHC.TypeLits (KnownSymbol, symbolVal)
25+import qualified Network.HTTP.Types as H
26+import Prelude ()
27+import Prelude.Compat
28+import Servant.API ((:<|>) ((:<|>)), (:>),
29+ AuthProtect, BasicAuth,
30+ BasicAuthData,
31+ BuildHeadersTo (..),
32+ Capture, CaptureAll,
33+ Description, EmptyAPI,
34+ Header, Headers (..),
35+ HttpVersion, IsSecure,
36+ MimeRender (mimeRender),
37+ MimeUnrender (mimeUnrender),
38+ NoContent (NoContent),
39+ QueryFlag, QueryParam,
40+ QueryParams, Raw,
41+ ReflectMethod (..),
42+ RemoteHost, ReqBody,
43+ Summary, ToHttpApiData,
44+ Vault, Verb,
45+ WithNamedContext,
46+ contentType,
47+ getHeadersHList,
48+ getResponse,
49+ toQueryParam,
50+ toUrlPiece)
51+import Servant.API.ContentTypes (contentTypes)
52+
53+import Servant.Client.Core.Internal.Auth
54+import Servant.Client.Core.Internal.BasicAuth
55+import Servant.Client.Core.Internal.Request
56+import Servant.Client.Core.Internal.RunClient
57+
58+-- * Accessing APIs as a Client
59+
60+-- | 'clientIn' allows you to produce operations to query an API from a client
61+-- within a 'RunClient' monad.
62+--
63+-- > type MyApi = "books" :> Get '[JSON] [Book] -- GET /books
64+-- > :<|> "books" :> ReqBody '[JSON] Book :> Post '[JSON] Book -- POST /books
65+-- >
66+-- > myApi :: Proxy MyApi
67+-- > myApi = Proxy
68+-- >
69+-- > clientM :: Proxy ClientM
70+-- > clientM = Proxy
71+-- >
72+-- > getAllBooks :: ClientM [Book]
73+-- > postNewBook :: Book -> ClientM Book
74+-- > (getAllBooks :<|> postNewBook) = myApi `clientIn` clientM
75+clientIn :: HasClient m api => Proxy api -> Proxy m -> Client m api
76+clientIn p pm = clientWithRoute pm p defaultRequest
77+
78+
79+-- | This class lets us define how each API combinator influences the creation
80+-- of an HTTP request.
81+--
82+-- Unless you are writing a new backend for @servant-client-core@ or new
83+-- combinators that you want to support client-generation, you can ignore this
84+-- class.
85+class RunClient m => HasClient m api where
86+ type Client (m :: * -> *) (api :: *) :: *
87+ clientWithRoute :: Proxy m -> Proxy api -> Request -> Client m api
88+
89+
90+-- | A client querying function for @a ':<|>' b@ will actually hand you
91+-- one function for querying @a@ and another one for querying @b@,
92+-- stitching them together with ':<|>', which really is just like a pair.
93+--
94+-- > type MyApi = "books" :> Get '[JSON] [Book] -- GET /books
95+-- > :<|> "books" :> ReqBody '[JSON] Book :> Post Book -- POST /books
96+-- >
97+-- > myApi :: Proxy MyApi
98+-- > myApi = Proxy
99+-- >
100+-- > getAllBooks :: ClientM [Book]
101+-- > postNewBook :: Book -> ClientM Book
102+-- > (getAllBooks :<|> postNewBook) = client myApi
103+instance (HasClient m a, HasClient m b) => HasClient m (a :<|> b) where
104+ type Client m (a :<|> b) = Client m a :<|> Client m b
105+ clientWithRoute pm Proxy req =
106+ clientWithRoute pm (Proxy :: Proxy a) req :<|>
107+ clientWithRoute pm (Proxy :: Proxy b) req
108+
109+-- | Singleton type representing a client for an empty API.
110+data EmptyClient = EmptyClient deriving (Eq, Show, Bounded, Enum)
111+
112+-- | The client for 'EmptyAPI' is simply 'EmptyClient'.
113+--
114+-- > type MyAPI = "books" :> Get '[JSON] [Book] -- GET /books
115+-- > :<|> "nothing" :> EmptyAPI
116+-- >
117+-- > myApi :: Proxy MyApi
118+-- > myApi = Proxy
119+-- >
120+-- > getAllBooks :: ClientM [Book]
121+-- > (getAllBooks :<|> EmptyClient) = client myApi
122+instance RunClient m => HasClient m EmptyAPI where
123+ type Client m EmptyAPI = EmptyClient
124+ clientWithRoute _pm Proxy _ = EmptyClient
125+
126+-- | If you use a 'Capture' in one of your endpoints in your API,
127+-- the corresponding querying function will automatically take
128+-- an additional argument of the type specified by your 'Capture'.
129+-- That function will take care of inserting a textual representation
130+-- of this value at the right place in the request path.
131+--
132+-- You can control how values for this type are turned into
133+-- text by specifying a 'ToHttpApiData' instance for your type.
134+--
135+-- Example:
136+--
137+-- > type MyApi = "books" :> Capture "isbn" Text :> Get '[JSON] Book
138+-- >
139+-- > myApi :: Proxy MyApi
140+-- > myApi = Proxy
141+-- >
142+-- > getBook :: Text -> ClientM Book
143+-- > getBook = client myApi
144+-- > -- then you can just use "getBook" to query that endpoint
145+instance (KnownSymbol capture, ToHttpApiData a, HasClient m api)
146+ => HasClient m (Capture capture a :> api) where
147+
148+ type Client m (Capture capture a :> api) =
149+ a -> Client m api
150+
151+ clientWithRoute pm Proxy req val =
152+ clientWithRoute pm (Proxy :: Proxy api)
153+ (appendToPath p req)
154+
155+ where p = (toUrlPiece val)
156+
157+-- | If you use a 'CaptureAll' in one of your endpoints in your API,
158+-- the corresponding querying function will automatically take an
159+-- additional argument of a list of the type specified by your
160+-- 'CaptureAll'. That function will take care of inserting a textual
161+-- representation of this value at the right place in the request
162+-- path.
163+--
164+-- You can control how these values are turned into text by specifying
165+-- a 'ToHttpApiData' instance of your type.
166+--
167+-- Example:
168+--
169+-- > type MyAPI = "src" :> CaptureAll Text -> Get '[JSON] SourceFile
170+-- >
171+-- > myApi :: Proxy
172+-- > myApi = Proxy
173+--
174+-- > getSourceFile :: [Text] -> ClientM SourceFile
175+-- > getSourceFile = client myApi
176+-- > -- then you can use "getSourceFile" to query that endpoint
177+instance (KnownSymbol capture, ToHttpApiData a, HasClient m sublayout)
178+ => HasClient m (CaptureAll capture a :> sublayout) where
179+
180+ type Client m (CaptureAll capture a :> sublayout) =
181+ [a] -> Client m sublayout
182+
183+ clientWithRoute pm Proxy req vals =
184+ clientWithRoute pm (Proxy :: Proxy sublayout)
185+ (foldl' (flip appendToPath) req ps)
186+
187+ where ps = map (toUrlPiece) vals
188+
189+instance OVERLAPPABLE_
190+ -- Note [Non-Empty Content Types]
191+ ( RunClient m, MimeUnrender ct a, ReflectMethod method, cts' ~ (ct ': cts)
192+ ) => HasClient m (Verb method status cts' a) where
193+ type Client m (Verb method status cts' a) = m a
194+ clientWithRoute _pm Proxy req = do
195+ response <- runRequest req
196+ { requestAccept = fromList $ toList accept
197+ , requestMethod = method
198+ }
199+ response `decodedAs` (Proxy :: Proxy ct)
200+ where
201+ accept = contentTypes (Proxy :: Proxy ct)
202+ method = reflectMethod (Proxy :: Proxy method)
203+
204+instance OVERLAPPING_
205+ ( RunClient m, ReflectMethod method
206+ ) => HasClient m (Verb method status cts NoContent) where
207+ type Client m (Verb method status cts NoContent)
208+ = m NoContent
209+ clientWithRoute _pm Proxy req = do
210+ _response <- runRequest req { requestMethod = method }
211+ return NoContent
212+ where method = reflectMethod (Proxy :: Proxy method)
213+
214+instance OVERLAPPING_
215+ -- Note [Non-Empty Content Types]
216+ ( RunClient m, MimeUnrender ct a, BuildHeadersTo ls
217+ , ReflectMethod method, cts' ~ (ct ': cts)
218+ ) => HasClient m (Verb method status cts' (Headers ls a)) where
219+ type Client m (Verb method status cts' (Headers ls a))
220+ = m (Headers ls a)
221+ clientWithRoute _pm Proxy req = do
222+ response <- runRequest req
223+ { requestMethod = method
224+ , requestAccept = fromList $ toList accept
225+ }
226+ case mimeUnrender (Proxy :: Proxy ct) $ responseBody response of
227+ Left err -> throwError $ DecodeFailure (pack err) response
228+ Right val -> return $ Headers
229+ { getResponse = val
230+ , getHeadersHList = buildHeadersTo . toList $ responseHeaders response
231+ }
232+ where method = reflectMethod (Proxy :: Proxy method)
233+ accept = contentTypes (Proxy :: Proxy ct)
234+
235+instance OVERLAPPING_
236+ ( RunClient m, BuildHeadersTo ls, ReflectMethod method
237+ ) => HasClient m (Verb method status cts (Headers ls NoContent)) where
238+ type Client m (Verb method status cts (Headers ls NoContent))
239+ = m (Headers ls NoContent)
240+ clientWithRoute _pm Proxy req = do
241+ let method = reflectMethod (Proxy :: Proxy method)
242+ response <- runRequest req { requestMethod = method }
243+ return $ Headers { getResponse = NoContent
244+ , getHeadersHList = buildHeadersTo . toList $ responseHeaders response
245+ }
246+
247+
248+-- | If you use a 'Header' in one of your endpoints in your API,
249+-- the corresponding querying function will automatically take
250+-- an additional argument of the type specified by your 'Header',
251+-- wrapped in Maybe.
252+--
253+-- That function will take care of encoding this argument as Text
254+-- in the request headers.
255+--
256+-- All you need is for your type to have a 'ToHttpApiData' instance.
257+--
258+-- Example:
259+--
260+-- > newtype Referer = Referer { referrer :: Text }
261+-- > deriving (Eq, Show, Generic, ToHttpApiData)
262+-- >
263+-- > -- GET /view-my-referer
264+-- > type MyApi = "view-my-referer" :> Header "Referer" Referer :> Get '[JSON] Referer
265+-- >
266+-- > myApi :: Proxy MyApi
267+-- > myApi = Proxy
268+-- >
269+-- > viewReferer :: Maybe Referer -> ClientM Book
270+-- > viewReferer = client myApi
271+-- > -- then you can just use "viewRefer" to query that endpoint
272+-- > -- specifying Nothing or e.g Just "http://haskell.org/" as arguments
273+instance (KnownSymbol sym, ToHttpApiData a, HasClient m api)
274+ => HasClient m (Header sym a :> api) where
275+
276+ type Client m (Header sym a :> api) =
277+ Maybe a -> Client m api
278+
279+ clientWithRoute pm Proxy req mval =
280+ clientWithRoute pm (Proxy :: Proxy api)
281+ (maybe req
282+ (\value -> addHeader hname value req)
283+ mval
284+ )
285+
286+ where hname = fromString $ symbolVal (Proxy :: Proxy sym)
287+
288+-- | Using a 'HttpVersion' combinator in your API doesn't affect the client
289+-- functions.
290+instance HasClient m api
291+ => HasClient m (HttpVersion :> api) where
292+
293+ type Client m (HttpVersion :> api) =
294+ Client m api
295+
296+ clientWithRoute pm Proxy =
297+ clientWithRoute pm (Proxy :: Proxy api)
298+
299+-- | Ignore @'Summary'@ in client functions.
300+instance HasClient m api => HasClient m (Summary desc :> api) where
301+ type Client m (Summary desc :> api) = Client m api
302+
303+ clientWithRoute pm _ = clientWithRoute pm (Proxy :: Proxy api)
304+
305+-- | Ignore @'Description'@ in client functions.
306+instance HasClient m api => HasClient m (Description desc :> api) where
307+ type Client m (Description desc :> api) = Client m api
308+
309+ clientWithRoute pm _ = clientWithRoute pm (Proxy :: Proxy api)
310+
311+-- | If you use a 'QueryParam' in one of your endpoints in your API,
312+-- the corresponding querying function will automatically take
313+-- an additional argument of the type specified by your 'QueryParam',
314+-- enclosed in Maybe.
315+--
316+-- If you give Nothing, nothing will be added to the query string.
317+--
318+-- If you give a non-'Nothing' value, this function will take care
319+-- of inserting a textual representation of this value in the query string.
320+--
321+-- You can control how values for your type are turned into
322+-- text by specifying a 'ToHttpApiData' instance for your type.
323+--
324+-- Example:
325+--
326+-- > type MyApi = "books" :> QueryParam "author" Text :> Get '[JSON] [Book]
327+-- >
328+-- > myApi :: Proxy MyApi
329+-- > myApi = Proxy
330+-- >
331+-- > getBooksBy :: Maybe Text -> ClientM [Book]
332+-- > getBooksBy = client myApi
333+-- > -- then you can just use "getBooksBy" to query that endpoint.
334+-- > -- 'getBooksBy Nothing' for all books
335+-- > -- 'getBooksBy (Just "Isaac Asimov")' to get all books by Isaac Asimov
336+instance (KnownSymbol sym, ToHttpApiData a, HasClient m api)
337+ => HasClient m (QueryParam sym a :> api) where
338+
339+ type Client m (QueryParam sym a :> api) =
340+ Maybe a -> Client m api
341+
342+ -- if mparam = Nothing, we don't add it to the query string
343+ clientWithRoute pm Proxy req mparam =
344+ clientWithRoute pm (Proxy :: Proxy api)
345+ (maybe req
346+ (flip (appendToQueryString pname) req . Just)
347+ mparamText
348+ )
349+
350+ where pname = pack $ symbolVal (Proxy :: Proxy sym)
351+ mparamText = fmap toQueryParam mparam
352+
353+-- | If you use a 'QueryParams' in one of your endpoints in your API,
354+-- the corresponding querying function will automatically take
355+-- an additional argument, a list of values of the type specified
356+-- by your 'QueryParams'.
357+--
358+-- If you give an empty list, nothing will be added to the query string.
359+--
360+-- Otherwise, this function will take care
361+-- of inserting a textual representation of your values in the query string,
362+-- under the same query string parameter name.
363+--
364+-- You can control how values for your type are turned into
365+-- text by specifying a 'ToHttpApiData' instance for your type.
366+--
367+-- Example:
368+--
369+-- > type MyApi = "books" :> QueryParams "authors" Text :> Get '[JSON] [Book]
370+-- >
371+-- > myApi :: Proxy MyApi
372+-- > myApi = Proxy
373+-- >
374+-- > getBooksBy :: [Text] -> ClientM [Book]
375+-- > getBooksBy = client myApi
376+-- > -- then you can just use "getBooksBy" to query that endpoint.
377+-- > -- 'getBooksBy []' for all books
378+-- > -- 'getBooksBy ["Isaac Asimov", "Robert A. Heinlein"]'
379+-- > -- to get all books by Asimov and Heinlein
380+instance (KnownSymbol sym, ToHttpApiData a, HasClient m api)
381+ => HasClient m (QueryParams sym a :> api) where
382+
383+ type Client m (QueryParams sym a :> api) =
384+ [a] -> Client m api
385+
386+ clientWithRoute pm Proxy req paramlist =
387+ clientWithRoute pm (Proxy :: Proxy api)
388+ (foldl' (\ req' -> maybe req' (flip (appendToQueryString pname) req' . Just))
389+ req
390+ paramlist'
391+ )
392+
393+ where pname = pack $ symbolVal (Proxy :: Proxy sym)
394+ paramlist' = map (Just . toQueryParam) paramlist
395+
396+-- | If you use a 'QueryFlag' in one of your endpoints in your API,
397+-- the corresponding querying function will automatically take
398+-- an additional 'Bool' argument.
399+--
400+-- If you give 'False', nothing will be added to the query string.
401+--
402+-- Otherwise, this function will insert a value-less query string
403+-- parameter under the name associated to your 'QueryFlag'.
404+--
405+-- Example:
406+--
407+-- > type MyApi = "books" :> QueryFlag "published" :> Get '[JSON] [Book]
408+-- >
409+-- > myApi :: Proxy MyApi
410+-- > myApi = Proxy
411+-- >
412+-- > getBooks :: Bool -> ClientM [Book]
413+-- > getBooks = client myApi
414+-- > -- then you can just use "getBooks" to query that endpoint.
415+-- > -- 'getBooksBy False' for all books
416+-- > -- 'getBooksBy True' to only get _already published_ books
417+instance (KnownSymbol sym, HasClient m api)
418+ => HasClient m (QueryFlag sym :> api) where
419+
420+ type Client m (QueryFlag sym :> api) =
421+ Bool -> Client m api
422+
423+ clientWithRoute pm Proxy req flag =
424+ clientWithRoute pm (Proxy :: Proxy api)
425+ (if flag
426+ then appendToQueryString paramname Nothing req
427+ else req
428+ )
429+
430+ where paramname = pack $ symbolVal (Proxy :: Proxy sym)
431+
432+
433+-- | Pick a 'Method' and specify where the server you want to query is. You get
434+-- back the full `Response`.
435+instance RunClient m => HasClient m Raw where
436+ type Client m Raw
437+ = H.Method -> m Response
438+
439+ clientWithRoute :: Proxy m -> Proxy Raw -> Request -> Client m Raw
440+ clientWithRoute _pm Proxy req httpMethod = do
441+ runRequest req { requestMethod = httpMethod }
442+
443+-- | If you use a 'ReqBody' in one of your endpoints in your API,
444+-- the corresponding querying function will automatically take
445+-- an additional argument of the type specified by your 'ReqBody'.
446+-- That function will take care of encoding this argument as JSON and
447+-- of using it as the request body.
448+--
449+-- All you need is for your type to have a 'ToJSON' instance.
450+--
451+-- Example:
452+--
453+-- > type MyApi = "books" :> ReqBody '[JSON] Book :> Post '[JSON] Book
454+-- >
455+-- > myApi :: Proxy MyApi
456+-- > myApi = Proxy
457+-- >
458+-- > addBook :: Book -> ClientM Book
459+-- > addBook = client myApi
460+-- > -- then you can just use "addBook" to query that endpoint
461+instance (MimeRender ct a, HasClient m api)
462+ => HasClient m (ReqBody (ct ': cts) a :> api) where
463+
464+ type Client m (ReqBody (ct ': cts) a :> api) =
465+ a -> Client m api
466+
467+ clientWithRoute pm Proxy req body =
468+ clientWithRoute pm (Proxy :: Proxy api)
469+ (let ctProxy = Proxy :: Proxy ct
470+ in setRequestBodyLBS (mimeRender ctProxy body)
471+ -- We use first contentType from the Accept list
472+ (contentType ctProxy)
473+ req
474+ )
475+
476+-- | Make the querying function append @path@ to the request path.
477+instance (KnownSymbol path, HasClient m api) => HasClient m (path :> api) where
478+ type Client m (path :> api) = Client m api
479+
480+ clientWithRoute pm Proxy req =
481+ clientWithRoute pm (Proxy :: Proxy api)
482+ (appendToPath p req)
483+
484+ where p = pack $ symbolVal (Proxy :: Proxy path)
485+
486+instance HasClient m api => HasClient m (Vault :> api) where
487+ type Client m (Vault :> api) = Client m api
488+
489+ clientWithRoute pm Proxy req =
490+ clientWithRoute pm (Proxy :: Proxy api) req
491+
492+instance HasClient m api => HasClient m (RemoteHost :> api) where
493+ type Client m (RemoteHost :> api) = Client m api
494+
495+ clientWithRoute pm Proxy req =
496+ clientWithRoute pm (Proxy :: Proxy api) req
497+
498+instance HasClient m api => HasClient m (IsSecure :> api) where
499+ type Client m (IsSecure :> api) = Client m api
500+
501+ clientWithRoute pm Proxy req =
502+ clientWithRoute pm (Proxy :: Proxy api) req
503+
504+instance HasClient m subapi =>
505+ HasClient m (WithNamedContext name context subapi) where
506+
507+ type Client m (WithNamedContext name context subapi) = Client m subapi
508+ clientWithRoute pm Proxy = clientWithRoute pm (Proxy :: Proxy subapi)
509+
510+instance ( HasClient m api
511+ ) => HasClient m (AuthProtect tag :> api) where
512+ type Client m (AuthProtect tag :> api)
513+ = AuthenticatedRequest (AuthProtect tag) -> Client m api
514+
515+ clientWithRoute pm Proxy req (AuthenticatedRequest (val,func)) =
516+ clientWithRoute pm (Proxy :: Proxy api) (func val req)
517+
518+-- * Basic Authentication
519+
520+instance HasClient m api => HasClient m (BasicAuth realm usr :> api) where
521+ type Client m (BasicAuth realm usr :> api) = BasicAuthData -> Client m api
522+
523+ clientWithRoute pm Proxy req val =
524+ clientWithRoute pm (Proxy :: Proxy api) (basicAuthReq val req)
525+
526+
527+{- Note [Non-Empty Content Types]
528+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
529+Rather than have
530+
531+ instance (..., cts' ~ (ct ': cts)) => ... cts' ...
532+
533+It may seem to make more sense to have:
534+
535+ instance (...) => ... (ct ': cts) ...
536+
537+But this means that if another instance exists that does *not* require
538+non-empty lists, but is otherwise more specific, no instance will be overall
539+more specific. This in turn generally means adding yet another instance (one
540+for empty and one for non-empty lists).
541+-}
servant-client-core/src/Servant/Client/Core/Internal/Class.hs → servant-client-core/src/Servant/Client/Core/Internal/RunClient.hsrenamed+1−1
@@ -4,7 +4,7 @@
44{-# LANGUAGE TypeFamilies #-}
55{-# LANGUAGE OverloadedStrings #-}
66-- | Types for possible backends to run client-side `Request` queries
7−module Servant.Client.Core.Internal.Class where
7+module Servant.Client.Core.Internal.RunClient where
88
99import Control.Monad (unless)
1010import Control.Monad.Error.Class (MonadError, throwError)
servant-client-core/src/Servant/Client/Core/Reexport.hsnew file+30
@@ -0,0 +1,30 @@
1+-- | This module is a utility for @servant-client-core@ backend writers. It
2+-- contains all the functionality fron @servant-client-core@ that should be
3+-- re-exported.
4+module Servant.Client.Core.Reexport
5+ (
6+ -- * HasClient
7+ HasClient(..)
8+ -- * Response (for @Raw@)
9+ , Response(..)
10+
11+ -- * Generic Client
12+ , ClientLike(..)
13+ , genericMkClientL
14+ , genericMkClientP
15+ , ServantError(..)
16+ , EmptyClient(..)
17+
18+ -- * BaseUrl
19+ , BaseUrl(..)
20+ , Scheme(..)
21+ , showBaseUrl
22+ , parseBaseUrl
23+ , InvalidBaseUrlException
24+ ) where
25+
26+
27+import Servant.Client.Core.Internal.BaseUrl
28+import Servant.Client.Core.Internal.HasClient
29+import Servant.Client.Core.Internal.Generic
30+import Servant.Client.Core.Internal.Request
servant-client/servant-client.cabal+1
@@ -86,6 +86,7 @@ test-suite spec
8686 , QuickCheck >= 2.7
8787 , servant
8888 , servant-client
89+ , servant-client-core
8990 , servant-server == 0.11.*
9091 , text
9192 , transformers
servant-client/src/Servant/Client.hs+2−2
@@ -6,8 +6,8 @@ module Servant.Client
66 , ClientM
77 , runClientM
88 , ClientEnv(..)
9− , module Servant.Client.Core
9+ , module X
1010 ) where
1111
1212import Servant.Client.Internal.HttpClient
13−import Servant.Client.Core
13+import Servant.Client.Core.Reexport as X
servant-client/src/Servant/Client/Internal/HttpClient.hs+1−2
@@ -9,7 +9,7 @@
99{-# LANGUAGE ScopedTypeVariables #-}
1010{-# LANGUAGE TypeFamilies #-}
1111
12−{-| http-client based client requests executor -}
12+-- | @http-client@-based client requests executor
1313module Servant.Client.Internal.HttpClient where
1414
1515
@@ -96,7 +96,6 @@ runClientM :: ClientM a -> ClientEnv -> IO (Either ServantError a)
9696runClientM cm env = runExceptT $ (flip runReaderT env) $ runClientM' cm
9797
9898
99−
10099performRequest :: Request -> ClientM Response
101100performRequest req = do
102101 m <- asks manager
servant-client/test/Servant/ClientSpec.hs+6−4
@@ -73,6 +73,8 @@ import Servant.API ((:<|>) ((:<|>)),
7373 getHeaders)
7474import Servant.API.Internal.Test.ComprehensiveAPI
7575import Servant.Client
76+import qualified Servant.Client.Core.Internal.Request as Req
77+import qualified Servant.Client.Core.Internal.Auth as Auth
7678import Servant.Server
7779import Servant.Server.Experimental.Auth
7880
@@ -176,7 +178,7 @@ server = serve api (
176178 :<|> (Tagged $ \ _request respond -> respond $ Wai.responseLBS HTTP.ok200 [] "rawSuccess")
177179 :<|> (Tagged $ \ _request respond -> respond $ Wai.responseLBS HTTP.badRequest400 [] "rawFailure")
178180 :<|> (\ a b c d -> return (a, b, c, d))
179− :<|> (return $ Servant.API.addHeader 1729 $ Servant.API.addHeader "eg2" True)
181+ :<|> (return $ addHeader 1729 $ addHeader "eg2" True)
180182 :<|> return NoContent
181183 :<|> emptyServer)
182184
@@ -226,7 +228,7 @@ genAuthAPI :: Proxy GenAuthAPI
226228genAuthAPI = Proxy
227229
228230type instance AuthServerData (AuthProtect "auth-tag") = ()
229−type instance AuthClientData (AuthProtect "auth-tag") = ()
231+type instance Auth.AuthClientData (AuthProtect "auth-tag") = ()
230232
231233genAuthHandler :: AuthHandler Wai.Request ()
232234genAuthHandler =
@@ -450,14 +452,14 @@ genAuthSpec = beforeAll (startWaiApp genAuthServer) $ afterAll endWaiApp $ do
450452
451453 it "Authenticates a AuthProtect protected server appropriately" $ \(_, baseUrl) -> do
452454 let getProtected = client genAuthAPI
453− let authRequest = mkAuthenticateReq () (\_ req -> Servant.Client.addHeader "AuthHeader" ("cool" :: String) req)
455+ let authRequest = Auth.mkAuthenticatedRequest () (\_ req -> Req.addHeader "AuthHeader" ("cool" :: String) req)
454456 left show <$> runClient (getProtected authRequest) baseUrl `shouldReturn` Right alice
455457
456458 context "Authentication is rejected when requests are not authenticated properly" $ do
457459
458460 it "Authenticates a AuthProtect protected server appropriately" $ \(_, baseUrl) -> do
459461 let getProtected = client genAuthAPI
460− let authRequest = mkAuthenticateReq () (\_ req -> Servant.Client.addHeader "Wrong" ("header" :: String) req)
462+ let authRequest = Auth.mkAuthenticatedRequest () (\_ req -> Req.addHeader "Wrong" ("header" :: String) req)
461463 Left (FailureResponse r) <- runClient (getProtected authRequest) baseUrl
462464 responseStatusCode r `shouldBe` (HTTP.Status 401 "Unauthorized")
463465