on da2af9f help sign in
servant/doc/tutorial/Client.lhs
257 lines 9.2 kB history raw
1# Querying an API
3While defining handlers that [serve an API](Server.lhs) has a lot to it, querying an API is simpler: we do not care about what happens inside the webserver, we just need to know how to talk to it and get a response back. That said, we usually have to write the querying functions by hand because the structure of the API isn't a first class citizen and can't be inspected to generate the client-side functions.
5**servant** however has a way to inspect APIs, because APIs are just Haskell types and (GHC) Haskell lets us do quite a few things with types. In the same way that we look at an API type to deduce the types the handlers should have, we can inspect the structure of the API to *derive* Haskell functions that take one argument for each occurrence of `Capture`, `ReqBody`, `QueryParam`
6and friends (see [the tutorial introduction](ApiType.lhs) for an overview). By *derive*, we mean that there's no code generation involved - the functions are defined just by the structure of the API type.
8The source for this tutorial section is a literate Haskell file, so first we need to have some language extensions and imports:
10``` haskell
11{-# LANGUAGE DataKinds #-}
12{-# LANGUAGE DeriveGeneric #-}
13{-# LANGUAGE TypeOperators #-}
15module Client where
17import Data.Aeson
18import Data.Proxy
19import GHC.Generics
20import Network.HTTP.Client (newManager, defaultManagerSettings)
21import Servant.API
22import Servant.Client
23import Servant.Types.SourceT (foreach)
24import Control.Monad.Codensity (Codensity)
25```
27Also, we need examples for some domain specific data types:
29``` haskell
30data Position = Position
31 { xCoord :: Int
32 , yCoord :: Int
33 } deriving (Show, Generic)
35instance FromJSON Position
37newtype HelloMessage = HelloMessage { msg :: String }
38 deriving (Show, Generic)
40instance FromJSON HelloMessage
42data ClientInfo = ClientInfo
43 { clientName :: String
44 , clientEmail :: String
45 , clientAge :: Int
46 , clientInterestedIn :: [String]
47 } deriving Generic
49instance ToJSON ClientInfo
51data Email = Email
52 { from :: String
53 , to :: String
54 , subject :: String
55 , body :: String
56 } deriving (Show, Generic)
58instance FromJSON Email
59```
61Enough chitchat, let's see an example. Consider the following API type from the previous section:
63``` haskell
64type API = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position
65 :<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage
66 :<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email
67```
69What we are going to get with **servant-client** here is three functions, one to query each endpoint:
71``` haskell
72position :: Int -- ^ value for "x"
73 -> Int -- ^ value for "y"
74 -> ClientM Position
76hello :: Maybe String -- ^ an optional value for "name"
77 -> ClientM HelloMessage
79marketing :: ClientInfo -- ^ value for the request body
80 -> ClientM Email
81```
83Each function makes available as an argument any value that the response may
84depend on, as evidenced in the API type. How do we get these functions? By calling
85the function `client`. It takes one argument:
87- a `Proxy` to your API,
89``` haskell
90api :: Proxy API
91api = Proxy
93position :<|> hello :<|> marketing = client api
94```
96`client api` returns client functions for our _entire_ API, combined with `:<|>`, which we can pattern match on as above. You could say `client` "calculates" the correct type and number of client functions for the API type it is given (via a `Proxy`), as well as their implementations.
98If you have an `EmptyAPI` in your API, servant-client will hand you a value of
99type `EmptyClient` in the corresponding slot, where `data EmptyClient =
100EmptyClient`, as a way to indicate that you can't do anything useful with it.
102``` haskell ignore
103type API' = API :<|> EmptyAPI
105api' :: Proxy API'
106api' = Proxy
108(position' :<|> hello' :<|> marketing') :<|> EmptyClient = client api'
109```
111``` haskell ignore
112-- | URI scheme to use
113data Scheme =
114 Http -- ^ http://
115 | Https -- ^ https://
116 deriving
118-- | Simple data type to represent the target of HTTP requests
119-- for servant's automatically-generated clients.
120data BaseUrl = BaseUrl
121 { baseUrlScheme :: Scheme -- ^ URI scheme to use
122 , baseUrlHost :: String -- ^ host (eg "haskell.org")
123 , baseUrlPort :: Int -- ^ port (eg 80)
124 , baseUrlPath :: String -- ^ path (eg "/a/b/c")
125 }
126```
128That's it. Let's now write some code that uses our client functions.
130``` haskell
131queries :: ClientM (Position, HelloMessage, Email)
132queries = do
133 pos <- position 10 10
134 message <- hello (Just "servant")
135 em <- marketing (ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"])
136 return (pos, message, em)
138run :: IO ()
139run = do
140 manager' <- newManager defaultManagerSettings
141 res <- runClientM queries (mkClientEnv manager' (BaseUrl Http "localhost" 8081 ""))
142 case res of
143 Left err -> putStrLn $ "Error: " ++ show err
144 Right (pos, message, em) -> do
145 print pos
146 print message
147 print em
148```
150Here's the output of the above code running against the appropriate server:
152```
153Position {xCoord = 10, yCoord = 10}
154HelloMessage {msg = "Hello, servant"}
155Email {from = "great@company.com", to = "alp@foo.com", subject = "Hey Alp, we miss you!", body = "Hi Alp,\n\nSince you've recently turned 26, have you checked out our latest haskell, mathematics products? Give us a visit!"}
156```
158The types of the arguments for the functions are the same as for (server-side) request handlers.
160## Changing the monad the client functions live in
162Just like `hoistServer` allows us to change the monad in which request handlers
163of a web application live in, we also have `hoistClient` for changing the monad
164in which _client functions_ live. Consider the following trivial API:
166``` haskell
167type HoistClientAPI = Get '[JSON] Int :<|> Capture "n" Int :> Post '[JSON] Int
169hoistClientAPI :: Proxy HoistClientAPI
170hoistClientAPI = Proxy
171```
173We already know how to derive client functions for this API, and as we have
174seen above they all return results in the `ClientM` monad when using `servant-client`.
175However, `ClientM` rarely (or never) is the actual monad we need to use the client
176functions in. Sometimes we need to run them in IO, sometimes in a custom monad
177stack. `hoistClient` is a very simple solution to the problem of "changing" the monad
178the clients run in.
180``` haskell ignore
181hoistClient
182 :: HasClient ClientM api -- we need a valid API
183 => Proxy api -- a Proxy to the API type
184 -> (forall a. m a -> n a) -- a "monad conversion function" (natural transformation)
185 -> Client m api -- clients in the source monad
186 -> Client n api -- result: clients in the target monad
187```
189The "conversion function" argument above, just like the ones given to `hoistServer`, must
190be able to turn an `m a` into an `n a` for any choice of type `a`.
192Let's see this in action on our example. We first derive our client functions as usual,
193with all of them returning a result in `ClientM`.
195``` haskell
196getIntClientM :: ClientM Int
197postIntClientM :: Int -> ClientM Int
198getIntClientM :<|> postIntClientM = client hoistClientAPI
199```
201And we finally decide that we want the handlers to run in IO instead, by
202"post-applying" `runClientM` to a fixed client environment.
204``` haskell
205-- our conversion function has type: forall a. ClientM a -> IO a
206-- the result has type:
207-- Client IO HoistClientAPI = IO Int :<|> (Int -> IO Int)
208getClients :: ClientEnv -> Client IO HoistClientAPI
209getClients clientEnv
210 = hoistClient hoistClientAPI
211 ( fmap (either (error . show) id)
212 . flip runClientM clientEnv
213 )
214 (client hoistClientAPI)
215```
217## Querying Streaming APIs.
219Consider the following streaming API type:
221``` haskell
222type StreamAPI = "positionStream" :> StreamGet NewlineFraming JSON (SourceIO Position)
223```
225Note that we use the same `SourceIO` type as on the server-side
226(this is different from `servant-0.14`).
228In any case, here's how we write a function to query our API:
230```haskell
231streamAPI :: Proxy StreamAPI
232streamAPI = Proxy
234posStream :: ClientM (Codensity IO (SourceIO Position))
235posStream = client streamAPI
236```
238We'll get back a `Codensity IO (SourceIO Position)`. The wrapping in
239`Codensity` is generally necessary, as `Codensity` lets us `bracket` things
240properly. This is best explained by an example. To consume `ClientM (Codentity
241IO ...)` we can use `withClientM` helper: the underlying HTTP connection is
242open only until the inner functions exits. Inside the block we can e.g just
243print out all elements from a `SourceIO`, to give some idea of how to work with
244them.
246``` haskell
247printSourceIO :: Show a => ClientEnv -> ClientM (Codensity IO (SourceIO a)) -> IO ()
248printSourceIO env c = withClientM c env $ \e -> case e of
249 Left err -> putStrLn $ "Error: " ++ show err
250 Right rs -> foreach fail print rs
251```
253The stream is parsed and provided incrementally. So the above loop prints out
254each result as soon as it is received on the stream, rather than waiting until
255they are all available to print them at once.
257You now know how to use **servant-client**!