BeepBeep, CouchDB and a Trivial Blog
One of the core concepts of Django is that of loose coupling. The Django framework is a well-stocked toolkit filled with great tools which work well together, but it makes no effort to force its most-favored software into your project: you can ignore the existing tools and bring your own, if you so choose.
Comparing BeepBeep to Django, one might suggest that the term micro framework is a polite way of saying that BeepBeep is an empty toolkit. BeepBeep is able and willing glue between your components, but has a strong Bring-Your-Own-Tools philosophy.
In this tutorial we'll look at a powerful tool you should consider pairing with BeepBeep: CouchDB
Using CouchDB, we're going to create a very simple blog where we'll create blog posts by using a form to send HTTP POSTs, and have simple views for listing all the posts and viewing individual posts as well. It's unquestionably feature light, but perhaps a bit concept heavy, so let's get started1.
You can look at the completed code in the BeepBeep Examples repository on GitHub.
Dependencies & Setup
First, follow the BeepBeep installation instructions if it isn't already installed.
Use
new_beep.erl
to create the project files forBB_Blog
.cd ~/git/beepbeep/ ./script/new_beep.erl BB_Blog ../BB_Blog
Install erlang_couchdb from its GitHub repository:
git clone git://github.com/ngerakines/erlang_couchdb.git
Then
make
it and copy the resulting beams into theBB_Blog/deps/
folder.cd erlang_couchdb make mkdir ../BB_Blog/bb_blog/deps/erlang_couchdb mkdir ../BB_Blog/bb_blog/deps/erlang_couchdb/ebin cp ebin/* ../BB_Blog/bb_blog/deps/erlang_couchdb/ebin/
Please be aware that
erlang_couchdb
requires Erlang R12B5 or later, so you may need to update your Erlang installation.The last step in setup is to verify that the dependencies have been installed correctly. To do this, first we need to build the
BB_Blog
application.cd ~/git/BB_Blog/bb_blog/ make
Then start CouchDB in the background.
couchdb &
And start the
BB_Blog
app server.cd ~/git/BB_Blog/bb_blog/ ./start-server.sh
And then test out creating a CouchDB database.
2> erlang_couchdb:create_database({"localhost", 5984}, "test_db2"). ok
This means that CouchDB is running,
erlang_couchdb
is running, BeepBeep is running, and everything is gravy. Excellent.If something looks different, please post a comment with the error message you received.
Piecing Together Some CouchDB Functionality
In order to use CouchDB as our document store, we'll need to create a database. In order to retrieve documents we'll want to create a CouchDB view. We might also want a way to clear out the current documents, a way to retrieve a given document, and so on.
We'll house this functionality in the file src/bb_blog_utils.erl
.
I am going to skim over the details of this file, so if you haven't used CouchDB before,
you may want to take a look at the documentation.
-module(bb_blog_utils).
-export([create_db/0, delete_db/0, info_db/0, reset_db/0]).
-export([create_document/3, delete_document/2, documents/0, documents/2, document/1]).
-define(COUCHDB_CONN, {"localhost", 5984}).
-define(BB_BLOG_DB, "bb_blog").
% Create a database in CouchDB, and then generate
% a CouchDB view for querying documents by slug.
create_db() ->
erlang_couchdb:create_database(?COUCHDB_CONN, ?BB_BLOG_DB),
erlang_couchdb:create_view(?COUCHDB_CONN, ?BB_BLOG_DB, "_blog", <<"javascript">>,
[{<<"entries">>, <<"function(doc) { emit(doc.slug, doc) }">>}]).
% Drop the database, including all documents and views.
delete_db() ->
erlang_couchdb:delete_database(?COUCHDB_CONN, ?BB_BLOG_DB).
% Retrieve the current status of the database.
info_db() ->
io:format("~p",[erlang_couchdb:database_info(?COUCHDB_CONN, ?BB_BLOG_DB)]).
% Delete and then recreate the database.
reset_db() ->
delete_db(),
create_db().
% Create a blog entry in the database.
create_document(Title, Slug, Body) ->
erlang_couchdb:create_document(?COUCHDB_CONN, ?BB_BLOG_DB,
[{<<"title">>, list_to_binary(Title)},
{<<"slug">>, list_to_binary(Slug)},
{<<"body">>, list_to_binary(Body)}]).
% Delete a blog entry from the database.
delete_document(Id, Rev) ->
erlang_couchdb:delete_document(?COUCHDB_CONN, ?BB_BLOG_DB, Id, Rev).
% Retrieve up to the first ten documents in the _blog/entries view.
documents() ->
documents(0,10).
% Retrieve documents Offset - Offset+Limit from _blog/entries view.
documents(Offset, Limit) ->
Attrs = [{"skip",Offset},{"limit",Limit}],
Resp = erlang_couchdb:invoke_view(?COUCHDB_CONN, ?BB_BLOG_DB, "_blog", "entries",Attrs),
{json, {struct, Data}} = Resp,
Data.
% Retrieve a given document by its slug.
document(Slug) ->
Attrs = [{"key",lists:concat([""",Slug,"""])}],
Resp = erlang_couchdb:invoke_view(?COUCHDB_CONN, ?BB_BLOG_DB, "_blog", "entries",Attrs),
{json, {struct, Data}} = Resp,
Rows = proplists:get_value(<<"rows">>, Data),
[{struct, Row}] = Rows,
Row.
Now go ahead and make
the application again,
make
You can test the interface out quickly by running ./start-server.sh
and then
trying these commands:
26> bb_blog_utils:create_db().
{json,{struct,[{<<"error">>,<<"conflict">>},
{<<"reason">>,<<"Document update conflict.">>}]}}
27> bb_blog_utils:delete_db().
ok
28> bb_blog_utils:create_db().
{json,{struct,[{<<"ok">>,true},
{<<"id">>,<<"_design/_blog">>},
{<<"rev">>,<<"1-800947609">>}]}}
29> bb_blog_utils:create_document("Title","slug","<p>some text</p>").
{json,{struct,[{<<"ok">>,true},
{<<"id">>,<<"94f2f8eb0a94959454c3bfdbcdaa54e0">>},
{<<"rev">>,<<"1-304401008">>}]}}
30> bb_blog_utils:info_db().
{ok,[{<<"db_name">>,<<"bb_blog">>},
{<<"doc_count">>,2},
{<<"doc_del_count">>,0},
{<<"update_seq">>,2},
{<<"purge_seq">>,0},
{<<"compact_running">>,false},
{<<"disk_size">>,14595},
{<<"instance_start_time">>,<<"1247033421520632">>}]}ok
Now our interface to CouchDB is prepared, and we just need to do... everything else.
Teaching start-server.sh
Some New Tricks
The first step in the reworking will be to make the start-server.sh
script
a bit more flexible. After extended it will let us create the CouchDB database,
check the status of the database, and also reset the database, all from the
command line.
mv start-server.sh manage.sh
emacs manage.sh
Replace the current code with this:
#!/bin/sh
cd `dirname $0`
case $1 in
#!/bin/sh
cd `dirname $0`
case $1 in
server)
exec erl -pa $PWD/ebin $PWD/deps/*/ebin -boot start_sasl -s reloader \
-s bb_blog -detached
;;
dev_server)
exec erl -pa $PWD/ebin $PWD/deps/*/ebin -boot start_sasl -s reloader \
-s bb_blog -detached
;;
create_db)
echo "create database..."
exec erl -pa $PWD/ebin $PWD/deps/*/ebin -run bb_blog_utils create_db \
-run init stop -noshell
;;
delete_db)
echo "delete database..."
exec erl -pa $PWD/ebin $PWD/deps/*/ebin -run bb_blog_utils delete_db \
-run init stop -noshell
;;
reset_db)
echo "reset database..."
exec erl -pa $PWD/ebin $PWD/deps/*/ebin -run bb_blog_utils reset_db \
-run init stop -noshell
;;
info_db)
echo "info for database..."
exec erl -pa $PWD/ebin $PWD/deps/*/ebin -run bb_blog_utils info_db \
-run init stop -noshell
;;
*)
echo "Usage: $0 {server|dev_server|create_db|delete_db|reset_db|info_db}"
exit 1
esac
exit 0
After saving the file, you can test out the new manage.sh
script.
will-larsons-macbook:bb_blog will$ ./manage.sh create_db
create database...
will-larsons-macbook:bb_blog will$ ./manage.sh reset_db
reset database...
will-larsons-macbook:bb_blog will$ ./manage.sh info_db
info for database...
{ok,[{<<"db_name">>,<<"bb_blog">>},
{<<"doc_count">>,0},
{<<"doc_del_count">>,0},
{<<"update_seq">>,0},
{<<"compact_running">>,false},
{<<"disk_size">>,4096}]}
will-larsons-macbook:bb_blog will$ ./manage.sh server
Note that the distinction between server
and dev_server
is that server
runs
in -detached
mode, and dev_server
runs in the current console.
Extending BB_Blog
to Display Blog Entries
To display blog entries we will need to make two controllers,
two implementations of handle_request
and three templates.
First let's create the index page, which will allow users to view all existing
blog entries. Go ahead and open up src/home_controller.erl
and
replace the curernt contents with:
-module(home_controller,[Env]).
-export([handle_request/2]).
handle_request("index",[]) ->
Data = bb_blog_utils:documents(),
RawRows = proplists:get_value(<<"rows">>, Data),
Rows = lists:map(fun({struct, X}) ->
{struct, Row} = proplists:get_value(<<"value">>, X),
CleanRow = lists:map(fun({Key, Value}) ->
{list_to_atom(binary_to_list(Key))
,Value}
end, Row),
CleanRow
end, RawRows),
{render,"home/index.html",[{rows, Rows}]}.
This is a bit ugly, largely as a result of the format for deserialized JSON. It could undoubtedly be cleaned up a bit, but very simply it:
- Retrieves the documents from CouchDB.
- Transforms the CouchDB documents into an ErlyDLT friendly format.
- Renders the
home/index.html
template with the transformed documents.
A quick debugging tip: if you add lines like:
error_logger:info_report(Data),
to your BeepBeep code and run in dev_server
mode (i.e. with
the server attached to a console), then you'll be able to see the
incoming data quite handily.
Sure printing to console isn't the most advanced debugging technique, but it's rather handy when you're stuck trying to remember the awkward format of those deserialized JSON datastructures.
Relying on our CouchDB utility functions, the logic here is pretty concise. Then
we need to tweak views/base.html
to clean up the default template a bit:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<title>BB_Blog: Rational Exuberance</title>
<link href="/stylesheets/style.css" rel="stylesheet" type="text/css"/>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>
And also tweak views/home/index.html
:
{% extends "../base.html" %}
{% block content %}
{% for row in rows %}
<div class="doc">
<h1><a href="/blog/view/{ { row.slug }}/">{ { row.title }}</a></h1>
</div>
{% endfor %}
{% endblock %}
Then make
the code and navigate to http://localhost:8000.
It should display a simple list of blog entries. Huzzah!
This is a good time to throw away views/home/show.html
, since we don't
need it.
rm views/home/show.html
The last step in displaying the blog entries is we want to create a controller
and view pair to display the individual entries based on their slug. Go ahead
and create src/blog_controller.erl
.
-module(blog_controller,[Env]).
-export([handle_request/2]).
handle_request("view",[Slug]) ->
Resp = bb_blog_utils:document(Slug),
error_logger:info_report(Resp),
{struct, Doc} = proplists:get_value(<<"value">>, Resp),
CleanDoc = lists:map(fun({K,V}) ->
{list_to_atom(binary_to_list(K)), V}
end, Doc),
{render,"blog/view.html",[{doc, CleanDoc}]};
Since we're only dealing with one result, the cleaning process is a bit more compact this time.
Next, we need to create the blog/view.html
view.
mkdir views/blog
emacs views/blog/view.html
And populate it with these contents:
{% extends "../base.html" %}
{% block content %}
<div id="doc">
<h1><a href="/blog/view/{ { doc.slug }}/">{ { doc.title }}</a></h1>
<div class="body">
{ { doc.body }}
</div>
</div>
{% endblock %}
Then make
the source once more, jump on over to http://localhost:8000, and then click on one of the (or, more likely, the) document we created earlier
to view it in its full glory.
The One Where We Can Create Blog Entries
Now that we can view blog entries, it would be mighty helpful if we could actually create blog entries as well. So, let's get cracking on that.
Note that this section introduces something a bit new, which is how to manage data received in POST requests via BeepBeep.
Return to the src/blog_controller.erl
and add this request handler:
handle_request("create",[]) ->
Title = beepbeep_args:get_param("title", Env),
Slug = beepbeep_args:get_param("slug", Env),
Body = beepbeep_args:get_param("body", Env),
case lists:member("", [Title, Slug, Body]) of
true ->
{render, "blog/create.html",[]};
false ->
bb_blog_utils:create_document(Title, Slug, Body),
{redirect, lists:concat(["/blog/view/",Slug,"/"])}
end.
You'll also need to change the ending .
to an ending ;
on
the existing implementation of handle_request
.
Then go ahead and create the views/blog/create.html
template,
{% extends "../base.html" %}
{% block content %}
<div id="create">
<h2>Create New Blog Entry</h2>
<form method="POST" action="/blog/create/">
<label for="title">Title:</label><input name="title"><br/>
<label for="slug">Slug:</label><input name="slug"><br/>
<label for="body">Body</label><br/>
<textarea name="body"></textarea><br/>
<input type="submit" value="Create">
</form>
</div>
{% endblock %}
With that, make
the code, and head off to
http://localhost:8000/blog/create/
to get started with your blogging... your painfully limited and awkward
blogging.
The End
With this third BeepBeep tutorial, we're approaching a point where we've covered many of the tools needed to create a legitimate web application. Throughout this tutorial I cut away features that I wanted to cover, because this was really about showcasing using CouchDB with BeepBeep, rather than trying to build a servicable blog application. As such, due apologies for the minimal ending state, but hopefully it was educational nonetheless.
You can grab the source for BB_Blog in the BeepBeep Examples repository,
under the BB_Blog
directory. As always, glad to hear any comments or
complaints, and I'll figure out where this thread of BeepBeep tutorials is headed.
If you want a more full-featured blog powered by BeepBeep, you could take the CouchDB aspects of this tutorial and merge them with the blog example in the
beepbeep/examples
folder, which contains quite a bit more functionality (although, even so checks in as an extremely lightweight blogging client :). Beyond that, it's a great example of BeepBeep techniques worth reading on its own merits.↩