packages feed

wrecker-0.1.1.0: examples/Client.lhs

# Building a API client for profiling with `wrecker`

Unlike most HTTP benchmarking applications, `wrecker` is intended to benchmark
HTTP calls inline with other forms of processing. This allows for complex
interactions necessary to benchmark certain API endpoints.

## TL;DR

`wrecker` let's you build elegant API clients that you can use for profiling

Here is the example we will build.

    testScript :: Int -> Recorder -> IO ()
    testScript port recorder = do
      Root { products
           , login
           , checkout
           }             <- get recorder "root"     (rootRef port)
      firstProduct : _   <- get recorder "products" products
      userRef            <- rpc recorder "login"    login
                                                    ( Credentials
                                                      { userName = "a@example.com"
                                                      , password = "password"
                                                      }
                                                    )
      User { usersCart } <- get recorder "user"     userRef
      Cart { items }     <- get recorder "cart"     usersCart

      insert "items" items firstProduct
      rpc "checkout" checkout cart

If this doesn't make sense on inspection, that is okay. This file builds up all
the necessary utilities and documents every line.

Most of the code in this file is "generic". It is the type of boilerplate you
make once for an API client.

You don't need to make a polish API client to use `wrecker`, just look at
TODO_MAKE_AESON_LENS_EXAMPLE to see how to use `record` with less setup.

## Boring Haskell Prelude

This is Haskell, so first we turn on the extensions we would like to use.

```haskell
{-# LANGUAGE NamedFieldPuns
           , DeriveAnyClass
           , DeriveGeneric
           , OverloadedStrings
           , DuplicateRecordFields
           , CPP
#-}
```

- `NamedFieldPuns` will let us destructure records conveniently. 
- `DeriveAnyClass` and `DeriveGeneric` are used turned on so the compiler
  can generate the JSON conversion functions for us automatically.
- `OverloadedStrings` is a here so redditors don't yell at me for using `String` instead of `Text`
- `DuplicateRecordFields` let's us use the `username` field in two records ... welcome to the future.

```haskell
#ifndef _CLIENT_IS_MAIN_
module Client where
#endif
```
Not the drones ...


### The Essence of `wrecker` is `record`

Introducing `wrecker`

```haskell
import Wrecker (record, defaultMain, Recorder)
```

- `record` is the primary function from `wrecker`. It has the signature
    ```
     record :: Recorder -> String -> IO a -> IO a
    ```

  `record` takes a `Recorder` and key in the form of a `String` and wraps some
  `IO` action. `record` runs the passed in `IO a` and um ... records information
  about such as the elapsed time and whether it succeeded or failed.
- `defaultMain` is one of two entry points `wrecker` provides (the other is
  `run`). `defaultMain` performs command line argument parsing for us, and
  runs the benchmarks with the provided options.
- `Recorder` is an opaque type we can call `record` with. `defaultMain` and `run`
  create a `Recorder` that is used by all the benchmark scripts.

```haskell
import Data.Aeson
```
We need JSON so of course we are using `aeson`.

```haskell
import qualified Network.Wreq as Wreq
```
`wrecker` does not provide any means for making HTTP calls. It records data,
computes statistics, controls concurrency and provides a convenient UI.
We leverage `wreq` to do the actual HTTP calls.

Here we wrap `wreq`'s `get` and `post` calls and make new functions which take
a `Recorder` so we can benchmark the times.

#### Other packages you can mostly ignore
```haskell
import GHC.Generics
import Data.ByteString.Lazy (ByteString)
import Data.Text (Text)
import Data.Text as T
import Network.HTTP.Client (responseBody)
```

## Make a Somewhat Generic JSON API

`wreq` is pretty easy to use for JSON APIs but it could be easier. Here we make
a quick wrapper around `wreq` specialized to JSON and we utilize `record`

### The Envelope

We wrap all JSON in sent to and from the server in an envelope,
mainly so we can also serialize a json object as opposed to an array.

```haskell
data Envelope a = Envelope { value :: a } -- <=> -- {"value" : toJSON a}
  deriving (Show, Eq, Generic, FromJSON, ToJSON)
```

The `Envelope` only exists to transmit data between the server and the browser.
- We wrap values going to the server in an `Envelope`

    ```haskell
    toEnvelope :: ToJSON a => a -> Value
    toEnvelope = toJSON . Envelope
    ```

- We unwrap values coming from the server in `Envelope`.

    ```haskell
    fromEnvelope :: FromJSON a => IO (Wreq.Response ByteString) -> IO a
    fromEnvelope x = fmap (value . responseBody) . Wreq.asJSON =<< x
    ```

- If we wrap inputs and unwrap outputs we can wrap a whole function.

    ```haskell
    liftEnvelope :: (ToJSON a, FromJSON b)
                 => (Value -> IO (Wreq.Response ByteString))
                 -> (a     -> IO b                         )
    liftEnvelope f = fromEnvelope . f . toEnvelope
    ```

### Wrap HTTP Calls with `record`

Not only do we want to wrap and unwrap types from our `Envelope`, we also need to wrap api calls with `record`.

```haskell
jsonGet :: FromJSON a => Recorder -> String -> Text -> IO a
jsonGet recorder key url = fromEnvelope $ record recorder key $ Wreq.get (T.unpack url)

jsonPost :: (ToJSON a, FromJSON b) => Recorder -> String -> Text -> a -> IO b
jsonPost recorder key url = liftEnvelope $ record recorder key . Wreq.post (T.unpack url)
```

## Make a Somewhat Generic REST API

### Resource References

We represent resource urls using the type `Ref`
```haskell
data Ref a = Ref { unRef :: Text }
  deriving (Show, Eq)
```

`Ref` is nothing more than a `Text` wrapper (the value there is the URL). `Ref`
has polymorphic `a` so we can talk about different types of resources.

A `FromJSON` instance which wraps a `Text` value, assuming the JSON is `Text`.

```haskell
instance FromJSON (Ref a) where
  parseJSON = withText "FromJSON (Ref a)" (return . Ref)
```

The `ToJSON` is just the reverse.

```haskell
instance ToJSON (Ref a) where
  toJSON (Ref x) = toJSON x
```

In addition to resources our API has ad-hoc RPC calls. RPC calls are also
represented as a URL.

### Adhoc RPC

```haskell
data RPC a b = RPC Text
  deriving (Show, Eq)

instance FromJSON (RPC a b) where
  parseJSON = withText "FromJSON (Ref a)" (return . RPC)
```

### REST API Actions

We utilize our `jsonGet` and `jsonPost` functions and make specialized versions
for our more specific REST and RPC calls.

- `get` takes a `Ref a` and returns an `a`. The `a` could be something
  like `Cart` or it could be a list like `[Ref a]`.

    ```haskell
    get :: FromJSON a => Recorder -> String -> Ref a -> IO a
    get recorder key (Ref url) = jsonGet recorder key url
    ```
- `insert` takes a `Ref` to a list and appends an item to it. It returns the
  reference that you passed in because why not.

    ```haskell
    insert :: ToJSON a => Recorder -> String -> Ref [a] -> a -> IO (Ref [a])
    insert recorder key (Ref url) = jsonPost recorder key url
    ```
- `rpc` unpacks the URL for the RPC endpoint and `POST`s the input, returning the output.

    ```haskell
    rpc :: (ToJSON a, FromJSON b) => Recorder -> String -> RPC a b -> a -> IO b
    rpc recorder key (RPC url) = jsonPost recorder key url
    ```

## The Example API

The API requires an initial call to the "/root" to obtain the URLs for
subsequent calls

```haskell
rootRef :: Int -> Ref Root
rootRef port = Ref $ T.pack $ "http://localhost:" ++ show port ++ "/root"
```

### API Response types

    Calling `GET` on "/root" returns the following JSON  ----------
                                                                  |
    Represented here --                                           |
                      |                                           |
                      v                                           v
```haskell                                                
data Root = Root                           
  { products :: Ref [Ref Product]          --     -- { "products" : "http://localhost:3000/products"
  , carts    :: Ref [Ref Cart   ]          -- <=> -- , "carts"    : "http://localhost:3000/carts"
  , users    :: Ref [Ref User   ]          --     -- , "users"    : "http://localhost:3000/users"
  , login    :: RPC Credentials (Ref User) --     -- , "login"    : "http://localhost:3000/login"
  , checkout :: RPC (Ref Cart)  ()         --     -- , "checkout" : "http://localhost:3000/checkout"
  } deriving (Eq, Show, Generic, FromJSON) --     -- }
```

Since the JSON is so uniform, we can use `aeson`s generic instances.

Calling `GET` on a `Ref Product` or "/products/:id" gives

```haskell
data Product = Product                     --     --
  { summary :: Text                        -- <=> -- { "summary" : "shirt" }
  } deriving (Eq, Show, Generic, FromJSON) --     --
```

Calling `GET` on a `Ref Cart` or "/carts/:id" gives

```haskell
data Cart = Cart                           --     --
  { items :: Ref [Ref Product]             -- <=> -- { "items" : ["http://localhost:3000/products/0"] }
  } deriving (Eq, Show, Generic, FromJSON) --     --
```

Calling `GET` on a `Ref User` or "/users/:id" gives

```haskell
data User = User                           --     --  
  { cart     :: Ref Cart                   -- <=> -- { "cart"     : "http://localhost:3000/carts/0"
  , username :: Text                       --     -- , "username" : "example"
  } deriving (Eq, Show, Generic, FromJSON) --     -- }
```

## RPC Types

The only additional type that we need is the input for the `login` RPC, mainly the `Credentials` type.

```haskell
data Credentials = Credentials             --     --
  { password :: Text                       -- <=> -- { "password" : "password"
  , username :: Text                       --     -- , "username" : "a@example.com"
  } deriving (Eq, Show, Generic, ToJSON)   --     -- }
```

## Profiling Script

We can now easily write our first script!

```haskell
testScript :: Int -> Recorder -> IO ()
testScript port recorder = do
```
Bootstrap the script and get all the URLs for the endpoints. Unpack
`products`, `login` and `checkout` for use later down.

```haskell
  Root { products
       , login
       , checkout
       } <- get recorder "root" (rootRef port)
```
We get all products and name the first one
```haskell
  firstProduct : _ <- get recorder "products" products
```

Login and get the user's ref.
```haskell
  userRef <- rpc recorder "login" login
                                  ( Credentials
                                     { username = "a@example.com"
                                     , password = "password"
                                     }
                                  )
```
Get the user and unpack the user's cart.
```haskell
  User { cart } <- get recorder "user" userRef
```
Get the cart unpack the items.
```haskell
  Cart { items } <- get recorder "cart" cart
```
Add the first product to the user's cart's items.
```haskell
  insert recorder "items" items firstProduct
```
Checkout.
```haskell
  rpc recorder "checkout" checkout cart
```

Port is hard coded to 3000 for this example

```haskell
benchmarks :: Int -> [(String, Recorder -> IO ())]
benchmarks port = [("test0", testScript port)]

main :: IO ()
main = defaultMain $ benchmarks 3000
```