1
1
{-# LANGUAGE ConstraintKinds #-}
2
- {-# LANGUAGE CPP #-}
3
2
{-# LANGUAGE DataKinds #-}
4
3
{-# LANGUAGE FlexibleContexts #-}
5
4
{-# LANGUAGE FlexibleInstances #-}
6
5
{-# LANGUAGE InstanceSigs #-}
7
6
{-# LANGUAGE MultiParamTypeClasses #-}
8
7
{-# LANGUAGE OverloadedStrings #-}
9
8
{-# LANGUAGE PolyKinds #-}
9
+ {-# LANGUAGE QuantifiedConstraints #-}
10
10
{-# LANGUAGE RankNTypes #-}
11
11
{-# LANGUAGE ScopedTypeVariables #-}
12
12
{-# LANGUAGE TypeApplications #-}
13
13
{-# LANGUAGE TypeFamilies #-}
14
14
{-# LANGUAGE TypeOperators #-}
15
15
{-# LANGUAGE UndecidableInstances #-}
16
16
17
- #if MIN_VERSION_base(4,9,0) && __GLASGOW_HASKELL__ >= 802
18
- #define HAS_TYPE_ERROR
19
- #endif
20
-
21
17
module Servant.Client.Core.HasClient (
22
18
clientIn ,
23
19
HasClient (.. ),
24
20
EmptyClient (.. ),
21
+ AsClientT ,
22
+ (//) ,
23
+ (/:) ,
25
24
foldMapUnion ,
26
25
matchUnion ,
27
26
) where
@@ -39,6 +38,7 @@ import Data.ByteString.Builder
39
38
import qualified Data.ByteString.Lazy as BL
40
39
import Data.Either
41
40
(partitionEithers )
41
+ import Data.Constraint (Dict (.. ))
42
42
import Data.Foldable
43
43
(toList )
44
44
import Data.List
@@ -47,7 +47,8 @@ import Data.Sequence
47
47
(fromList )
48
48
import qualified Data.Text as T
49
49
import Network.HTTP.Media
50
- (MediaType , matches , parseAccept , (//) )
50
+ (MediaType , matches , parseAccept )
51
+ import qualified Network.HTTP.Media as Media
51
52
import qualified Data.Sequence as Seq
52
53
import Data.SOP.BasicFunctors
53
54
(I (I ), (:.:) (Comp ))
@@ -79,7 +80,10 @@ import Servant.API
79
80
ReflectMethod (.. ), RemoteHost , ReqBody' , SBoolI , Stream ,
80
81
StreamBody' , Summary , ToHttpApiData , ToSourceIO (.. ), Vault ,
81
82
Verb , WithNamedContext , WithStatus (.. ), contentType , getHeadersHList ,
82
- getResponse , toEncodedUrlPiece , toUrlPiece )
83
+ getResponse , toEncodedUrlPiece , toUrlPiece , NamedRoutes )
84
+ import Servant.API.Generic
85
+ (GenericMode (.. ), ToServant , ToServantApi
86
+ , GenericServant , toServant , fromServant )
83
87
import Servant.API.ContentTypes
84
88
(contentTypes , AllMime (allMime ), AllMimeUnrender (allMimeUnrender ))
85
89
import Servant.API.TypeLevel (FragmentUnique , AtLeastOneFragment )
@@ -792,11 +796,7 @@ instance ( HasClient m api
792
796
-- > getBooks = client myApi
793
797
-- > -- then you can just use "getBooksBy" to query that endpoint.
794
798
-- > -- 'getBooks' for all books.
795
- #ifdef HAS_TYPE_ERROR
796
799
instance (AtLeastOneFragment api , FragmentUnique (Fragment a :> api ), HasClient m api
797
- #else
798
- instance ( HasClient m api
799
- #endif
800
800
) => HasClient m (Fragment a :> api ) where
801
801
802
802
type Client m (Fragment a :> api ) = Client m api
@@ -816,6 +816,119 @@ instance HasClient m api => HasClient m (BasicAuth realm usr :> api) where
816
816
hoistClientMonad pm _ f cl = \ bauth ->
817
817
hoistClientMonad pm (Proxy :: Proxy api ) f (cl bauth)
818
818
819
+ -- | A type that specifies that an API record contains a client implementation.
820
+ data AsClientT (m :: * -> * )
821
+ instance GenericMode (AsClientT m ) where
822
+ type AsClientT m :- api = Client m api
823
+
824
+
825
+ type GClientConstraints api m =
826
+ ( GenericServant api (AsClientT m )
827
+ , Client m (ToServantApi api ) ~ ToServant api (AsClientT m )
828
+ )
829
+
830
+ class GClient (api :: * -> * ) m where
831
+ proof :: Dict (GClientConstraints api m )
832
+
833
+ instance GClientConstraints api m => GClient api m where
834
+ proof = Dict
835
+
836
+ instance
837
+ ( forall n . GClient api n
838
+ , HasClient m (ToServantApi api )
839
+ , RunClient m
840
+ )
841
+ => HasClient m (NamedRoutes api ) where
842
+ type Client m (NamedRoutes api ) = api (AsClientT m )
843
+
844
+ clientWithRoute :: Proxy m -> Proxy (NamedRoutes api ) -> Request -> Client m (NamedRoutes api )
845
+ clientWithRoute pm _ request =
846
+ case proof @ api @ m of
847
+ Dict -> fromServant $ clientWithRoute pm (Proxy @ (ToServantApi api )) request
848
+
849
+ hoistClientMonad
850
+ :: forall ma mb .
851
+ Proxy m
852
+ -> Proxy (NamedRoutes api )
853
+ -> (forall x . ma x -> mb x )
854
+ -> Client ma (NamedRoutes api )
855
+ -> Client mb (NamedRoutes api )
856
+ hoistClientMonad _ _ nat clientA =
857
+ case (proof @ api @ ma , proof @ api @ mb ) of
858
+ (Dict , Dict ) ->
859
+ fromServant @ api @ (AsClientT mb ) $
860
+ hoistClientMonad @ m @ (ToServantApi api ) @ ma @ mb Proxy Proxy nat $
861
+ toServant @ api @ (AsClientT ma ) clientA
862
+
863
+ infixl 1 //
864
+ infixl 2 /:
865
+
866
+ -- | Helper to make code using records of clients more readable.
867
+ --
868
+ -- Can be mixed with (/:) for supplying arguments.
869
+ --
870
+ -- Example:
871
+ --
872
+ -- @@
873
+ -- type Api = NamedRoutes RootApi
874
+ --
875
+ -- data RootApi mode = RootApi
876
+ -- { subApi :: mode :- NamedRoutes SubApi
877
+ -- , …
878
+ -- } deriving Generic
879
+ --
880
+ -- data SubApi mode = SubApi
881
+ -- { endpoint :: mode :- Get '[JSON] Person
882
+ -- , …
883
+ -- } deriving Generic
884
+ --
885
+ -- api :: Proxy API
886
+ -- api = Proxy
887
+ --
888
+ -- rootClient :: RootApi (AsClientT ClientM)
889
+ -- rootClient = client api
890
+ --
891
+ -- endpointClient :: ClientM Person
892
+ -- endpointClient = client // subApi // endpoint
893
+ -- @@
894
+ (//) :: a -> (a -> b ) -> b
895
+ x // f = f x
896
+
897
+ -- | Convenience function for supplying arguments to client functions when
898
+ -- working with records of clients.
899
+ --
900
+ -- Intended to be used in conjunction with '(//)'.
901
+ --
902
+ -- Example:
903
+ --
904
+ -- @@
905
+ -- type Api = NamedRoutes RootApi
906
+ --
907
+ -- data RootApi mode = RootApi
908
+ -- { subApi :: mode :- Capture "token" String :> NamedRoutes SubApi
909
+ -- , hello :: mode :- Capture "name" String :> Get '[JSON] String
910
+ -- , …
911
+ -- } deriving Generic
912
+ --
913
+ -- data SubApi mode = SubApi
914
+ -- { endpoint :: mode :- Get '[JSON] Person
915
+ -- , …
916
+ -- } deriving Generic
917
+ --
918
+ -- api :: Proxy API
919
+ -- api = Proxy
920
+ --
921
+ -- rootClient :: RootApi (AsClientT ClientM)
922
+ -- rootClient = client api
923
+ --
924
+ -- hello :: String -> ClientM String
925
+ -- hello name = rootClient // hello /: name
926
+ --
927
+ -- endpointClient :: ClientM Person
928
+ -- endpointClient = client // subApi /: "foobar123" // endpoint
929
+ -- @@
930
+ (/:) :: (a -> b -> c ) -> b -> a -> c
931
+ (/:) = flip
819
932
820
933
821
934
{- Note [Non-Empty Content Types]
@@ -841,7 +954,7 @@ for empty and one for non-empty lists).
841
954
checkContentTypeHeader :: RunClient m => Response -> m MediaType
842
955
checkContentTypeHeader response =
843
956
case lookup " Content-Type" $ toList $ responseHeaders response of
844
- Nothing -> return $ " application" // " octet-stream"
957
+ Nothing -> return $ " application" Media. // " octet-stream"
845
958
Just t -> case parseAccept t of
846
959
Nothing -> throwClientError $ InvalidContentTypeHeader response
847
960
Just t' -> return t'
0 commit comments