git.iko.soy/typeable/servant
on master help sign in
Review fixes
commit4e9c342c4e43c6ff9ca2ceb4e571b9e0ca6e4d0b authorJulian K. Arni <jkarni@gmail.com> dateThu 14 Sep 2017, 13:21 (9 years ago) parent30528a8
3 files changed+6−539
servant-client-core/src/Servant/Client/Core.hs+4−537 servant-client-core/src/Servant/Client/Core/Reexport.hs+1−1 stack-ghc-8.2.1.yaml+1−1
servant-client-core/src/Servant/Client/Core.hs+4−537
@@ -2,8 +2,8 @@
22-- from @servant@ APIs. By "backend," we mean something that concretely
33-- executes the request, such as:
44--
5−-- * The 'http-client' library
6−-- * The 'haxl' library
5+-- * The @http-client@ library
6+-- * The @haxl@ library
77-- * GHCJS via FFI
88--
99-- etc.
@@ -11,8 +11,8 @@
1111-- Each backend is encapsulated in a monad that is an instance of the
1212-- 'RunClient' class.
1313--
14−-- This library is primarily of interest to backend-writers, who are encouraged
15−-- to re-export the parts of the
14+-- This library is primarily of interest to backend-writers and
15+-- combinator-writers. For more information, see the README.md
1616module Servant.Client.Core
1717 (
1818 -- * Client generation
@@ -63,536 +63,3 @@ import Servant.Client.Core.Internal.HasClient
6363import Servant.Client.Core.Internal.Generic
6464import Servant.Client.Core.Internal.Request
6565import Servant.Client.Core.Internal.RunClient
66−
67−{-
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
76−import Prelude ()
77−import Prelude.Compat
78−import Servant.API ((:<|>) ((:<|>)), (:>),
79− AuthProtect, BasicAuth,
80− BasicAuthData,
81− BuildHeadersTo (..),
82− Capture, CaptureAll,
83− Description, EmptyAPI,
84− Header, Headers (..),
85− HttpVersion, IsSecure,
86− MimeRender (mimeRender),
87− MimeUnrender (mimeUnrender),
88− NoContent (NoContent),
89− QueryFlag, QueryParam,
90− QueryParams, Raw,
91− ReflectMethod (..),
92− RemoteHost, ReqBody,
93− Summary, ToHttpApiData,
94− Vault, Verb,
95− WithNamedContext,
96− contentType,
97− getHeadersHList,
98− getResponse,
99− toQueryParam,
100− toUrlPiece)
101−import Servant.API.ContentTypes (contentTypes)
102−
103−import Servant.Client.Core.Internal.Auth
104−import Servant.Client.Core.Internal.BaseUrl (BaseUrl (..),
105− InvalidBaseUrlException,
106− Scheme (..),
107− parseBaseUrl,
108− showBaseUrl)
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
113−
114−-- * Accessing APIs as a Client
115−
116−-- | 'clientIn' allows you to produce operations to query an API from a client
117−-- within a 'RunClient' monad.
118−--
119−-- > type MyApi = "books" :> Get '[JSON] [Book] -- GET /books
120−-- > :<|> "books" :> ReqBody '[JSON] Book :> Post '[JSON] Book -- POST /books
121−-- >
122−-- > myApi :: Proxy MyApi
123−-- > myApi = Proxy
124−-- >
125−-- > clientM :: Proxy ClientM
126−-- > clientM = Proxy
127−-- >
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
133−
134−
135−-- | This class lets us define how each API combinator influences the creation
136−-- of an HTTP request.
137−--
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
140−-- class.
141−class RunClient m => HasClient m api where
142− type Client (m :: * -> *) (api :: *) :: *
143− clientWithRoute :: Proxy m -> Proxy api -> Request -> Client m api
144−
145−
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.
149−--
150−-- > type MyApi = "books" :> Get '[JSON] [Book] -- GET /books
151−-- > :<|> "books" :> ReqBody '[JSON] Book :> Post Book -- POST /books
152−-- >
153−-- > myApi :: Proxy MyApi
154−-- > myApi = Proxy
155−-- >
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
164−
165−-- | Singleton type representing a client for an empty API.
166−data EmptyClient = EmptyClient deriving (Eq, Show, Bounded, Enum)
167−
168−-- | The client for 'EmptyAPI' is simply 'EmptyClient'.
169−--
170−-- > type MyAPI = "books" :> Get '[JSON] [Book] -- GET /books
171−-- > :<|> "nothing" :> EmptyAPI
172−-- >
173−-- > myApi :: Proxy MyApi
174−-- > myApi = Proxy
175−-- >
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
181−
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.
187−--
188−-- You can control how values for this type are turned into
189−-- text by specifying a 'ToHttpApiData' instance for your type.
190−--
191−-- Example:
192−--
193−-- > type MyApi = "books" :> Capture "isbn" Text :> Get '[JSON] Book
194−-- >
195−-- > myApi :: Proxy MyApi
196−-- > myApi = Proxy
197−-- >
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
203−
204− type Client m (Capture capture a :> api) =
205− a -> Client m api
206−
207− clientWithRoute pm Proxy req val =
208− clientWithRoute pm (Proxy :: Proxy api)
209− (appendToPath p req)
210−
211− where p = (toUrlPiece val)
212−
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
218−-- path.
219−--
220−-- You can control how these values are turned into text by specifying
221−-- a 'ToHttpApiData' instance of your type.
222−--
223−-- Example:
224−--
225−-- > type MyAPI = "src" :> CaptureAll Text -> Get '[JSON] SourceFile
226−-- >
227−-- > myApi :: Proxy
228−-- > myApi = Proxy
229−--
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
235−
236− type Client m (CaptureAll capture a :> sublayout) =
237− [a] -> Client m sublayout
238−
239− clientWithRoute pm Proxy req vals =
240− clientWithRoute pm (Proxy :: Proxy sublayout)
241− (foldl' (flip appendToPath) req ps)
242−
243− where ps = map (toUrlPiece) vals
244−
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
254− }
255− response `decodedAs` (Proxy :: Proxy ct)
256− where
257− accept = contentTypes (Proxy :: Proxy ct)
258− method = reflectMethod (Proxy :: Proxy method)
259−
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)
264− = m NoContent
265− clientWithRoute _pm Proxy req = do
266− _response <- runRequest req { requestMethod = method }
267− return NoContent
268− where method = reflectMethod (Proxy :: Proxy method)
269−
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))
276− = m (Headers ls a)
277− clientWithRoute _pm Proxy req = do
278− response <- runRequest req
279− { requestMethod = method
280− , requestAccept = fromList $ toList accept
281− }
282− case mimeUnrender (Proxy :: Proxy ct) $ responseBody response of
283− Left err -> throwError $ DecodeFailure (pack err) response
284− Right val -> return $ Headers
285− { getResponse = val
286− , getHeadersHList = buildHeadersTo . toList $ responseHeaders response
287− }
288− where method = reflectMethod (Proxy :: Proxy method)
289− accept = contentTypes (Proxy :: Proxy ct)
290−
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
301− }
302−
303−
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',
307−-- wrapped in Maybe.
308−--
309−-- That function will take care of encoding this argument as Text
310−-- in the request headers.
311−--
312−-- All you need is for your type to have a 'ToHttpApiData' instance.
313−--
314−-- Example:
315−--
316−-- > newtype Referer = Referer { referrer :: Text }
317−-- > deriving (Eq, Show, Generic, ToHttpApiData)
318−-- >
319−-- > -- GET /view-my-referer
320−-- > type MyApi = "view-my-referer" :> Header "Referer" Referer :> Get '[JSON] Referer
321−-- >
322−-- > myApi :: Proxy MyApi
323−-- > myApi = Proxy
324−-- >
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
331−
332− type Client m (Header sym a :> api) =
333− Maybe a -> Client m api
334−
335− clientWithRoute pm Proxy req mval =
336− clientWithRoute pm (Proxy :: Proxy api)
337− (maybe req
338− (\value -> addHeader hname value req)
339− mval
340− )
341−
342− where hname = fromString $ symbolVal (Proxy :: Proxy sym)
343−
344−-- | Using a 'HttpVersion' combinator in your API doesn't affect the client
345−-- functions.
346−instance HasClient m api
347− => HasClient m (HttpVersion :> api) where
348−
349− type Client m (HttpVersion :> api) =
350− Client m api
351−
352− clientWithRoute pm Proxy =
353− clientWithRoute pm (Proxy :: Proxy api)
354−
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
358−
359− clientWithRoute pm _ = clientWithRoute pm (Proxy :: Proxy api)
360−
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
364−
365− clientWithRoute pm _ = clientWithRoute pm (Proxy :: Proxy api)
366−
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.
371−--
372−-- If you give Nothing, nothing will be added to the query string.
373−--
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.
376−--
377−-- You can control how values for your type are turned into
378−-- text by specifying a 'ToHttpApiData' instance for your type.
379−--
380−-- Example:
381−--
382−-- > type MyApi = "books" :> QueryParam "author" Text :> Get '[JSON] [Book]
383−-- >
384−-- > myApi :: Proxy MyApi
385−-- > myApi = Proxy
386−-- >
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
394−
395− type Client m (QueryParam sym a :> api) =
396− Maybe a -> Client m api
397−
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)
401− (maybe req
402− (flip (appendToQueryString pname) req . Just)
403− mparamText
404− )
405−
406− where pname = pack $ symbolVal (Proxy :: Proxy sym)
407− mparamText = fmap toQueryParam mparam
408−
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'.
413−--
414−-- If you give an empty list, nothing will be added to the query string.
415−--
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.
419−--
420−-- You can control how values for your type are turned into
421−-- text by specifying a 'ToHttpApiData' instance for your type.
422−--
423−-- Example:
424−--
425−-- > type MyApi = "books" :> QueryParams "authors" Text :> Get '[JSON] [Book]
426−-- >
427−-- > myApi :: Proxy MyApi
428−-- > myApi = Proxy
429−-- >
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
438−
439− type Client m (QueryParams sym a :> api) =
440− [a] -> Client m api
441−
442− clientWithRoute pm Proxy req paramlist =
443− clientWithRoute pm (Proxy :: Proxy api)
444− (foldl' (\ req' -> maybe req' (flip (appendToQueryString pname) req' . Just))
445− req
446− paramlist'
447− )
448−
449− where pname = pack $ symbolVal (Proxy :: Proxy sym)
450− paramlist' = map (Just . toQueryParam) paramlist
451−
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.
455−--
456−-- If you give 'False', nothing will be added to the query string.
457−--
458−-- Otherwise, this function will insert a value-less query string
459−-- parameter under the name associated to your 'QueryFlag'.
460−--
461−-- Example:
462−--
463−-- > type MyApi = "books" :> QueryFlag "published" :> Get '[JSON] [Book]
464−-- >
465−-- > myApi :: Proxy MyApi
466−-- > myApi = Proxy
467−-- >
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
475−
476− type Client m (QueryFlag sym :> api) =
477− Bool -> Client m api
478−
479− clientWithRoute pm Proxy req flag =
480− clientWithRoute pm (Proxy :: Proxy api)
481− (if flag
482− then appendToQueryString paramname Nothing req
483− else req
484− )
485−
486− where paramname = pack $ symbolVal (Proxy :: Proxy sym)
487−
488−
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
492− type Client m Raw
493− = H.Method -> m Response
494−
495− clientWithRoute :: Proxy m -> Proxy Raw -> Request -> Client m Raw
496− clientWithRoute _pm Proxy req httpMethod = do
497− runRequest req { requestMethod = httpMethod }
498−
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.
504−--
505−-- All you need is for your type to have a 'ToJSON' instance.
506−--
507−-- Example:
508−--
509−-- > type MyApi = "books" :> ReqBody '[JSON] Book :> Post '[JSON] Book
510−-- >
511−-- > myApi :: Proxy MyApi
512−-- > myApi = Proxy
513−-- >
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
519−
520− type Client m (ReqBody (ct ': cts) a :> api) =
521− a -> Client m api
522−
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)
529− req
530− )
531−
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
535−
536− clientWithRoute pm Proxy req =
537− clientWithRoute pm (Proxy :: Proxy api)
538− (appendToPath p req)
539−
540− where p = pack $ symbolVal (Proxy :: Proxy path)
541−
542−instance HasClient m api => HasClient m (Vault :> api) where
543− type Client m (Vault :> api) = Client m api
544−
545− clientWithRoute pm Proxy req =
546− clientWithRoute pm (Proxy :: Proxy api) req
547−
548−instance HasClient m api => HasClient m (RemoteHost :> api) where
549− type Client m (RemoteHost :> api) = Client m api
550−
551− clientWithRoute pm Proxy req =
552− clientWithRoute pm (Proxy :: Proxy api) req
553−
554−instance HasClient m api => HasClient m (IsSecure :> api) where
555− type Client m (IsSecure :> api) = Client m api
556−
557− clientWithRoute pm Proxy req =
558− clientWithRoute pm (Proxy :: Proxy api) req
559−
560−instance HasClient m subapi =>
561− HasClient m (WithNamedContext name context subapi) where
562−
563− type Client m (WithNamedContext name context subapi) = Client m subapi
564− clientWithRoute pm Proxy = clientWithRoute pm (Proxy :: Proxy subapi)
565−
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
570−
571− clientWithRoute pm Proxy req (AuthenticateReq (val,func)) =
572− clientWithRoute pm (Proxy :: Proxy api) (func val req)
573−
574−-- * Basic Authentication
575−
576−instance HasClient m api => HasClient m (BasicAuth realm usr :> api) where
577− type Client m (BasicAuth realm usr :> api) = BasicAuthData -> Client m api
578−
579− clientWithRoute pm Proxy req val =
580− clientWithRoute pm (Proxy :: Proxy api) (basicAuthReq val req)
581−
582−
583−{- Note [Non-Empty Content Types]
584−~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
585−Rather than have
586−
587− instance (..., cts' ~ (ct ': cts)) => ... cts' ...
588−
589−It may seem to make more sense to have:
590−
591− instance (...) => ... (ct ': cts) ...
592−
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).
597−-}
598−-}
servant-client-core/src/Servant/Client/Core/Reexport.hs+1−1
@@ -1,5 +1,5 @@
11-- | This module is a utility for @servant-client-core@ backend writers. It
2−-- contains all the functionality fron @servant-client-core@ that should be
2+-- contains all the functionality from @servant-client-core@ that should be
33-- re-exported.
44module Servant.Client.Core.Reexport
55 (
stack-ghc-8.2.1.yaml+1−1
@@ -1,4 +1,4 @@
1−resolver: nightly-2017-09-13
1+resolver: nightly-2017-09-01
22packages:
33- servant-client/
44- servant-client-core/