@@ -0,0 +1,173 @@
1+# Using Free Client (for tests)
2+
3+Someone asked on IRC about getting `Request` & `Response` of what
4+`servant-client` uses, for testing purposes. My response: to ad-hoc extend
5+`servant-client` (as for tests), use `Servant.Client.Free`. This recipe is an
6+evidence.
7+
8+First the module header, but this time We'll comment the imports.
9+
10+```haskell
11+{-# LANGUAGE DataKinds #-}
12+{-# LANGUAGE TypeOperators #-}
13+module Main (main) where
14+```
15+
16+We will primarily use `Servant.Client.Free`, it doesn't re-export anything
17+from `free` package, so we need to import it as well.
18+
19+```haskell
20+import Control.Monad.Free
21+import Servant.Client.Free
22+```
23+
24+Also we'll use `servant-client` internals, which uses `http-client`,
25+so let's import them *qualified*
26+
27+```haskell
28+import qualified Servant.Client.Internal.HttpClient as I
29+import qualified Network.HTTP.Client as HTTP
30+```
31+
32+The rest of the imports are for a server we implement here for completeness.
33+
34+```haskell
35+import Servant
36+import Network.Wai.Handler.Warp (run)
37+import System.Environment (getArgs)
38+```
39+
40+## API & Main
41+
42+We'll work with a very simple API:
43+
44+```haskell
45+type API = "square" :> Capture "n" Int :> Get '[JSON] Int
46+
47+api :: Proxy API
48+api = Proxy
49+```
50+
51+Next we implement a `main`. If passed `server` it will run `server`, if
52+`client` a small `test` (to be defined next) will be run. This should be pretty
53+straigh-forward:
54+
55+```haskell
56+main :: IO ()
57+main = do
58+ args <- getArgs
59+ case args of
60+ ("server":_) -> do
61+ putStrLn "Starting cookbook-using-free-client at http://localhost:8000"
62+ run 8000 $ serve api $ \n -> return (n * n)
63+ ("client":_) ->
64+ test
65+ _ -> do
66+ putStrLn "Try:"
67+ putStrLn "cabal new-run cookbook-using-free-client server"
68+ putStrLn "cabal new-run cookbook-using-free-client client"
69+```
70+
71+## Test
72+
73+In the actual test, we'll use a `Servant.Client.Free` client.
74+Cecause we have a single endpoint API, we'll get a single client function:
75+
76+```haskell
77+fcli :: Int -> Free ClientF Int
78+fcli = client api
79+```
80+
81+Next, we can write our small test. We'll pass a value to `fcli` and inspect
82+the `Free` structure. First three possibilities are self-explanatory:
83+
84+```haskell
85+test :: IO ()
86+test = case fcli 42 of
87+ Pure n ->
88+ putStrLn $ "ERROR: got pure result: " ++ show n
89+ Free (Throw err) ->
90+ putStrLn $ "ERROR: got error right away: " ++ show err
91+ Free (StreamingRequest _req _k) ->
92+ putStrLn $ "ERROR: need to do streaming request" -- TODO: no Show Req :(
93+```
94+
95+We are interested in `RunRequest`, that's what client should block on:
96+
97+```haskell
98+ Free (RunRequest req k) -> do
99+```
100+
101+Then we need to prepare the context, get HTTP (connection) `Manager`
102+and `BaseUrl`:
103+
104+```haskell
105+ burl <- parseBaseUrl "http://localhost:8000"
106+ mgr <- HTTP.newManager HTTP.defaultManagerSettings
107+```
108+
109+Now we can use `servant-client` internals to convert servant's `Request`
110+to http-client's `Request`, and we can inspect it:
111+
112+```haskell
113+ let req' = I.requestToClientRequest burl req
114+ putStrLn $ "Making request: " ++ show req'
115+```
116+
117+`servant-client` does a bit more than `httpLbs`, but that's enough for us.
118+We get back a http-client `Response` which we can also inspect
119+
120+```haskell
121+ res' <- HTTP.httpLbs req' mgr
122+ putStrLn $ "Got response: " ++ show res'
123+```
124+
125+And we continue by turning http-client's `Response` into servant's `Response`,
126+and calling the continuation. We should get a `Pure` value.
127+
128+```haskell
129+ let res = I.clientResponseToResponse res'
130+
131+ case k res of
132+ Pure n ->
133+ putStrLn $ "Expected 1764, got " ++ show n
134+ _ ->
135+ putStrLn "ERROR: didn't got a response"
136+```
137+
138+So that's it. Using `Free` we can evaluate servant clients step-by-step, and
139+validate that they or the backend does what we expect (think: debugger). At the
140+end an example of client run (prettified):
141+
142+```
143+Making request: Request {
144+ host = "localhost"
145+ port = 8000
146+ secure = False
147+ requestHeaders = [("Accept","application/json;charset=utf-8,application/json")]
148+ path = "/square/42"
149+ queryString = ""
150+ method = "GET"
151+ proxy = Nothing
152+ rawBody = False
153+ redirectCount = 10
154+ responseTimeout = ResponseTimeoutDefault
155+ requestVersion = HTTP/1.1
156+}
157+
158+Got response: Response
159+ { responseStatus = Status {statusCode = 200, statusMessage = "OK"}
160+ , responseVersion = HTTP/1.1
161+ , responseHeaders =
162+ [ ("Transfer-Encoding","chunked")
163+ , ("Date","Thu, 05 Jul 2018 21:12:41 GMT")
164+ , ("Server","Warp/3.2.22")
165+ , ("Content-Type","application/json;charset=utf-8")
166+ ]
167+ , responseBody = "1764"
168+ , responseCookieJar = CJ {expose = []}
169+ , responseClose' = ResponseClose
170+ }
171+
172+Expected 1764, got 1764
173+```