Contents
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.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()