@@ -1,5 +1,4 @@
1{-# LANGUAGE ConstraintKinds #-}
1{-# LANGUAGE ConstraintKinds #-}
3{-# LANGUAGE DataKinds #-}
2{-# LANGUAGE DataKinds #-}
4{-# LANGUAGE FlexibleContexts #-}
3{-# LANGUAGE FlexibleContexts #-}
5{-# LANGUAGE FlexibleInstances #-}
4{-# LANGUAGE FlexibleInstances #-}
@@ -7,6 +6,7 @@
7{-# LANGUAGE MultiParamTypeClasses #-}
6{-# LANGUAGE MultiParamTypeClasses #-}
8{-# LANGUAGE OverloadedStrings #-}
7{-# LANGUAGE OverloadedStrings #-}
9{-# LANGUAGE PolyKinds #-}
8{-# LANGUAGE PolyKinds #-}
9+{-# LANGUAGE QuantifiedConstraints #-}
10{-# LANGUAGE RankNTypes #-}
10{-# LANGUAGE RankNTypes #-}
11{-# LANGUAGE ScopedTypeVariables #-}
11{-# LANGUAGE ScopedTypeVariables #-}
12{-# LANGUAGE TypeApplications #-}
12{-# LANGUAGE TypeApplications #-}
@@ -14,14 +14,13 @@
14{-# LANGUAGE TypeOperators #-}
14{-# LANGUAGE TypeOperators #-}
15{-# LANGUAGE UndecidableInstances #-}
15{-# LANGUAGE UndecidableInstances #-}
17−#if MIN_VERSION_base(4,9,0) && __GLASGOW_HASKELL__ >= 802
18−#define HAS_TYPE_ERROR
21module Servant.Client.Core.HasClient (
17module Servant.Client.Core.HasClient (
23 HasClient (..),
19 HasClient (..),
24 EmptyClient (..),
20 EmptyClient (..),
25 foldMapUnion,
24 foldMapUnion,
26 matchUnion,
25 matchUnion,
@@ -34,11 +33,10 @@ import Control.Arrow
34import Control.Monad
33import Control.Monad
36import qualified Data.ByteString as BS
35import qualified Data.ByteString as BS
37−import Data.ByteString.Builder
39−import qualified Data.ByteString.Lazy as BL
36+import qualified Data.ByteString.Lazy as BL
40import Data.Either
37import Data.Either
41 (partitionEithers)
38 (partitionEithers)
39+import Data.Constraint (Dict(..))
42import Data.Foldable
40import Data.Foldable
44import Data.List
42import Data.List
@@ -47,7 +45,8 @@ import Data.Sequence
47 (fromList)
45 (fromList)
48import qualified Data.Text as T
46import qualified Data.Text as T
49import Network.HTTP.Media
47import Network.HTTP.Media
50− (MediaType, matches, parseAccept, (//))
48+ (MediaType, matches, parseAccept)
49+import qualified Network.HTTP.Media as Media
51import qualified Data.Sequence as Seq
50import qualified Data.Sequence as Seq
52import Data.SOP.BasicFunctors
51import Data.SOP.BasicFunctors
53 (I (I), (:.:) (Comp))
52 (I (I), (:.:) (Comp))
@@ -64,7 +63,7 @@ import Data.Text
64import Data.Proxy
63import Data.Proxy
65 (Proxy (Proxy))
64 (Proxy (Proxy))
66import GHC.TypeLits
65import GHC.TypeLits
67− (KnownSymbol, symbolVal)
66+ (KnownNat, KnownSymbol, TypeError, symbolVal)
68import Network.HTTP.Types
67import Network.HTTP.Types
70import qualified Network.HTTP.Types as H
69import qualified Network.HTTP.Types as H
@@ -79,12 +78,18 @@ import Servant.API
79 ReflectMethod (..), RemoteHost, ReqBody', SBoolI, Stream,
78 ReflectMethod (..), RemoteHost, ReqBody', SBoolI, Stream,
80 StreamBody', Summary, ToHttpApiData, ToSourceIO (..), Vault,
79 StreamBody', Summary, ToHttpApiData, ToSourceIO (..), Vault,
81 Verb, WithNamedContext, WithStatus (..), contentType, getHeadersHList,
80 Verb, WithNamedContext, WithStatus (..), contentType, getHeadersHList,
82− getResponse, toEncodedUrlPiece, toUrlPiece)
81+ getResponse, toEncodedUrlPiece, toUrlPiece, NamedRoutes)
82+import Servant.API.Generic
83+ (GenericMode(..), ToServant, ToServantApi
84+ , GenericServant, toServant, fromServant)
83import Servant.API.ContentTypes
85import Servant.API.ContentTypes
84 (contentTypes, AllMime (allMime), AllMimeUnrender (allMimeUnrender))
86 (contentTypes, AllMime (allMime), AllMimeUnrender (allMimeUnrender))
87+import Servant.API.Status
85import Servant.API.TypeLevel (FragmentUnique, AtLeastOneFragment)
89import Servant.API.TypeLevel (FragmentUnique, AtLeastOneFragment)
86import Servant.API.Modifiers
90import Servant.API.Modifiers
87 (FoldRequired, RequiredArgument, foldRequiredArgument)
91 (FoldRequired, RequiredArgument, foldRequiredArgument)
92+import Servant.API.TypeErrors
88import Servant.API.UVerb
93import Servant.API.UVerb
89 (HasStatus, HasStatuses (Statuses, statuses), UVerb, Union, Unique, inject, statusOf, foldMapUnion, matchUnion)
94 (HasStatus, HasStatuses (Statuses, statuses), UVerb, Union, Unique, inject, statusOf, foldMapUnion, matchUnion)
@@ -203,7 +208,7 @@ instance (KnownSymbol capture, ToHttpApiData a, HasClient m api)
203 clientWithRoute pm (Proxy :: Proxy api)
208 clientWithRoute pm (Proxy :: Proxy api)
204 (appendToPath p req)
209 (appendToPath p req)
206− where p = (toUrlPiece val)
211+ where p = toEncodedUrlPiece val
208 hoistClientMonad pm _ f cl = \a ->
213 hoistClientMonad pm _ f cl = \a ->
209 hoistClientMonad pm (Proxy :: Proxy api) f (cl a)
214 hoistClientMonad pm (Proxy :: Proxy api) f (cl a)
@@ -238,7 +243,7 @@ instance (KnownSymbol capture, ToHttpApiData a, HasClient m sublayout)
238 clientWithRoute pm (Proxy :: Proxy sublayout)
243 clientWithRoute pm (Proxy :: Proxy sublayout)
239 (foldl' (flip appendToPath) req ps)
244 (foldl' (flip appendToPath) req ps)
241− where ps = map (toUrlPiece) vals
246+ where ps = map toEncodedUrlPiece vals
243 hoistClientMonad pm _ f cl = \as ->
248 hoistClientMonad pm _ f cl = \as ->
244 hoistClientMonad pm (Proxy :: Proxy sublayout) f (cl as)
249 hoistClientMonad pm (Proxy :: Proxy sublayout) f (cl as)
@@ -246,10 +251,11 @@ instance (KnownSymbol capture, ToHttpApiData a, HasClient m sublayout)
246instance {-# OVERLAPPABLE #-}
251instance {-# OVERLAPPABLE #-}
247 -- Note [Non-Empty Content Types]
252 -- Note [Non-Empty Content Types]
248 ( RunClient m, MimeUnrender ct a, ReflectMethod method, cts' ~ (ct ': cts)
253 ( RunClient m, MimeUnrender ct a, ReflectMethod method, cts' ~ (ct ': cts)
249 ) => HasClient m (Verb method status cts' a) where
255 ) => HasClient m (Verb method status cts' a) where
250 type Client m (Verb method status cts' a) = m a
256 type Client m (Verb method status cts' a) = m a
251 clientWithRoute _pm Proxy req = do
257 clientWithRoute _pm Proxy req = do
252− response <- runRequest req
258+ response <- runRequestAcceptStatus (Just [status]) req
253 { requestAccept = fromList $ toList accept
259 { requestAccept = fromList $ toList accept
254 , requestMethod = method
260 , requestMethod = method
@@ -257,18 +263,20 @@ instance {-# OVERLAPPABLE #-}
258 accept = contentTypes (Proxy :: Proxy ct)
264 accept = contentTypes (Proxy :: Proxy ct)
259 method = reflectMethod (Proxy :: Proxy method)
265 method = reflectMethod (Proxy :: Proxy method)
266+ status = statusFromNat (Proxy :: Proxy status)
261 hoistClientMonad _ _ f ma = f ma
268 hoistClientMonad _ _ f ma = f ma
263instance {-# OVERLAPPING #-}
270instance {-# OVERLAPPING #-}
264− ( RunClient m, ReflectMethod method
271+ ( RunClient m, ReflectMethod method, KnownNat status
265 ) => HasClient m (Verb method status cts NoContent) where
272 ) => HasClient m (Verb method status cts NoContent) where
266 type Client m (Verb method status cts NoContent)
273 type Client m (Verb method status cts NoContent)
267 = m NoContent
274 = m NoContent
268 clientWithRoute _pm Proxy req = do
275 clientWithRoute _pm Proxy req = do
269− _response <- runRequest req { requestMethod = method }
276+ _response <- runRequestAcceptStatus (Just [status]) req { requestMethod = method }
270 return NoContent
277 return NoContent
271 where method = reflectMethod (Proxy :: Proxy method)
278 where method = reflectMethod (Proxy :: Proxy method)
279+ status = statusFromNat (Proxy :: Proxy status)
273 hoistClientMonad _ _ f ma = f ma
281 hoistClientMonad _ _ f ma = f ma
@@ -285,13 +293,13 @@ instance (RunClient m, ReflectMethod method) =>
286instance {-# OVERLAPPING #-}
294instance {-# OVERLAPPING #-}
287 -- Note [Non-Empty Content Types]
295 -- Note [Non-Empty Content Types]
288− ( RunClient m, MimeUnrender ct a, BuildHeadersTo ls
296+ ( RunClient m, MimeUnrender ct a, BuildHeadersTo ls, KnownNat status
289 , ReflectMethod method, cts' ~ (ct ': cts)
297 , ReflectMethod method, cts' ~ (ct ': cts)
290 ) => HasClient m (Verb method status cts' (Headers ls a)) where
298 ) => HasClient m (Verb method status cts' (Headers ls a)) where
291 type Client m (Verb method status cts' (Headers ls a))
299 type Client m (Verb method status cts' (Headers ls a))
292 = m (Headers ls a)
300 = m (Headers ls a)
293 clientWithRoute _pm Proxy req = do
301 clientWithRoute _pm Proxy req = do
294− response <- runRequest req
302+ response <- runRequestAcceptStatus (Just [status]) req
295 { requestMethod = method
303 { requestMethod = method
296 , requestAccept = fromList $ toList accept
304 , requestAccept = fromList $ toList accept
@@ -299,22 +307,26 @@ instance {-# OVERLAPPING #-}
299 return $ Headers { getResponse = val
307 return $ Headers { getResponse = val
300 , getHeadersHList = buildHeadersTo . toList $ responseHeaders response
308 , getHeadersHList = buildHeadersTo . toList $ responseHeaders response
302− where method = reflectMethod (Proxy :: Proxy method)
311+ method = reflectMethod (Proxy :: Proxy method)
303− accept = contentTypes (Proxy :: Proxy ct)
312+ accept = contentTypes (Proxy :: Proxy ct)
313+ status = statusFromNat (Proxy :: Proxy status)
305 hoistClientMonad _ _ f ma = f ma
315 hoistClientMonad _ _ f ma = f ma
307instance {-# OVERLAPPING #-}
317instance {-# OVERLAPPING #-}
308− ( RunClient m, BuildHeadersTo ls, ReflectMethod method
318+ ( RunClient m, BuildHeadersTo ls, ReflectMethod method, KnownNat status
309 ) => HasClient m (Verb method status cts (Headers ls NoContent)) where
319 ) => HasClient m (Verb method status cts (Headers ls NoContent)) where
310 type Client m (Verb method status cts (Headers ls NoContent))
320 type Client m (Verb method status cts (Headers ls NoContent))
311 = m (Headers ls NoContent)
321 = m (Headers ls NoContent)
312 clientWithRoute _pm Proxy req = do
322 clientWithRoute _pm Proxy req = do
313− let method = reflectMethod (Proxy :: Proxy method)
314− response <- runRequest req { requestMethod = method }
323+ response <- runRequestAcceptStatus (Just [status]) req { requestMethod = method }
315 return $ Headers { getResponse = NoContent
324 return $ Headers { getResponse = NoContent
316 , getHeadersHList = buildHeadersTo . toList $ responseHeaders response
325 , getHeadersHList = buildHeadersTo . toList $ responseHeaders response
328+ method = reflectMethod (Proxy :: Proxy method)
329+ status = statusFromNat (Proxy :: Proxy status)
319 hoistClientMonad _ _ f ma = f ma
331 hoistClientMonad _ _ f ma = f ma
@@ -557,7 +569,7 @@ instance (KnownSymbol sym, ToHttpApiData a, HasClient m api, SBoolI (FoldRequire
557 (Proxy :: Proxy mods) add (maybe req add) mparam
569 (Proxy :: Proxy mods) add (maybe req add) mparam
559 add :: a -> Request
571 add :: a -> Request
560− add param = appendToQueryString pname (Just $ encodeQueryParam param) req
572+ add param = appendToQueryString pname (Just $ encodeQueryParamValue param) req
562 pname :: Text
574 pname :: Text
563 pname = pack $ symbolVal (Proxy :: Proxy sym)
575 pname = pack $ symbolVal (Proxy :: Proxy sym)
@@ -565,9 +577,6 @@ instance (KnownSymbol sym, ToHttpApiData a, HasClient m api, SBoolI (FoldRequire
565 hoistClientMonad pm _ f cl = \arg ->
577 hoistClientMonad pm _ f cl = \arg ->
566 hoistClientMonad pm (Proxy :: Proxy api) f (cl arg)
578 hoistClientMonad pm (Proxy :: Proxy api) f (cl arg)
568−encodeQueryParam :: ToHttpApiData a => a -> BS.ByteString
569−encodeQueryParam = BL.toStrict . toLazyByteString . toEncodedUrlPiece
571-- | If you use a 'QueryParams' in one of your endpoints in your API,
580-- | If you use a 'QueryParams' in one of your endpoints in your API,
572-- the corresponding querying function will automatically take
581-- the corresponding querying function will automatically take
573-- an additional argument, a list of values of the type specified
582-- an additional argument, a list of values of the type specified
@@ -609,7 +618,7 @@ instance (KnownSymbol sym, ToHttpApiData a, HasClient m api)
611 where pname = pack $ symbolVal (Proxy :: Proxy sym)
620 where pname = pack $ symbolVal (Proxy :: Proxy sym)
612− paramlist' = map (Just . encodeQueryParam) paramlist
621+ paramlist' = map (Just . encodeQueryParamValue) paramlist
614 hoistClientMonad pm _ f cl = \as ->
623 hoistClientMonad pm _ f cl = \as ->
615 hoistClientMonad pm (Proxy :: Proxy api) f (cl as)
624 hoistClientMonad pm (Proxy :: Proxy api) f (cl as)
@@ -731,7 +740,7 @@ instance (KnownSymbol path, HasClient m api) => HasClient m (path :> api) where
731 clientWithRoute pm (Proxy :: Proxy api)
740 clientWithRoute pm (Proxy :: Proxy api)
732 (appendToPath p req)
741 (appendToPath p req)
734− where p = pack $ symbolVal (Proxy :: Proxy path)
743+ where p = toEncodedUrlPiece $ pack $ symbolVal (Proxy :: Proxy path)
736 hoistClientMonad pm _ f cl = hoistClientMonad pm (Proxy :: Proxy api) f cl
745 hoistClientMonad pm _ f cl = hoistClientMonad pm (Proxy :: Proxy api) f cl
@@ -780,7 +789,7 @@ instance ( HasClient m api
781-- | Ignore @'Fragment'@ in client functions.
790-- | Ignore @'Fragment'@ in client functions.
782-- See <https://ietf.org/rfc/rfc2616.html#section-15.1.3> for more details.
791-- See <https://ietf.org/rfc/rfc2616.html#section-15.1.3> for more details.
784-- Example:
793-- Example:
786-- > type MyApi = "books" :> Fragment Text :> Get '[JSON] [Book]
795-- > type MyApi = "books" :> Fragment Text :> Get '[JSON] [Book]
@@ -792,16 +801,12 @@ instance ( HasClient m api
792-- > getBooks = client myApi
801-- > getBooks = client myApi
793-- > -- then you can just use "getBooksBy" to query that endpoint.
802-- > -- then you can just use "getBooksBy" to query that endpoint.
794-- > -- 'getBooks' for all books.
803-- > -- 'getBooks' for all books.
795−#ifdef HAS_TYPE_ERROR
796instance (AtLeastOneFragment api, FragmentUnique (Fragment a :> api), HasClient m api
804instance (AtLeastOneFragment api, FragmentUnique (Fragment a :> api), HasClient m api
798−instance ( HasClient m api
800 ) => HasClient m (Fragment a :> api) where
805 ) => HasClient m (Fragment a :> api) where
802 type Client m (Fragment a :> api) = Client m api
807 type Client m (Fragment a :> api) = Client m api
804− clientWithRoute pm _ = clientWithRoute pm (Proxy :: Proxy api)
809+ clientWithRoute pm _ = clientWithRoute pm (Proxy :: Proxy api)
806 hoistClientMonad pm _ = hoistClientMonad pm (Proxy :: Proxy api)
811 hoistClientMonad pm _ = hoistClientMonad pm (Proxy :: Proxy api)
@@ -816,6 +821,119 @@ instance HasClient m api => HasClient m (BasicAuth realm usr :> api) where
816 hoistClientMonad pm _ f cl = \bauth ->
821 hoistClientMonad pm _ f cl = \bauth ->
817 hoistClientMonad pm (Proxy :: Proxy api) f (cl bauth)
822 hoistClientMonad pm (Proxy :: Proxy api) f (cl bauth)
824+-- | A type that specifies that an API record contains a client implementation.
825+data AsClientT (m :: * -> *)
826+instance GenericMode (AsClientT m) where
827+ type AsClientT m :- api = Client m api
830+type GClientConstraints api m =
831+ ( GenericServant api (AsClientT m)
832+ , Client m (ToServantApi api) ~ ToServant api (AsClientT m)
835+class GClient (api :: * -> *) m where
836+ gClientProof :: Dict (GClientConstraints api m)
838+instance GClientConstraints api m => GClient api m where
842+ ( forall n. GClient api n
843+ , HasClient m (ToServantApi api)
846+ => HasClient m (NamedRoutes api) where
847+ type Client m (NamedRoutes api) = api (AsClientT m)
849+ clientWithRoute :: Proxy m -> Proxy (NamedRoutes api) -> Request -> Client m (NamedRoutes api)
850+ clientWithRoute pm _ request =
851+ case gClientProof @api @m of
852+ Dict -> fromServant $ clientWithRoute pm (Proxy @(ToServantApi api)) request
857+ -> Proxy (NamedRoutes api)
858+ -> (forall x. ma x -> mb x)
859+ -> Client ma (NamedRoutes api)
860+ -> Client mb (NamedRoutes api)
861+ hoistClientMonad _ _ nat clientA =
862+ case (gClientProof @api @ma, gClientProof @api @mb) of
864+ fromServant @api @(AsClientT mb) $
865+ hoistClientMonad @m @(ToServantApi api) @ma @mb Proxy Proxy nat $
866+ toServant @api @(AsClientT ma) clientA
871+-- | Helper to make code using records of clients more readable.
873+-- Can be mixed with (/:) for supplying arguments.
878+-- type Api = NamedRoutes RootApi
880+-- data RootApi mode = RootApi
881+-- { subApi :: mode :- NamedRoutes SubApi
883+-- } deriving Generic
885+-- data SubApi mode = SubApi
886+-- { endpoint :: mode :- Get '[JSON] Person
888+-- } deriving Generic
893+-- rootClient :: RootApi (AsClientT ClientM)
894+-- rootClient = client api
896+-- endpointClient :: ClientM Person
897+-- endpointClient = client // subApi // endpoint
899+(//) :: a -> (a -> b) -> b
902+-- | Convenience function for supplying arguments to client functions when
903+-- working with records of clients.
905+-- Intended to be used in conjunction with '(//)'.
910+-- type Api = NamedRoutes RootApi
912+-- data RootApi mode = RootApi
913+-- { subApi :: mode :- Capture "token" String :> NamedRoutes SubApi
914+-- , hello :: mode :- Capture "name" String :> Get '[JSON] String
916+-- } deriving Generic
918+-- data SubApi mode = SubApi
919+-- { endpoint :: mode :- Get '[JSON] Person
921+-- } deriving Generic
926+-- rootClient :: RootApi (AsClientT ClientM)
927+-- rootClient = client api
929+-- hello :: String -> ClientM String
930+-- hello name = rootClient // hello /: name
932+-- endpointClient :: ClientM Person
933+-- endpointClient = client // subApi /: "foobar123" // endpoint
935+(/:) :: (a -> b -> c) -> b -> a -> c
821{- Note [Non-Empty Content Types]
939{- Note [Non-Empty Content Types]
@@ -841,7 +959,7 @@ for empty and one for non-empty lists).
841checkContentTypeHeader :: RunClient m => Response -> m MediaType
959checkContentTypeHeader :: RunClient m => Response -> m MediaType
842checkContentTypeHeader response =
960checkContentTypeHeader response =
843 case lookup "Content-Type" $ toList $ responseHeaders response of
961 case lookup "Content-Type" $ toList $ responseHeaders response of
844− Nothing -> return $ "application"//"octet-stream"
962+ Nothing -> return $ "application" Media.// "octet-stream"
845 Just t -> case parseAccept t of
963 Just t -> case parseAccept t of
846 Nothing -> throwClientError $ InvalidContentTypeHeader response
964 Nothing -> throwClientError $ InvalidContentTypeHeader response
847 Just t' -> return t'
965 Just t' -> return t'
@@ -857,3 +975,19 @@ decodedAs response ct = do
857 Right val -> return val
975 Right val -> return val
859 accept = toList $ contentTypes ct
977 accept = toList $ contentTypes ct
979+-------------------------------------------------------------------------------
980+-- Custom type errors
981+-------------------------------------------------------------------------------
983+-- Erroring instance for HasClient' when a combinator is not fully applied
984+instance (RunClient m, TypeError (PartialApplication HasClient arr)) => HasClient m ((arr :: a -> b) :> sub)
986+ type Client m (arr :> sub) = TypeError (PartialApplication HasClient arr)
987+ clientWithRoute _ _ _ = error "unreachable"
988+ hoistClientMonad _ _ _ _ = error "unreachable"
990+-- Erroring instances for 'HasClient' for unknown API combinators
991+instance {-# OVERLAPPABLE #-} (RunClient m, TypeError (NoInstanceForSub (HasClient m) ty)) => HasClient m (ty :> sub)
993+instance {-# OVERLAPPABLE #-} (RunClient m, TypeError (NoInstanceFor (HasClient m api))) => HasClient m api