Simple Webmachine - Proper HTTP Resources

Published Monday, April 13, 2009 by Bryan

Update: This post sparked my Webmachine nerve in such a way that I wrote four more posts about extending the resource described below. Read them if you'd like to see how this resource can evolve.

This morning I read a post about CouchDB's HTTP Handlers. Jón Grétar Borgþórsson demonstrates how one might implement a handler that serves a JSON structure describing the OS environment variables.

I thought that two additional pieces of code might be interesting. I'll lead with an example showing the most likely way this resource would have been coded for Webmachine:

%% dispatch:
%% {["_env"],      env_resource, []}.
%% {["_env", env], env_resource, []}.

-module(env_resource).
-export([init/1, content_types_provided/2, resource_exists/2, to_json/2]).
-include_lib("webmachine/include/webmachine.hrl").

init(_) -> {ok, undefined}.

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

resource_exists(RD, Ctx) ->
    case wrq:path_info(env, RD) of
        undefined ->
            Result = [ list_to_tuple(string:tokens(E, "="))
                       || E <- os:getenv() ],
            {true, RD, {struct, Result}};
        Env ->
            case os:getenv(Env) of
                false  -> {false, RD, Ctx};
                Result -> {true, RD, Result}
            end
    end.

to_json(RD, Result) ->
    {mochijson:encode(Result), RD, Result}.

The biggest difference to note between the Webmachine version and the CouchDB version is that the proper HTTP status codes are returned from the Webmachine resource. The CouchDB handler returns 500 or 405 when the requested resource is not found. The proper status for this case is 404. Webmachine knows this, and handles the choice automatically.

A little extra emphasis, I think, is appropriate here: Webmachine chooses the proper response code for you. You define methods that describe the state of your resource (like whether or not it exists, what methods it allows, etc.), and Webmachine negotiates the muck of HTTP.

As a bonus, the Webmachine resource is the same length, while at the same time being less dense and more readable.

Let's not get hasty, though. If there is a really good reason for returning an alternate status code, Webmachine won't get in your way. To prove it, here's a Webmachine resource that (as near as I can tell, I'm not a CouchDB guru) returns exactly the same statuses as the CouchDB handler:

%% dispatch:
%% {["_env2", '*'], env_resource, []}.

-module(env2_resource).
-export([init/1, content_types_provided/2, resource_exists/2, to_json/2]).
-include_lib("webmachine/include/webmachine.hrl").

init(_) -> {ok, undefined}.

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

resource_exists(RD, Ctx) ->
    case string:tokens(wrq:disp_path(RD), "/") of
        [] ->
            Result = [ list_to_tuple(string:tokens(E, "="))
                       || E <- os:getenv() ],
            {true, RD, {struct, Result}};
        [Env] ->
            case os:getenv(Env) of
                false ->
                    {{halt, 500},
                     wrq:set_resp_header(
                       "Content-type", "application/json",
                       wrq:append_to_response_body(
                         mochijson:encode(
                           {struct, [{error, "not_found"},
                                     {reason, "Variable Not Found"}]}),
                         RD)),
                     Ctx};
                Result ->
                    {true, RD, Result}
            end;
        _ ->
            {{halt, 405},
             wrq:set_resp_header("Allow", "GET,HEAD", RD),
             Ctx}
    end.

to_json(RD, Result) ->
    {mochijson:encode(Result), RD, Result}.

Categories: Erlang Webmachine