@@ -0,0 +1,194 @@
1+# Inspecting, debugging, simulating clients and more
2+
3+or simply put: _a practical introduction to `Servant.Client.Free`_.
4+
5+Someone asked on IRC how one could access the intermediate Requests (resp. Responses)
6+produced (resp. received) by client functions derived using servant-client.
7+My response to such inquiries is: to extend `servant-client` in an ad-hoc way (e.g for testing or debugging
8+purposes), use `Servant.Client.Free`. This recipe shows how.
9+
10+First the module header, but this time We'll comment the imports.
11+
12+```haskell
13+{-# LANGUAGE DataKinds #-}
14+{-# LANGUAGE TypeOperators #-}
15+module Main (main) where
16+```
17+
18+We will primarily use `Servant.Client.Free`, it doesn't re-export anything
19+from `free` package, so we need to import it as well.
20+
21+```haskell
22+import Control.Monad.Free
23+import Servant.Client.Free
24+```
25+
26+Also we'll use `servant-client` internals, which uses `http-client`,
27+so let's import them *qualified*
28+
29+```haskell
30+import qualified Servant.Client.Internal.HttpClient as I
31+import qualified Network.HTTP.Client as HTTP
32+```
33+
34+The rest of the imports are for a server we implement here for completeness.
35+
36+```haskell
37+import Servant
38+import Network.Wai.Handler.Warp (run)
39+import System.Environment (getArgs)
40+```
41+
42+## API & Main
43+
44+We'll work with a very simple API:
45+
46+```haskell
47+type API = "square" :> Capture "n" Int :> Get '[JSON] Int
48+
49+api :: Proxy API
50+api = Proxy
51+```
52+
53+Next we implement a `main`. If passed `"server"` it will run `server`, if passed
54+`"client"` it will run a `test` function (to be defined next). This should be
55+pretty straightforward:
56+
57+```haskell
58+main :: IO ()
59+main = do
60+ args <- getArgs
61+ case args of
62+ ("server":_) -> do
63+ putStrLn "Starting cookbook-using-free-client at http://localhost:8000"
64+ run 8000 $ serve api $ \n -> return (n * n)
65+ ("client":_) ->
66+ test
67+ _ -> do
68+ putStrLn "Try:"
69+ putStrLn "cabal new-run cookbook-using-free-client server"
70+ putStrLn "cabal new-run cookbook-using-free-client client"
71+```
72+
73+## Test
74+
75+In the client part, we will use a `Servant.Client.Free` client.
76+Because we have a single endpoint API, we'll get a single client function,
77+running in the `Free ClientF` (free) monad:
78+
79+```haskell
80+getSquare :: Int -> Free ClientF Int
81+getSquare = client api
82+```
83+
84+Such clients are "client functions without a backend", so to speak,
85+or where the backend has been abstracted out. To be more precise, `ClientF` is a functor that
86+precisely represents the operations servant-client-core needs from an http client backend.
87+So if we are to emulate one or augment what such a backend does, it will be by interpreting
88+all those operations, the way we want to. This also means we get access to the requests and
89+responses and can do anything we want with them, right when they are produced or consumed,
90+respectively.
91+
92+Next, we can write our small test. We'll pass a value to `getSquare` and inspect
93+the `Free` structure. The first three possibilities are self-explanatory:
94+
95+```haskell
96+test :: IO ()
97+test = case getSquare 42 of
98+ Pure n ->
99+ putStrLn $ "ERROR: got pure result: " ++ show n
100+ Free (Throw err) ->
101+ putStrLn $ "ERROR: got error right away: " ++ show err
102+ Free (StreamingRequest _req _k) ->
103+ putStrLn $ "ERROR: need to do streaming request" -- TODO: no Show Req :(
104+```
105+
106+We are interested in `RunRequest`, that's what client should block on:
107+
108+```haskell
109+ Free (RunRequest req k) -> do
110+```
111+
112+Then we need to prepare the context, get HTTP (connection) `Manager`
113+and `BaseUrl`:
114+
115+```haskell
116+ burl <- parseBaseUrl "http://localhost:8000"
117+ mgr <- HTTP.newManager HTTP.defaultManagerSettings
118+```
119+
120+Now we can use `servant-client`'s internals to convert servant's `Request`
121+to http-client's `Request`, and we can inspect it:
122+
123+```haskell
124+ let req' = I.requestToClientRequest burl req
125+ putStrLn $ "Making request: " ++ show req'
126+```
127+
128+`servant-client`'s request does a little more, but this is good enough for
129+our demo. We get back an http-client `Response` which we can also inspect.
130+
131+```haskell
132+ res' <- HTTP.httpLbs req' mgr
133+ putStrLn $ "Got response: " ++ show res'
134+```
135+
136+And we continue by turning http-client's `Response` into servant's `Response`,
137+and calling the continuation. We should get a `Pure` value.
138+
139+```haskell
140+ let res = I.clientResponseToResponse res'
141+
142+ case k res of
143+ Pure n ->
144+ putStrLn $ "Expected 1764, got " ++ show n
145+ _ ->
146+ putStrLn "ERROR: didn't got a response"
147+```
148+
149+So that's it. Using `Free` we can evaluate servant clients step-by-step, and
150+validate that the client functions or the HTTP client backend does what we expect
151+(e.g by printing requests/responses on the fly). In fact, using `Servant.Client.Free`
152+is a little simpler than defining a custom `RunClient` instance, especially
153+for those cases where it is handy to have the full sequence of client calls
154+and responses available for us to inspect, since `RunClient` only gives us
155+access to one `Request` or `Response` at a time.
156+
157+On the other hand, a "batch collection" of requests and/or responses can be achieved
158+with both free clients and a custom `RunClient` instance rather easily, for example
159+by using a `Writer [(Request, Response)]` monad.
160+
161+Here is an example of running our small `test` against a running server:
162+
163+```
164+Making request: Request {
165+ host = "localhost"
166+ port = 8000
167+ secure = False
168+ requestHeaders = [("Accept","application/json;charset=utf-8,application/json")]
169+ path = "/square/42"
170+ queryString = ""
171+ method = "GET"
172+ proxy = Nothing
173+ rawBody = False
174+ redirectCount = 10
175+ responseTimeout = ResponseTimeoutDefault
176+ requestVersion = HTTP/1.1
177+}
178+
179+Got response: Response
180+ { responseStatus = Status {statusCode = 200, statusMessage = "OK"}
181+ , responseVersion = HTTP/1.1
182+ , responseHeaders =
183+ [ ("Transfer-Encoding","chunked")
184+ , ("Date","Thu, 05 Jul 2018 21:12:41 GMT")
185+ , ("Server","Warp/3.2.22")
186+ , ("Content-Type","application/json;charset=utf-8")
187+ ]
188+ , responseBody = "1764"
189+ , responseCookieJar = CJ {expose = []}
190+ , responseClose' = ResponseClose
191+ }
192+
193+Expected 1764, got 1764
194+```