:<|> "new" :> "user" :> ReqBody '[JSON] User :> Post '[JSON] ()
data User = User
{ name :: String
, age :: Int
, email :: String
} deriving (Eq, Show, Generic)
instance Arbitrary User where
arbitrary = genericArbitrary
shrink = genericShrink
instance ToJSON User
instance FromJSON User
```
Notice the `Arbitrary User` instance which we will later need to create mock data.
Also, the obligatory servant boilerplate:
``` haskell
api :: Proxy UserAPI
api = Proxy
```
## servant-forgein and the HasForeignType Class
Servant-foreign allows us to look into the API we designed.
The entry point is `listFromAPI` which takes three types and returns a list of endpoints:
``` haskell ignore
listFromAPI :: (HasForeign lang ftype api, GenerateList ftype (Foreign ftype api)) => Proxy lang -> Proxy ftype -> Proxy api -> [Req ftype]
```
This looks a bit confusing...
[Here](https://hackage.haskell.org/package/servant-foreign-0.11.1/docs/Servant-Foreign.html#t:HasForeignType) is the documentation for the `HasForeign` typeclass.
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.
In our case we want to create a mock of every type `a`.
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:
``` haskell
data NoLang
data Mocked = Mocked (IO Text)
```
Now, we create an instance of `HasForeignType` for `NoLang` and `Mocked` for every `a` that implements `ToJSON` and `Arbitrary`:
``` haskell
instance (ToJSON a, Arbitrary a) => HasForeignType NoLang Mocked a where
typeFor _ _ _ =
Mocked (genText (Proxy :: Proxy a))
```
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):
``` haskell
genText :: (ToJSON a, Arbitrary a) => Proxy a -> IO Text
`listFromAPI` gives us a list of endpoints. We iterate over them (`foldr`) and call `generateEndpoint` for every endpoint.
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.
``` haskell
mCons :: IO a -> IO [a] -> IO [a]
mCons ele list =
ele >>= \e -> list >>= \l -> return ( e : l )
```
Now comes the juicy part; accessing the endpoints data:
``` haskell
generateEndpoint :: Text -> Req Mocked -> IO Text
generateEndpoint host req =
case maybeBody of
Just body ->
body >>= \b -> return $ T.intercalate " " [ "curl", "-X", method, "-d", "'" <> b <> "'"
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!
Of course only if the endpoint has a request body.
Some (incomplete) code for url segments:
``` haskell
segment :: Segment Mocked -> Text
segment seg =
case unSegment seg of
Static p ->
unPathSegment p
Cap arg ->
-- Left as exercise for the reader: Mock args in the url
unPathSegment $ arg ^. argName
```
And now, lets hook it all up in our main function:
``` haskell
main :: IO ()
main =
generateCurl api "localhost:8081" >>= (\v -> T.IO.putStrLn v)
```
Done:
``` curl
curl -X GET localhost:8081/users
curl -X POST -d '{"email":"wV_b:z!(3DM V","age":10,"name":"=|W"}' -H 'Content-Type: application/json' localhost:8081/new/user
```
This is of course no complete curl call mock generator, many things including path arguments are missing.
But it correctly generate mock calls for simple POST requests.
Also, we now know how to use `HasForeignType` and `listFromAPI` to generate anything we want.