Erlang製Webツールキットwebmachine 触ってみた

イントロダクション
bashoがオープンソースとして公開しているwebmachine触ってみました。

webmachineはErlang製のHTTPアプリケーション用フレームワーク...じゃなくてツールキットだそうです。
といってもRuby On Railsみたいにアレもコレもな感じではなく、あくまでサーバーにHTTPもっといえばRESTful APIを被せることを目的としたアプリケーションのようです。触りはじめたばかりでよくわかりませんが。Ruby界隈だとSinatraに近い感じでしょうか。

RubyでコーディングするSinatraに比べて、webmachineではErlang/OTPを使うことになるので、単純に言語やフレームワークの学習コストはwebmachineが高くつきますが、webmachine+Erlang/OTPの場合は軽量プロセス、OTPが

インストール
とりあえず、Quick Startにしたがってインストールしてみます。

githubからコードを取得

$ git clone git://github.com/basho/webmachine

以上w

Webアプリケーション雛形の生成
生成されたディレクトリに移動して、Webアプリケーションの雛形を作る

$ mkdir ~/tmp
$ ./scripts/new_webmachine.sh demo ~/tmp

できたアプリケーションの雛形のディレクトリに移動してビルドする

$ cd ~/tmp/demo
$ make

起動
エラーなくコンパイルが終了したら、HTTPサーバーを起動します。

$ ./start.sh

つらつら〜〜っとメッセージが表示されて起動完了したら、ブラウザから http://localhost:8000 にアクセスします。

Hello, new world

と表示されたら無事起動しています。

リソースの追加
では作ったWebアプリケーションにリソースを追加してみます。
emacsなどのエディタで"priv/dispatch.conf" を開きます。
初期状態では以下の用になっているはず。

{[], demo_resource, []}.

この設定だと、"/"へのリクエストはdemo_resourceというモジュールに振られ、それ以外はNotFoundが返ります。

ではひとつ設定を追加してみます。

%%-*- mode: erlang -*-
{[], demo_resource, []}.
{["hello", '*'], webmachine_hello_resource, []}.

追加した設定により、http://localhost:8000/hello へのリクエストはwebmachine_hello_resourceというモジュールへ振られることになります。
ちなみに最初の"hello"は文字列、そのあとの'*'はアトムですので気をつけてください。"*"では期待通りに動作しません。

今はまだ、そのモジュールがないので作ります。エディタで"src/webmachine_hello_resource.erl"という名前のファイルを新規で作ります。

-module(webmachine_hello_resource).
-export([init/1, to_html/2]).

-include_lib("webmachine/include/webmachine.hrl").

init([]) -> {ok, undefined}.

to_html(ReqData, State) ->
    io:format("~p", [ReqData]),
    {"<html><body>hello</body></html>", ReqData, State}.

再びビルドして起動します。

$ make
$ ./start.sh

ブラウザから http://localhost:8000/hello にアクセスしてみます。

hello

とアクセスされたらおkですね。

先ほどのルート指定では

["hello", '*']

としていました。

ここを何種類か変えてみます。

再び"/priv/dispatch.conf"を開いて、いくつかリソースを追加します。

{[], demo_resource, []}.
{["hello", "china", '*'], webmachine_hello_china_resource, []}.
{["hello", "japan", '*'], webmachine_hello_japan_resource, []}.
{["hello", '*'], webmachine_hello_resource, []}.

以前と同様に、webmachine_hello_china_resource.erlとwebmachine_hello_japan_resource.erlを作ります。

src/webmachine_hello_china.erl

-module(webmachine_hello_china_resource).
-export([init/1, to_html/2]).

-include_lib("webmachine/include/webmachine.hrl").

init([]) -> {ok, undefined}.

to_html(ReqData, State) ->
    io:format("~p", [ReqData]),
    {"<html><body>に〜はお</body></html>", ReqData, State}.

src/webmachine_hello_japan.erl

-module(webmachine_hello_china_resource).
-export([init/1, to_html/2]).

-include_lib("webmachine/include/webmachine.hrl").

init([]) -> {ok, undefined}.

to_html(ReqData, State) ->
    io:format("~p", [ReqData]),
    {"<html><body>こんにちは</body></html>", ReqData, State}.

一旦サーバを止めてビルドしてサーバを起動します。

$ make
& ./start.sh

"http://localhost:8000/hello/china" へのリクエストでは "に〜はお"、http://localhost/8000/hello/japanへのリクエストでは "こんにちは"と表示されたと思いますがどうでしょうか。

RESTfulアプリケーション
で、最後にRESTです。とりあえずこんな感じなんだろうかと書いてみました。
"http://localhost:8000/hello/china"に対して、GET,POST,PUT,DELETEそれぞれのメソッドごとに呼び出される関数を分けています。
こういう使い方が一般的なのかどうかわかりませんが...

-module(webmachine_hello_china_resource).
-export([init/1,
	 allowed_methods/2, 
	 content_types_accepted/2,
	 post_is_create/2,
	 create_path/2,
	 to_html/2, 
	 from_json/2,
	 delete_resource/2]).

-include_lib("webmachine/include/webmachine.hrl").

init([]) -> {ok, undefined}.

%%%===================================================================
%%% Allow Methods and Accepted Content Types
%%%===================================================================

%% 許可するHTTPメソッド
allowed_methods(Req, State) ->
    {['GET', 'POST', 'PUT', 'DELETE'], Req, State}.

%% 許可するcontent type
content_types_accepted(Req, State) ->
    {[{"text/html", to_html},
      {"application/json", from_json}], Req, State}.

%%%===================================================================
%%% Post setting
%%%===================================================================

%% POSTリクエストは新規作成として扱うかどうか
post_is_create(Req, State) ->
    {true, Req, State}.

%% post_is_create/2がtrueを返したときに呼ばれる
%% 新しいリソースへのパスを返す(このパスをLocationヘッダに埋め込んでかえす)
create_path(Req, State) ->
    {"/new/path", Req,State}.

%%%===================================================================
%%% Handler Functions
%%%===================================================================

%% GET
to_html(#wm_reqdata{method = 'GET'} = Req, State) ->
    io:format("http request(method:~p).~n", [Req#wm_reqdata.method]),
    {"<html><body>に〜はお</body></html>", Req, State}.

%% POST
from_json(#wm_reqdata{method = 'POST'} = Req, State) ->
    %% なにか新規作成処理する...
    io:format("json post request(method:~p).~n", [Req#wm_reqdata.method]),
    {true, Req, State};

%% PUT
from_json(#wm_reqdata{method = 'PUT'} = Req, State) ->
    %% なにか更新処理する...
    io:format("json put request(method:~p).~n", [Req#wm_reqdata.method]),
    {true, Req, State}.

%% DELETE
delete_resource(Req, State) ->
    %% 削除処理する...
    io:format("delete request(method:~p).~n", [Req#wm_reqdata.method]),
    {true, Req, State}.

で、ここまできて今更ですが、わかりやすいスライドありました。

それからVoluntas氏によるGistが 
https://gist.github.com/voluntas/4363064