1   Introduction and preliminaries

This post gives instructions on how to create a simple REST application that responds to a GET request and delivers the time and date in any of HTML, JSON, or plain text. It also provides a Python client that can be used to test the Web application.

You can find additional examples of how to implement a REST application on top of Cowboy in the examples subdirectory here: https://github.com/ninenines/cowboy.

You will also want to read the Cowboy User Guide: https://ninenines.eu/docs/en/cowboy/1.0/guide/.

2   Instructions etc.

2.1   Create an application skeleton

Create a skeleton for your application. I'm following the instructions here: https://ninenines.eu/docs/en/cowboy/2.0/guide/getting_started/.

Note: There are differences between Cowboy 1.0 and Cowboy 2.0. In this document, I'm following version 2.0.

Do the following:

$ mkdir simple_rest
$ cd simple_rest
$ wget https://erlang.mk/erlang.mk
$ make -f erlang.mk bootstrap bootstrap-rel
$ make

Test it to make sure that things are good so far:

$ ./_rel/simple_rest_release/bin/simple_rest_release console

You might want to create a shell script to start your server:

#!/bin/bash
./_rel/simple_rest_release/bin/simple_rest_release console

Or, you can do make and run in one step:

$ make run

Your application should run without errors. But, it won't do much yet. So, we'll start adding some functionality.

Next, add cowboy to your application -- Add these lines to your Makefile:

DEPS = cowboy
dep_cowboy_commit = master

So that your Makefile looks something like this:

PROJECT = simple_rest
PROJECT_DESCRIPTION = New project
PROJECT_VERSION = 0.1.0

DEPS = cowboy
dep_cowboy_commit = master

include erlang.mk

Now, run make again:

$ make

And, check to make sure that it still runs:

$ ./_rel/simple_rest_release/bin/simple_rest_release console

2.2   Create a REST handler

2.2.1   Routing

First, we need to create a routing to our handler. So, change src/simple_rest_app.erl so that it looks like this:

-module(simple_rest_app).
-behaviour(application).

-export([start/2]).
-export([stop/1]).

start(_Type, _Args) ->
    Dispatch = cowboy_router:compile([
        {'_', [
               {"/", rest_time_handler, []}
              ]}
    ]),
    {ok, _} = cowboy:start_clear(my_http_listener, 100,
        [{port, 8080}],
        #{env => #{dispatch => Dispatch}}
    ),
    simple_rest_sup:start_link().

stop(_State) ->
    ok.

Notes:

  • We've added those lines to start/2.
  • Basically, what those lines say is that when an HTTP client requests the URL http://my_server:8080/, i.e. uses the path /, we'll handle that request with the Erlang module src/simple_rest_handler.erl.
  • We've also changed the line that calls cowboy:start_clear/4. I suspect that is necessary because we're using cowboy 2.0, rather than version 1.0.

Arguments -- If we want to pass a segment of the URL to our handler, then we prefix that segment with a colon. Then, in our handler, we can retrieve the value of that segment by calling cowboy_req:binding/{2,3}. For example, given the following routing pattern in the call to cowboy_router:compile/1:

{"/help/:style", rest_time_handler, []}

And, a request URL that looks like this:

http://localhost:8080/help/verbose

Then, in our handler, we could retrieve the value "verbose" with the following:

Style = cowboy_req:binding(style, Req),

Options -- The third value in the path and handler tuple is passed as the second argument to init/2 in our handler. So, for example, given the following routings in the call to cowboy_router:compile/1:

{"/operation1", rest_time_handler, [operation1]},
{"/operation2", rest_time_handler, [operation2]}

And, this request URL:

http://localhost:8080/operation2

Then, the init/2 function in rest_time_handler would receive the value [operation2] as its second argument.

2.2.2   The handler

Next, we'll create our handler module. A reasonable way to do that is to find one in the cowboy examples (at https://github.com/ninenines/cowboy/tree/master/examples).

I've created one that responds to requests for HTML, JSON, and plain text. Here is my rest_time_handler.erl:

%% @doc REST time handler.
-module(rest_time_handler).

%% Webmachine API
-export([
         init/2,
         content_types_provided/2
        ]).

-export([
         time_to_html/2,
         time_to_json/2,
         time_to_text/2
        ]).

init(Req, Opts) ->
    {cowboy_rest, Req, Opts}.

content_types_provided(Req, State) ->
    {[
        {<<"text/html">>, time_to_html},
        {<<"application/json">>, time_to_json},
        {<<"text/plain">>, time_to_text}
    ], Req, State}.

time_to_html(Req, State) ->
    {Hour, Minute, Second} = erlang:time(),
    {Year, Month, Day} = erlang:date(),
    Body = "<html>
<head>
    <meta charset=\"utf-8\">
    <title>REST Time</title>
</head>
<body>
    <h1>REST time server</h1>
    <ul>
        <li>Time -- ~2..0B:~2..0B:~2..0B</li>
        <li>Date -- ~4..0B/~2..0B/~2..0B</li>
</body>
</html>",
    Body1 = io_lib:format(Body, [
        Hour, Minute, Second,
        Year, Month, Day
    ]),
    Body2 = list_to_binary(Body1),
    {Body2, Req, State}.

time_to_json(Req, State) ->
    {Hour, Minute, Second} = erlang:time(),
    {Year, Month, Day} = erlang:date(),
    Body = "
{
    \"time\": \"~2..0B:~2..0B:~2..0B\",
    \"date\": \"~4..0B/~2..0B/~2..0B\"
}",
    Body1 = io_lib:format(Body, [
        Hour, Minute, Second,
        Year, Month, Day
    ]),
    Body2 = list_to_binary(Body1),
    {Body2, Req, State}.

time_to_text(Req, State) ->
    {Hour, Minute, Second} = erlang:time(),
    {Year, Month, Day} = erlang:date(),
    Body = "
    time: ~2..0B:~2..0B:~2..0B,
    date: ~4..0B/~2..0B/~2..0B
",
    Body1 = io_lib:format(Body, [
        Hour, Minute, Second,
        Year, Month, Day
    ]),
    Body2 = list_to_binary(Body1),
    {Body2, Req, State}.

Notes:

  • The init/2 callback function uses the return value {cowboy_rest, Req, Opts} to tell cowboy to use its REST decision mechanism and logic to handle this request. For more on the logic used by cowboy for REST, see https://ninenines.eu/docs/en/cowboy/2.0/guide/rest_flowcharts/.
  • The callback function content_types_provided/2 tells the cowboy REST decision tree which of our functions to call in order to handle requests for each content type.
  • And, of course, we implement each of the functions that we specified in content_types_provided/2. In each of these functions, we return a tuple containing the following items: (1) the content or body or payload to be returned to the requesting client; (2) the (possibly modified) request object; and (3) the (possibly modified) state object.

2.3   Build a release

We can build our release and executable with a simple:

$ make

2.4   Run it

Run it as before:

$ ./_rel/simple_rest_release/bin/simple_rest_release console

Or, if you have a script that contains the above, run that.

Or, use this to build and run:

$ make run

3   Testing -- using a client

3.1   Using a Web browser

If you visit the following address into your Web browser:

http://localhost:8080/

You should see the time and date.

3.2   Using cUrl

You should be able to use the following in order to request JSON, HTML, and plain text:

# request HTML
$ curl http://localhost:8080

# request JSON
$ curl -H "Accept: application/json" http://localhost:8080

# request HTML
$ curl -H "Accept: text/html" http://localhost:8080

# request plain text
$ curl -H "Accept: text/plain" http://localhost:8080

3.3   Using a client written in Python

Here are several client programs written in Python that can be used to test our REST application. Each of the following will run under either Python 2 or Python 3.

The following is a simple client written in Python that requests JSON content:

#!/usr/bin/env python

"""
synopsis:
    Request time and date from cowboy REST time server on crow.local.
usage:
    python test01.py
"""

from __future__ import print_function
import sys
if sys.version_info.major == 2:
    from urllib2 import Request, urlopen
else:
    from urllib.request import Request, urlopen
import json


def get_time():
    request = Request(
        'http://crow.local:8080',
        headers={'Accept': 'application/json'},
    )
    response = urlopen(request)
    content = response.read()
    #print('JSON: {}'.format(content))
    # convert from bytes to str.
    content = content.decode()
    content = json.loads(content)
    return content


def test():
    time = get_time()
    print('Time: {}  Date: {}'.format(time['time'], time['date']))


def main():
    test()


if __name__ == '__main__':
    main()

And, here is a slightly more complex client, also written in Python, that can be used to request each of the following content types: JSON, HTTP, and plain text:

#!/usr/bin/env python

"""
usage: test02.py [-h] [--content-type CONTENT_TYPE]

Retrieve the time

optional arguments:
  -h, --help            show this help message and exit
  -c {json,html,plain}, --content-type {json,html,plain}
                        content type to request (json, html, plain).
                        default=json
"""


from __future__ import print_function
import sys
if sys.version_info.major == 2:
    from urllib2 import Request, urlopen
else:
    from urllib.request import Request, urlopen
import json
import argparse


URL = 'http://crow.local:8080'


def get_time(content_type):
    if content_type == 'json':
        headers = {'Accept': 'application/json'}
    elif content_type == 'html':
        headers = {'Accept': 'text/html'}
    elif content_type == 'plain':
        headers = {'Accept': 'text/plain'}
    request = Request(
        URL,
        headers=headers,
    )
    response = urlopen(request)
    content = response.read()
    #print('JSON: {}'.format(content))
    # convert from bytes to str.
    content = content.decode()
    if content_type == 'json':
        content = json.loads(content)
    return content


def test(opts):
    time = get_time(opts.content_type)
    if opts.content_type == 'json':
        print('Time: {}  Date: {}'.format(time['time'], time['date']))
    print('raw data: {}'.format(time))


def main():
    parser = argparse.ArgumentParser(description='Retrieve the time')
    parser.add_argument(
        '-c', '--content-type',
        dest='content_type',
        type=str,
        choices=['json', 'html', 'plain', ],
        default='json',
        help="content type to request.  "
             "(choose from 'json', 'html', 'plain').  "
             'default=json.')
    opts = parser.parse_args()

    test(opts)


if __name__ == '__main__':
    main()

- Dave Kuhlman