eCloudEdit: Erlang, Webmachine and Backbone.js

To experiment with using a pure client-side rendering talking to an Erlang backend I’ve taken James Yu’s CloudEdit tutorial an app written with Backbone.js and Rails and converted the backend to use Webmachine and CouchDB. You can see eCloudEdit in action here. The Backbone.js code is the same so to understand that please see James’ post, here I’ll describe the Erlang backend.

To begin with we setup two applications, one for handling the web interaction and a second for handling the database interaction. We end up with this directory layout:

  |-eCloudEdit
   |-bin
   |-config
   |-doc
   |-lib
   |---ece_db
   |-----doc
   |-----ebin
   |-----include
   |-----src
   |---ece_web
   |-----doc
   |-----ebin
   |-----include
   |-----priv
   |-------images
   |-------javascripts
   |---------controllers
   |---------models
   |---------views
   |-------stylesheets
   |-----src

Under ece_web/priv is where all the html and Javascript from CloudEdit is placed, with nothing changed. To serve up the static content through Webmachine we use this resource which we won’t worry about in this article. Lets look at how webmachine deciced where to route requests. This is handled by a set of dispatch rules that are passed into Webmachine on startup. We can see how this is done by looking at ece_web_sup and how the supervisor loads Webmachine:

-spec init(list()) -> {ok, {SupFlags::any(), [ChildSpec::any()]}} |
                          ignore | {error, Reason::any()}.
init([]) ->
    WebChild = {webmachine_mochiweb,
                {webmachine_mochiweb, start, [config()]},
                permanent, 5000, worker, dynamic},

    RestartStrategy = one_for_one,
    MaxRestarts = 3,
    MaxSecondsBetweenRestarts = 10,
    SupFlags = {RestartStrategy, MaxRestarts, MaxSecondsBetweenRestarts},

    {ok, {SupFlags , [WebChild]}}.

config() ->
    {ok, IP} = application:get_env(webmachine_ip),
    {ok, Port} = application:get_env(webmachine_port),
    {ok, App}= application:get_application(),
    LogDir = code:priv_dir(App) ++ "/logs",
    {ok, Dispatch} = file:consult(filename:join([code:priv_dir(App), "dispatch"])),

    [{ip, IP},
     {port, Port},
     {log_dir, LogDir},
     {backlog, 128},
     {dispatch, Dispatch}].

The dispatch terms are loaded from a file dispatch in the application’s priv directory. For this app the dispatch file contains:

{["documents", id], ece_resource_documents, []}.
{["documents"], ece_resource_documents, []}.
{['*'], ece_resource_static, []}.

There are two resources, one for handling the requests for creating, updating and viewing documents and one for serving up all other requests (static html, js and css files). The first rule matches paths like, /documents/foo, id is set to foo and the request is sent to the documents resource. If there is nothing after /documents it is still sent to the documents resource but there is no id.

Webmachine is essentially a REST toolkit. You build resources by defining functions that handle the different possible HTTP requests. For this webapp we’ll only be dealing with GET, POST and PUT. GET is used if we’d like to retrieve information on documents, POST is for creating a new document and PUT is for updating a document.

-record(ctx, {db}).

init([]) ->
    {ok, PID} = ece_db_sup:start_child(),
    {ok, #ctx{db=PID}}.

allowed_methods(ReqData, Ctx) ->
    {['HEAD', 'GET', 'POST', 'PUT'], ReqData, Ctx}.

content_types_accepted(ReqData, Ctx) ->
    {[{"application/json", from_json}], ReqData, Ctx}.

content_types_provided(ReqData, Ctx) ->
    {[{"application/json", to_json}], ReqData, Ctx}.

finish_request(ReqData, Ctx) ->
    ece_db_sup:terminate_child(Ctx#ctx.db),
    {true, ReqData, Ctx}.

To handle GET requests we implemented the to_json function we specified in content_types_provided to handle GET requests for json data.

to_json(ReqData, Ctx) ->
    case wrq:path_info(id, ReqData) of
        undefined ->
            All = ece_db:all(Ctx#ctx.db),
            {All, ReqData, Ctx};
        ID ->
            JsonDoc = ece_db:find(Ctx#ctx.db, ID),
            {JsonDoc, ReqData, Ctx}
    end.

wrq:path_info is used to find the value of id, which we set in the dispatch file, if it is undefined we know the request is for all documents, while if it has a value we know to find the contents of the document with that id and return its contents. We’ll see the content of ece_db:all/1 and ece_db:find/2 in the next article. Just know they both return JSON data structures.

Now we must support POST for creating documents or we have nothing to return to a GET request.

process_post(ReqData, Ctx) ->
    [{JsonDoc, _}] = mochiweb_util:parse_qs(wrq:req_body(ReqData)),
    {struct, Doc} = mochijson2:decode(JsonDoc),
    NewDoc = ece_db:create(Ctx#ctx.db, {Doc}),
    ReqData2 = wrq:set_resp_body(NewDoc, ReqData),
    {true, ReqData2, Ctx}.

wrq:req_body/1 returns the contents of the body sent in the HTTP request. Here it is the conents of the document to store. We decode it to an Erlang data structure and pass it to the ece_db app for inserting into the database. After inserting to the database the create/2 function returns the new document with a new element id (in this case generated by CouchDB). This is required so we know the document’s id which is used by the Backbone.js frontend. In order to return it from the POST request we must set response body to the contents of the document with wrq:set_resp_body/2

Lastly, updating documents requires support for PUT. In contents_type_accepted/2 we’ve specified that PUT requests with JSON content is sent to the function from_json/2:

from_json(ReqData, Ctx) ->
    case wrq:path_info(id, ReqData) of
        undefined ->
            {false, ReqData, Ctx};
        ID ->
            JsonDoc = wrq:req_body(ReqData),
            {struct, Doc} = mochijson2:decode(JsonDoc),
            NewDoc = ece_db:update(Ctx#ctx.db, ID, Doc),
            ReqData2 = wrq:set_resp_body(NewDoc, ReqData),
            {true, ReqData2, Ctx}
    end.

If this request was not routed through the first rule in our dispatch file it does not have an id and thus can not be an update. When this happens we return false so the frontend is aware something has gone wrong. For requests containing an id we pass the contents of the requests body to ece_db‘s update/2 function.

In the next post I’ll show how ece_db is implemented with Couchbeam for reading and writing the documents to CouchDB on Cloudant.

About these ads

9 thoughts on “eCloudEdit: Erlang, Webmachine and Backbone.js

  1. Adam Walters says:

    Great article, thanks!

    I’ve been playing with Webmachine recently and the one thing that i have been unable to get a solid answer for is when to use post_is_create and create_path vs process_post. What was your reason for using process_post here? Everything seems cleaner going the process_post route, but it _seems_ like creates should use post_is_create…

    Any insight would be great. Thanks!

    • Tristan Sloughter says:

      Thats actually funny, I’d been meaning to put up a post on that exact question. I’ll try to do it soon. But I can give a short answer here.

      ‘post_is_create’ is for instances that the path of resource being posted to does not “exist”. So for example if to add a book we did:

      POST /book/Title

      That title would not exist yet and would be created, so post_is_create. If instead we do:

      POST /book

      And provide some json (or whatever) and on the server side create the ID we’d use to GET the book we simply use process_post.

      Usually I’d say just use PUT (edit) if you are providing the identifier in the url.

    • That’s true. What I’m hoping for is a saardtnd way of factoring out the differences between efcgi, httpd, pico or anything else, and just have a simple API that handles web requests that anyone can implement to and deploy in different scenarios.

  2. Some parts may be difficult – didenpeng on how the erlang virtual machine handles the message passing between processes.but yes, it could be very interesting to do something similar to vmkit with it.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s