@@ -0,0 +1,218 @@
1+# Generating mock curl calls
2+
3+In this example we will generate curl requests with mock post data from a servant API.
4+This may be usefull for testing and development purposes.
5+Especially post requests with a request body are tedious to send manually.
6+
7+Also, we will learn how to use the servant-forein library to generate stuff from servant APIs.
8+
9+
10+Language extensions and imports:
11+``` haskell
12+{-# LANGUAGE DataKinds #-}
13+{-# LANGUAGE DeriveGeneric #-}
14+{-# LANGUAGE FlexibleContexts #-}
15+{-# LANGUAGE FlexibleInstances #-}
16+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
17+{-# LANGUAGE MultiParamTypeClasses #-}
18+{-# LANGUAGE OverloadedStrings #-}
19+{-# LANGUAGE RankNTypes #-}
20+{-# LANGUAGE RecordWildCards #-}
21+{-# LANGUAGE ScopedTypeVariables #-}
22+{-# LANGUAGE TypeOperators #-}
23+
24+import Control.Lens ((^.))
25+import Data.Aeson.Compat
26+import Data.Aeson.Text
27+import Data.Monoid ((<>))
28+import Data.Proxy (Proxy (Proxy))
29+import Data.Text (Text)
30+import Data.Text.Encoding (decodeUtf8)
31+import qualified Data.Text.IO as T.IO
32+import qualified Data.Text.Lazy as LazyT
33+import GHC.Generics
34+import Servant ((:<|>), (:>), Get, JSON,
35+ Post, ReqBody)
36+import Servant.Foreign (Foreign, GenerateList,
37+ HasForeign, HasForeignType, Req,
38+ Segment, SegmentType (Cap, Static),
39+ argName, listFromAPI, path,
40+ reqBody, reqMethod, reqUrl, typeFor,
41+ unPathSegment, unSegment,)
42+import Test.QuickCheck.Arbitrary
43+import Test.QuickCheck.Arbitrary.Generic
44+import Test.QuickCheck.Gen (generate)
45+import qualified Data.Text as T
46+
47+```
48+
49+
50+Let's define our API:
51+
52+``` haskell
53+type UserAPI = "users" :> Get '[JSON] [User]
54+ :<|> "new" :> "user" :> ReqBody '[JSON] User :> Post '[JSON] ()
55+
56+data User = User
57+ { name :: String
58+ , age :: Int
59+ , email :: String
60+ } deriving (Eq, Show, Generic)
61+
62+instance Arbitrary User where
63+ arbitrary = genericArbitrary
64+ shrink = genericShrink
65+instance ToJSON User
66+instance FromJSON User
67+```
68+
69+Notice the `Arbitrary User` instance which we will later need to create mock data.
70+
71+Also, the obligatory servant boilerplate:
72+
73+``` haskell
74+api :: Proxy UserAPI
75+api = Proxy
76+```
77+
78+
79+## servant-forgein and the HasForeignType Class
80+
81+Servant-foreign allows us to look into the API we designed.
82+The entry point is `listFromAPI` which takes three types and returns a list of endpoints:
83+
84+``` haskell ignore
85+listFromAPI :: (HasForeign lang ftype api, GenerateList ftype (Foreign ftype api)) => Proxy lang -> Proxy ftype -> Proxy api -> [Req ftype]
86+```
87+
88+This looks a bit confusing...
89+[Here](https://hackage.haskell.org/package/servant-foreign-0.11.1/docs/Servant-Foreign.html#t:HasForeignType) is the documentation for the `HasForeign` typeclass.
90+We will not go into details here, but this allows us to create a value of type `ftype` for any type `a` in our API.
91+
92+In our case we want to create a mock of every type `a`.
93+
94+We create a new datatype that holds our mocked value. Well, not the mocked value itself. To mock it we need IO (random). So the promise of a mocked value after some IO is performed:
95+
96+``` haskell
97+data NoLang
98+
99+data Mocked = Mocked (IO Text)
100+```
101+
102+Now, we create an instance of `HasForeignType` for `NoLang` and `Mocked` for every `a` that implements `ToJSON` and `Arbitrary`:
103+``` haskell
104+instance (ToJSON a, Arbitrary a) => HasForeignType NoLang Mocked a where
105+ typeFor _ _ _ =
106+ Mocked (genText (Proxy :: Proxy a))
107+```
108+
109+What does `genText` do? It generates an arbitrary value of type `a` and encodes it as text. (And does some lazy to non-lazy text transformation we do not care about):
110+
111+``` haskell
112+genText :: (ToJSON a, Arbitrary a) => Proxy a -> IO Text
113+genText p =
114+ fmap (\v -> LazyT.toStrict $ encodeToLazyText v) (genArb p)
115+
116+genArb :: Arbitrary a => Proxy a -> IO a
117+genArb _ =
118+ generate arbitrary
119+```
120+
121+### Generating curl calls for every endpoint
122+
123+Everything is prepared now and we can start generating some curl calls.
124+
125+``` haskell
126+generateCurl :: (GenerateList Mocked (Foreign Mocked api), HasForeign NoLang Mocked api)
127+ => Proxy api
128+ -> Text
129+ -> IO Text
130+generateCurl p host =
131+ fmap T.unlines body
132+ where
133+ body = foldr (\endp curlCalls -> mCons (generateEndpoint host endp) curlCalls) (return [])
134+ $ listFromAPI (Proxy :: Proxy NoLang) (Proxy :: Proxy Mocked) p
135+```
136+
137+To understand the, better start at the end:
138+
139+`listFromAPI` gives us a list of endpoints. We iterate over them (`foldr`) and call `generateEndpoint` for every endpoint.
140+
141+As generate endpoint will not return `Text` but `IO Text` (remember we need some random bits to mock), we cannot just use the cons operator but need to build `IO [Text]` from `IO Text`s.
142+
143+``` haskell
144+mCons :: IO a -> IO [a] -> IO [a]
145+mCons ele list =
146+ ele >>= \e -> list >>= \l -> return ( e : l )
147+```
148+
149+
150+Now comes the juicy part; accessing the endpoints data:
151+
152+``` haskell
153+generateEndpoint :: Text -> Req Mocked -> IO Text
154+generateEndpoint host req =
155+ case maybeBody of
156+ Just body ->
157+ body >>= \b -> return $ T.intercalate " " [ "curl", "-X", method, "-d", "'" <> b <> "'"
158+ , "-H 'Content-Type: application/json'", host <> "/" <> url ]
159+ Nothing ->
160+ return $ T.intercalate " " [ "curl", "-X", method, host <> "/" <> url ]
161+ where
162+ method = decodeUtf8 $ req ^. reqMethod
163+
164+ url = T.intercalate "/" $ map segment (req ^. reqUrl . path)
165+
166+ maybeBody = fmap (\(Mocked io) -> io) (req ^. reqBody)
167+
168+```
169+`servant-foreign` offers a multitude of lenses to be used with `Req`-values.
170+
171+`reqMethod` gives us a straigthforward `Network.HTTP.Types.Method`, `reqUrl` the url part and so on.
172+Just take a look at [the docs](https://hackage.haskell.org/package/servant-foreign-0.11.1/docs/Servant-Foreign.html).
173+
174+But how do we get our mocked json string? This seems to be a bit to short to be true:
175+
176+``` haskell ignore
177+maybeBody = fmap (\(Mocked io) -> io) (req ^. reqBody)
178+```
179+
180+But it is that simple!
181+The docs say `reqBody` gives us a `Maybe f`. What is `f`, you ask? As defined in `generateCurl`, `f` is `Mocked` and contains a `IO Text`. How is this `Mocked` value created? The `HasForeignType::typeFor` does it!
182+
183+Of course only if the endpoint has a request body.
184+
185+
186+Some (incomplete) code for url segments:
187+``` haskell
188+segment :: Segment Mocked -> Text
189+segment seg =
190+ case unSegment seg of
191+ Static p ->
192+ unPathSegment p
193+
194+ Cap arg ->
195+ -- Left as exercise for the reader: Mock args in the url
196+ unPathSegment $ arg ^. argName
197+```
198+
199+And now, lets hook it all up in our main function:
200+
201+``` haskell
202+main :: IO ()
203+main =
204+ generateCurl api "localhost:8081" >>= (\v -> T.IO.putStrLn v)
205+```
206+
207+Done:
208+
209+``` curl
210+curl -X GET localhost:8081/users
211+curl -X POST -d '{"email":"wV_b:z!(3DM V","age":10,"name":"=|W"}' -H 'Content-Type: application/json' localhost:8081/new/user
212+
213+```
214+
215+This is of course no complete curl call mock generator, many things including path arguments are missing.
216+But it correctly generate mock calls for simple POST requests.
217+
218+Also, we now know how to use `HasForeignType` and `listFromAPI` to generate anything we want.