on master help sign in
Merge pull request #640 from fierce-katie/generic-client

Generic client

commitc50fdefe325941acb53addb2914a7343905d4498 authorOleg Grenrus <oleg.grenrus@iki.fi> dateTue 17 Jan 2017, 17:06 (10 years ago) parentc09c0cfaf1b267
4 files changed+235
servant-client/CHANGELOG.md+2 servant-client/servant-client.cabal+3 servant-client/src/Servant/Client/Generic.hs+164 servant-client/test/Servant/ClientSpec.hs+66
servant-client/CHANGELOG.md+2
@@ -7,6 +7,8 @@
77* client asks for any content-type in Accept contentTypes non-empty list
88 ([#615](https://github.com/haskell-servant/servant/pull/615))
99
10+* Add `ClientLike` class that matches client functions generated using `client` with client data structure.
11+
10120.9.1.1
1113-------
1214
servant-client/servant-client.cabal+3
@@ -30,6 +30,7 @@ source-repository head
3030library
3131 exposed-modules:
3232 Servant.Client
33+ Servant.Client.Generic
3334 Servant.Client.Experimental.Auth
3435 Servant.Common.BaseUrl
3536 Servant.Common.BasicAuth
@@ -42,6 +43,7 @@ library
4243 , base64-bytestring >= 1.0.0.1 && < 1.1
4344 , bytestring >= 0.10 && < 0.11
4445 , exceptions >= 0.8 && < 0.9
46+ , generics-sop >= 0.1.0.0 && < 0.3
4547 , http-api-data >= 0.3 && < 0.4
4648 , http-client >= 0.4.18.1 && < 0.6
4749 , http-client-tls >= 0.2.2 && < 0.4
@@ -96,3 +98,4 @@ test-suite spec
9698 , transformers-compat
9799 , wai
98100 , warp
101+ , generics-sop
servant-client/src/Servant/Client/Generic.hsnew file+164
@@ -0,0 +1,164 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE DefaultSignatures #-}
4+{-# LANGUAGE FlexibleContexts #-}
5+{-# LANGUAGE FlexibleInstances #-}
6+{-# LANGUAGE MultiParamTypeClasses #-}
7+{-# LANGUAGE TypeFamilies #-}
8+{-# LANGUAGE TypeOperators #-}
9+{-# LANGUAGE UndecidableInstances #-}
10+
11+#include "overlapping-compat.h"
12+
13+module Servant.Client.Generic
14+ ( ClientLike(..)
15+ , genericMkClientL
16+ , genericMkClientP
17+ ) where
18+
19+import Generics.SOP (Code, Generic, I(..), NP(..), NS(Z), SOP(..), to)
20+import Servant.API ((:<|>)(..))
21+import Servant.Client (ClientM)
22+
23+-- | This class allows us to match client structure with client functions
24+-- produced with 'client' without explicit pattern-matching.
25+--
26+-- The client structure needs a 'Generics.SOP.Generic' instance.
27+--
28+-- Example:
29+--
30+-- > type API
31+-- > = "foo" :> Capture "x" Int :> Get '[JSON] Int
32+-- > :<|> "bar" :> QueryParam "a" Char :> QueryParam "b" String :> Post '[JSON] [Int]
33+-- > :<|> Capture "nested" Int :> NestedAPI
34+-- >
35+-- > type NestedAPI
36+-- > = Get '[JSON] String
37+-- > :<|> "baz" :> QueryParam "c" Char :> Post '[JSON] ()
38+-- >
39+-- > data APIClient = APIClient
40+-- > { getFoo :: Int -> ClientM Int
41+-- > , postBar :: Maybe Char -> Maybe String -> ClientM [Int]
42+-- > , mkNestedClient :: Int -> NestedClient
43+-- > } deriving GHC.Generic
44+-- >
45+-- > instance Generics.SOP.Generic APIClient
46+-- > instance (Client API ~ client) => ClientLike client APIClient
47+-- >
48+-- > data NestedClient = NestedClient
49+-- > { getString :: ClientM String
50+-- > , postBaz :: Maybe Char -> ClientM ()
51+-- > } deriving GHC.Generic
52+-- >
53+-- > instance Generics.SOP.Generic NestedClient
54+-- > instance (Client NestedAPI ~ client) => ClientLike client NestedClient
55+-- >
56+-- > mkAPIClient :: APIClient
57+-- > mkAPIClient = mkClient (client (Proxy :: Proxy API))
58+--
59+-- By default, left-nested alternatives are expanded:
60+--
61+-- > type API1
62+-- > = "foo" :> Capture "x" Int :> Get '[JSON] Int
63+-- > :<|> "bar" :> QueryParam "a" Char :> Post '[JSON] String
64+-- >
65+-- > type API2
66+-- > = "baz" :> QueryParam "c" Char :> Post '[JSON] ()
67+-- >
68+-- > type API = API1 :<|> API2
69+-- >
70+-- > data APIClient = APIClient
71+-- > { getFoo :: Int -> ClientM Int
72+-- > , postBar :: Maybe Char -> ClientM String
73+-- > , postBaz :: Maybe Char -> ClientM ()
74+-- > } deriving GHC.Generic
75+-- >
76+-- > instance Generics.SOP.Generic APIClient
77+-- > instance (Client API ~ client) => ClientLike client APIClient
78+-- >
79+-- > mkAPIClient :: APIClient
80+-- > mkAPIClient = mkClient (client (Proxy :: Proxy API))
81+--
82+-- If you want to define client for @API1@ as a separate data structure,
83+-- you can use 'genericMkClientP':
84+--
85+-- > data APIClient1 = APIClient1
86+-- > { getFoo :: Int -> ClientM Int
87+-- > , postBar :: Maybe Char -> ClientM String
88+-- > } deriving GHC.Generic
89+-- >
90+-- > instance Generics.SOP.Generic APIClient1
91+-- > instance (Client API1 ~ client) => ClientLike client APIClient1
92+-- >
93+-- > data APIClient = APIClient
94+-- > { mkAPIClient1 :: APIClient1
95+-- > , postBaz :: Maybe Char -> ClientM ()
96+-- > } deriving GHC.Generic
97+-- >
98+-- > instance Generics.SOP.Generic APIClient
99+-- > instance (Client API ~ client) => ClientLike client APIClient where
100+-- > mkClient = genericMkClientP
101+-- >
102+-- > mkAPIClient :: APIClient
103+-- > mkAPIClient = mkClient (client (Proxy :: Proxy API))
104+class ClientLike client custom where
105+ mkClient :: client -> custom
106+ default mkClient :: (Generic custom, Code custom ~ '[xs], GClientList client '[], GClientLikeL (ClientList client '[]) xs)
107+ => client -> custom
108+ mkClient = genericMkClientL
109+
110+instance ClientLike client custom
111+ => ClientLike (a -> client) (a -> custom) where
112+ mkClient c = mkClient . c
113+
114+instance ClientLike (ClientM a) (ClientM a) where
115+ mkClient = id
116+
117+-- | Match client structure with client functions, regarding left-nested API clients
118+-- as separate data structures.
119+class GClientLikeP client xs where
120+ gMkClientP :: client -> NP I xs
121+
122+instance (GClientLikeP b (y ': xs), ClientLike a x)
123+ => GClientLikeP (a :<|> b) (x ': y ': xs) where
124+ gMkClientP (a :<|> b) = I (mkClient a) :* gMkClientP b
125+
126+instance ClientLike a x => GClientLikeP a '[x] where
127+ gMkClientP a = I (mkClient a) :* Nil
128+
129+-- | Match client structure with client functions, expanding left-nested API clients
130+-- in the same structure.
131+class GClientLikeL (xs :: [*]) (ys :: [*]) where
132+ gMkClientL :: NP I xs -> NP I ys
133+
134+instance GClientLikeL '[] '[] where
135+ gMkClientL Nil = Nil
136+
137+instance (ClientLike x y, GClientLikeL xs ys) => GClientLikeL (x ': xs) (y ': ys) where
138+ gMkClientL (I x :* xs) = I (mkClient x) :* gMkClientL xs
139+
140+type family ClientList (client :: *) (acc :: [*]) :: [*] where
141+ ClientList (a :<|> b) acc = ClientList a (ClientList b acc)
142+ ClientList a acc = a ': acc
143+
144+class GClientList client (acc :: [*]) where
145+ gClientList :: client -> NP I acc -> NP I (ClientList client acc)
146+
147+instance (GClientList b acc, GClientList a (ClientList b acc))
148+ => GClientList (a :<|> b) acc where
149+ gClientList (a :<|> b) acc = gClientList a (gClientList b acc)
150+
151+instance OVERLAPPABLE_ (ClientList client acc ~ (client ': acc))
152+ => GClientList client acc where
153+ gClientList c acc = I c :* acc
154+
155+-- | Generate client structure from client type, expanding left-nested API (done by default).
156+genericMkClientL :: (Generic custom, Code custom ~ '[xs], GClientList client '[], GClientLikeL (ClientList client '[]) xs)
157+ => client -> custom
158+genericMkClientL = to . SOP . Z . gMkClientL . flip gClientList Nil
159+
160+-- | Generate client structure from client type, regarding left-nested API clients as separate data structures.
161+genericMkClientP :: (Generic custom, Code custom ~ '[xs], GClientLikeP client xs)
162+ => client -> custom
163+genericMkClientP = to . SOP . Z . gMkClientP
164+
servant-client/test/Servant/ClientSpec.hs+66
@@ -36,6 +36,7 @@ import Data.Char (chr, isPrint)
3636import Data.Foldable (forM_)
3737import Data.Monoid hiding (getLast)
3838import Data.Proxy
39+import qualified Generics.SOP as SOP
3940import GHC.Generics (Generic)
4041import qualified Network.HTTP.Client as C
4142import Network.HTTP.Media
@@ -55,6 +56,7 @@ import Web.FormUrlEncoded (FromForm, ToForm)
5556import Servant.API
5657import Servant.API.Internal.Test.ComprehensiveAPI
5758import Servant.Client
59+import Servant.Client.Generic
5860import qualified Servant.Common.Req as SCR
5961import Servant.Server
6062import Servant.Server.Experimental.Auth
@@ -69,6 +71,7 @@ spec = describe "Servant.Client" $ do
6971 wrappedApiSpec
7072 basicAuthSpec
7173 genAuthSpec
74+ genericClientSpec
7275
7376-- * test data types
7477
@@ -222,6 +225,53 @@ genAuthServerContext = genAuthHandler :. EmptyContext
222225genAuthServer :: Application
223226genAuthServer = serveWithContext genAuthAPI genAuthServerContext (const (return alice))
224227
228+-- * generic client stuff
229+
230+type GenericClientAPI
231+ = QueryParam "sqr" Int :> Get '[JSON] Int
232+ :<|> Capture "foo" String :> NestedAPI1
233+
234+data GenericClient = GenericClient
235+ { getSqr :: Maybe Int -> SCR.ClientM Int
236+ , mkNestedClient1 :: String -> NestedClient1
237+ } deriving Generic
238+instance SOP.Generic GenericClient
239+instance (Client GenericClientAPI ~ client) => ClientLike client GenericClient
240+
241+type NestedAPI1
242+ = QueryParam "int" Int :> NestedAPI2
243+ :<|> QueryParam "id" Char :> Get '[JSON] Char
244+
245+data NestedClient1 = NestedClient1
246+ { mkNestedClient2 :: Maybe Int -> NestedClient2
247+ , idChar :: Maybe Char -> SCR.ClientM Char
248+ } deriving Generic
249+instance SOP.Generic NestedClient1
250+instance (Client NestedAPI1 ~ client) => ClientLike client NestedClient1
251+
252+type NestedAPI2
253+ = "sum" :> Capture "first" Int :> Capture "second" Int :> Get '[JSON] Int
254+ :<|> "void" :> Post '[JSON] ()
255+
256+data NestedClient2 = NestedClient2
257+ { getSum :: Int -> Int -> SCR.ClientM Int
258+ , doNothing :: SCR.ClientM ()
259+ } deriving Generic
260+instance SOP.Generic NestedClient2
261+instance (Client NestedAPI2 ~ client) => ClientLike client NestedClient2
262+
263+genericClientServer :: Application
264+genericClientServer = serve (Proxy :: Proxy GenericClientAPI) (
265+ (\ mx -> case mx of
266+ Just x -> return (x*x)
267+ Nothing -> throwError $ ServantErr 400 "missing parameter" "" []
268+ )
269+ :<|> nestedServer1
270+ )
271+ where
272+ nestedServer1 _str = nestedServer2 :<|> (maybe (throwError $ ServantErr 400 "missing parameter" "" []) return)
273+ nestedServer2 _int = (\ x y -> return (x + y)) :<|> return ()
274+
225275{-# NOINLINE manager #-}
226276manager :: C.Manager
227277manager = unsafePerformIO $ C.newManager C.defaultManagerSettings
@@ -392,6 +442,22 @@ genAuthSpec = beforeAll (startWaiApp genAuthServer) $ afterAll endWaiApp $ do
392442 Left FailureResponse{..} <- runClientM (getProtected authRequest) (ClientEnv manager baseUrl)
393443 responseStatus `shouldBe` (HTTP.Status 401 "Unauthorized")
394444
445+genericClientSpec :: Spec
446+genericClientSpec = beforeAll (startWaiApp genericClientServer) $ afterAll endWaiApp $ do
447+ describe "Servant.Client.Generic" $ do
448+
449+ let GenericClient{..} = mkClient (client (Proxy :: Proxy GenericClientAPI))
450+ NestedClient1{..} = mkNestedClient1 "example"
451+ NestedClient2{..} = mkNestedClient2 (Just 42)
452+
453+ it "works for top-level client function" $ \(_, baseUrl) -> do
454+ (left show <$> (runClientM (getSqr (Just 5)) (ClientEnv manager baseUrl))) `shouldReturn` Right 25
455+
456+ it "works for nested clients" $ \(_, baseUrl) -> do
457+ (left show <$> (runClientM (idChar (Just 'c')) (ClientEnv manager baseUrl))) `shouldReturn` Right 'c'
458+ (left show <$> (runClientM (getSum 3 4) (ClientEnv manager baseUrl))) `shouldReturn` Right 7
459+ (left show <$> (runClientM doNothing (ClientEnv manager baseUrl))) `shouldReturn` Right ()
460+
395461-- * utils
396462
397463startWaiApp :: Application -> IO (ThreadId, BaseUrl)