on type-errors help sign in
servant/doc/tutorial/Client.lhs
257 lines 9.1 kB history raw
1# Querying an API
3While defining handlers that [serve an API](Server.html) 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.html) 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)
25import qualified Servant.Client.Streaming as S
26```
28Also, we need examples for some domain specific data types:
30``` haskell
31data Position = Position
32 { xCoord :: Int
33 , yCoord :: Int
34 } deriving (Show, Generic)
36instance FromJSON Position
38newtype HelloMessage = HelloMessage { msg :: String }
39 deriving (Show, Generic)
41instance FromJSON HelloMessage
43data ClientInfo = ClientInfo
44 { clientName :: String
45 , clientEmail :: String
46 , clientAge :: Int
47 , clientInterestedIn :: [String]
48 } deriving Generic
50instance ToJSON ClientInfo
52data Email = Email
53 { from :: String
54 , to :: String
55 , subject :: String
56 , body :: String
57 } deriving (Show, Generic)
59instance FromJSON Email
60```
62Enough chitchat, let's see an example. Consider the following API type from the previous section:
64``` haskell
65type API = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position
66 :<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage
67 :<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email
68```
70What we are going to get with **servant-client** here is three functions, one to query each endpoint:
72``` haskell
73position :: Int -- ^ value for "x"
74 -> Int -- ^ value for "y"
75 -> ClientM Position
77hello :: Maybe String -- ^ an optional value for "name"
78 -> ClientM HelloMessage
80marketing :: ClientInfo -- ^ value for the request body
81 -> ClientM Email
82```
84Each function makes available as an argument any value that the response may
85depend on, as evidenced in the API type. How do we get these functions? By calling
86the function `client`. It takes one argument:
88- a `Proxy` to your API,
90``` haskell
91api :: Proxy API
92api = Proxy
94position :<|> hello :<|> marketing = client api
95```
97`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.
99If you have an `EmptyAPI` in your API, servant-client will hand you a value of
100type `EmptyClient` in the corresponding slot, where `data EmptyClient =
101EmptyClient`, as a way to indicate that you can't do anything useful with it.
103``` haskell ignore
104type API' = API :<|> EmptyAPI
106api' :: Proxy API'
107api' = Proxy
109(position' :<|> hello' :<|> marketing') :<|> EmptyClient = client api'
110```
112``` haskell ignore
113-- | URI scheme to use
114data Scheme =
115 Http -- ^ http://
116 | Https -- ^ https://
117 deriving
119-- | Simple data type to represent the target of HTTP requests
120-- for servant's automatically-generated clients.
121data BaseUrl = BaseUrl
122 { baseUrlScheme :: Scheme -- ^ URI scheme to use
123 , baseUrlHost :: String -- ^ host (eg "haskell.org")
124 , baseUrlPort :: Int -- ^ port (eg 80)
125 , baseUrlPath :: String -- ^ path (eg "/a/b/c")
126 }
127```
129That's it. Let's now write some code that uses our client functions.
131``` haskell
132queries :: ClientM (Position, HelloMessage, Email)
133queries = do
134 pos <- position 10 10
135 message <- hello (Just "servant")
136 em <- marketing (ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"])
137 return (pos, message, em)
139run :: IO ()
140run = do
141 manager' <- newManager defaultManagerSettings
142 res <- runClientM queries (mkClientEnv manager' (BaseUrl Http "localhost" 8081 ""))
143 case res of
144 Left err -> putStrLn $ "Error: " ++ show err
145 Right (pos, message, em) -> do
146 print pos
147 print message
148 print em
149```
151Here's the output of the above code running against the appropriate server:
153```
154Position {xCoord = 10, yCoord = 10}
155HelloMessage {msg = "Hello, servant"}
156Email {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!"}
157```
159The types of the arguments for the functions are the same as for (server-side) request handlers.
161## Changing the monad the client functions live in
163Just like `hoistServer` allows us to change the monad in which request handlers
164of a web application live, we also have `hoistClient` for changing the monad
165in which _client functions_ live. Consider the following trivial API:
167``` haskell
168type HoistClientAPI = Get '[JSON] Int :<|> Capture "n" Int :> Post '[JSON] Int
170hoistClientAPI :: Proxy HoistClientAPI
171hoistClientAPI = Proxy
172```
174We already know how to derive client functions for this API, and as we have
175seen above they all return results in the `ClientM` monad when using `servant-client`.
176However, `ClientM` is rarely (or never) the actual monad we need to use the client
177functions in. Sometimes we need to run them in IO, sometimes in a custom monad
178stack. `hoistClient` is a very simple solution to the problem of "changing" the monad
179the clients run in.
181``` haskell ignore
182hoistClient
183 :: HasClient ClientM api -- we need a valid API
184 => Proxy api -- a Proxy to the API type
185 -> (forall a. m a -> n a) -- a "monad conversion function" (natural transformation)
186 -> Client m api -- clients in the source monad
187 -> Client n api -- result: clients in the target monad
188```
190The "conversion function" argument above, just like the ones given to `hoistServer`, must
191be able to turn an `m a` into an `n a` for any choice of type `a`.
193Let's see this in action on our example. We first derive our client functions as usual,
194with all of them returning a result in `ClientM`.
196``` haskell
197getIntClientM :: ClientM Int
198postIntClientM :: Int -> ClientM Int
199getIntClientM :<|> postIntClientM = client hoistClientAPI
200```
202And we finally decide that we want the handlers to run in IO instead, by
203"post-applying" `runClientM` to a fixed client environment.
205``` haskell
206-- our conversion function has type: forall a. ClientM a -> IO a
207-- the result has type:
208-- Client IO HoistClientAPI = IO Int :<|> (Int -> IO Int)
209getClients :: ClientEnv -> Client IO HoistClientAPI
210getClients clientEnv
211 = hoistClient hoistClientAPI
212 ( fmap (either (error . show) id)
213 . flip runClientM clientEnv
214 )
215 (client hoistClientAPI)
216```
218## Querying Streaming APIs.
220Consider the following streaming API type:
222``` haskell
223type StreamAPI = "positionStream" :> StreamGet NewlineFraming JSON (SourceIO Position)
224```
226Note that we use the same `SourceIO` type as on the server-side
227(this is different from `servant-0.14`).
228However, we have to use different client, `Servant.Client.Streaming`,
229which can stream (but has different API).
231In any case, here's how we write a function to query our API:
233```haskell
234streamAPI :: Proxy StreamAPI
235streamAPI = Proxy
237posStream :: S.ClientM (SourceIO Position)
238posStream = S.client streamAPI
239```
241We'll get back a `SourceIO Position`. Instead of `runClientM`, the streaming
242client provides `withClientM`: the underlying HTTP connection is open only
243until the inner functions exits. Inside the block we can e.g just print out
244all elements from a `SourceIO`, to give some idea of how to work with them.
246``` haskell
247printSourceIO :: Show a => ClientEnv -> S.ClientM (SourceIO a) -> IO ()
248printSourceIO env c = S.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**!