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