LambdaConf 2015: Haskell web workshop

July 27, 2015

These are my notes from working through the LambdaConf 2015 Haskell web workshop.

Setup

Clone serras/lambdaconf-2015-web:

$ git clone git@github.com:serras/lambdaconf-2015-web.git

Example 1: JSON

This first example uses aeson to explore JSON in Haskell. Let's set it up:

$ cd lambdaconf-2015-web/ex1-json/
$ cabal sandbox init
$ cabal update
$ cabal install --only-dependencies -j

The source code consists of a single Main.hs file:

src/Main.hs:

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Main where

import Data.Aeson hiding (json)
import Web.Spock.Safe

data Person = Person { name    :: String
                     , age     :: Integer
                     , address :: String
                     }

listPeople :: [Person]
listPeople = [ Person "Fulanito"  30 "Boulder, CO"
             , Person "Menganito" 25 "Madrid, ES"
             , Person "Zutano"    35 "Utrecht, NL"
             ]

main :: IO ()
main = runSpock 8080 $ spockT id $ do
  -- /hello/:name
  get ("hello" <//> var) $ \name ->
    json $ object [ "hello" .= String name ]
  -- /allow/:age
  get ("allow" <//> var) $ \(age :: Integer) ->
    error "Implement me!"

Right off the bat, we're dealing with an HTTP server. This code uses Spock, a Web framework for rapid development. A tutorial on Spock is available at www.spock.li/tutorial/.

There's a lot of stuff intertwined here, so we'll dive into functions and their types as we go.

Task 1: rewrite the code for the /allow/:age route to return either { "allowed": true } or { "allowed": false }.

Our first task is to fill in the missing implementation following get ("allow" <//> var). It will produce JSON akin to {"allowed":true} and {"allowed":false}.

We start with a function to determine whether an age is disallowed:

under21 :: Integer -> Bool
under21 = (< 21)

Following the example for /hello/:name, we can use object, which takes an array of Pairs and produces a Value that can be converted into JSON.

To produce our singular (allowed, true|false) pair, we'll use (.=):

main = runSpock 8080 $ spockT id $ do
  -- /allow/:age
  get ("allow" <//> var) $ \(age :: Integer) ->
    json $ object [ "allowed" .= not (under21 age) ]

Let's try it out:

$ cabal run
Spock is running on port 8080
$ curl localhost:8080/hello/bob
{
  "hello":"bob"
}

Task 2: the template code includes the definition of a Person data type and a list of people. Add a route to your Spock application of the form /person/:n, which returns the information of the n-th person in the list as JSON.

This one is a two-parter. First, we'll follow our instinct as novice Haskell developers and write a function to directly serialize a Person to a Value. Later, we'll write a ToJSON type class instance to do the work for us.

Direct serialization:

serializePerson :: Person -> Value
serializePerson p = object [ "name"    .= name p
                           , "age"     .= age p
                           , "address" .= address p
                           ]

main = runSpock 8080 $ spockT id $ do
  -- /person/:n
  get ("person" <//> var) $ \n ->
    json $ serializePerson (listPeople !! n)

Indirect serialization with a ToJSON instance:

instance ToJSON Person where
  toJSON (Person nm ag ad) = object [ "name"    .= nm
                                    , "age"     .= ag
                                    , "address" .= ad
                                    ]
main = runSpock 8080 $ spockT id $ do
  -- /person/:n
  get ("person" <//> var) $ \n ->
    json $ listPeople !! n

We can save some keystrokes on our Person instance of ToJSON if we use the DeriveGeneric language extension and make Person derive Generic:

{-# LANGUAGE DeriveGeneric #-}
data Person = Person { name    :: String
                     , age     :: Integer
                     , address :: String
                     } deriving Generic

instance ToJSON Person

Let's try it:

$ curl localhost:8080/person/1
{
  "name": "Menganito",
  "age": 25,
  "address": "Madrid, ES"
}

Task 3: create a new data type Task which stores a title, a description and the Person who is in charge of the task. Write a corresponding ToJSON instance. Note that you can reuse the definition for Person in the code for Task.

Don't get thrown by the phrase "stores a title" above; this task only involves writing a new data type, a ToJSON instance for it, and an endpoint to retrieve values of it.

The data type is much like the structure of Person, except that it includes a Person field instead of just strings and integers:

data Task = Task { title       :: String
                 , description :: String
                 , person      :: Person
                 }

The ToJSON instance is also similar to that for Person, automatically using its ToJSON instance as needed:

instance ToJSON Task where
  toJSON (Task t d p) = object [ "title"       .= t
                               , "description" .= d
                               , "person"      .= p
                               ]

Like listPeople, let's create a list of Tasks that our endpoint can draw from.

listTasks :: [Task]
listTasks = [ Task "Hack" "Code all the things." $ listPeople !! 1
            ]

Finally we implement the new endpoint.

main = runSpock 8080 $ spockT id $ do
  -- /task/:n
  get ("task" <//> var) $ \n ->
    json $ listTasks !! n

Let's try it:

$ curl -i localhost:8080/task/0
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Date: Tue, 28 Jul 2015 03:22:46 GMT
Server: Warp/3.1.0
Content-Type: application/json; charset=utf-8

{
  "title": "Hack",
  "description": "Code all the things.",
  "person": {
    "name": "Menganito",
    "age": 25,
    "address": "Madrid, ES"
  }
}
$ curl -i localhost:8080/task/1
curl: (52) Empty reply from server

Example 3: Database

Once again, let's set up a Cabal sandbox:

$ cd lambdaconf-2015-web/ex3and4-db/
$ cabal sandbox init
$ cabal update
$ cabal install --only-dependencies -j

Task 1 - Create a new task

First, write the code for the /task/new/:userId/:title which adds a new task to the database with the given title and related to the given user. Note that you need to check that the user exists prior to inserting the record in the database.

The response in case of error should contain a sensible error code. When the task is correctly inserted in the database, return a JSON representation of the record including its given identifier.

get ("task" <//> "new" <//> var <//> var) $ \(userId :: Int64) (title :: String) -> do
  error "Implement task #1"