Erlang製のWebフレームワークMochiWebをさらっと触ってみたでござるの巻(その2)

前回に引き続き、Erlang製のWebフレームワークMochiWebをもうちょっと触ってみます。
前回のソースでは、ブラウザからどのパスにアクセスしても同じ結果でした。

Erlang製のWebフレームワークMochiWebをさらっと触ってみたでござるの巻

例えば
http://localhost:9999/helo
でも
http://localhost:9999/helo/world?name=shin

でも結果は同じ。
そこで今回はURLによって異なる動作をするようにソースに手を加えてみようと思います。

パスの取得の確認
まずはパスの取得がどうなっているのか確認する為に前回と同じ場所で以下のようなソースを用意します。

-module(action).

-export([start/1, stop/0]).

start(Port) ->
    mochiweb_http:start([{port, Port}, {loop, fun dispatch/1}]).

stop()->
    mochiweb_http:stop().

dispatch(Req) ->
    Path = Req:get(path),
    Req:ok({"text/html", Path}).

あとはノード上でコンパイルして走らせて...

$ erl -pa ../mochiweb/ebin
1> c(action).
{ok,action}

2> action:start(9999).
{ok,<0.38.0>}

ブラウザでアクセスしてみます。まずは http://localhost:9999

ふむ。パスが無い時は"/"のみですか。

では http://localhost:9999/helo だと?

なるほど。"/helo"らしい。

では http://localhost:9999/helo/world は?

ふむふむ。

では最後にパラメータを付加したらどうなる? http://localhost:9999/helo/world?name=shin

なる。パラメータは取り除いてくれるのですね。

ちょっと汎用化
というわけで、ちょっとだけ汎用化してみた。

-module(action).

-export([start/1, stop/0]).

start(Port) ->
    mochiweb_http:start([{port, Port}, {loop, fun dispatch/1}]).

stop()->
    mochiweb_http:stop().

%%%===================================================================
%%% Reqest Dispatcher
%%%===================================================================

dispatch(Req) ->
    Path = Req:get(path),

    case string:tokens(Path, "/") of
        [Controller, Action | UrlParams] ->
            handle(Controller, Action, UrlParams, Req);
        [Controller] ->
            handle(Controller, "index", [], Req);
        [] ->
            handle("top", "index", [], Req)
    end.

%%%===================================================================
%%% Request Handlers
%%%===================================================================

handle("top", "index", _, Req) ->
    Req:ok({"text/html", "Welcome!"});

handle("helo", "index", _, Req) ->
    Req:ok({"text/html", "hello! mochiweb beginner! :-)"});

handle("helo", "world", _, Req) ->
    Req:ok({"text/html", "hello! mochiweb beginner! from world! :-)"});

handle(UnknownC, UnknownA, _, Req) ->
    Req:respond({404, [{content_type, "text/plain"}], 
                 subst("Unknown Route ~s:~s", [UnknownC, UnknownA])}).

%%%===================================================================
%%% Local Functions
%%%===================================================================

subst(Template, Values) ->
    list_to_binary(lists:flatten(io_lib:fwrite(Template, Values))).

*このページの最後にReq:not_found()を使用した改良版をのせてます。

ブラウザで

にアクセスするとそれぞれのコンテンツが表示されます(●ω●)

また、どの定義にもマッチしなかったら "handle(UnknownC, UnknownA, _, Req) " にマッチして404Unknownを返します。
http://localhost:9999/this/is/unknown

このへんのルーティングに関してはErlangのパターンマッチが助けてくれます。
関数定義の引数のところに変数名ではなくマッチする値そのものを置いて、それがら全てマッチする関数が実行されるので冗長なcase文や読み解きにくくなりがちなメタな方法をとらなくてもすんなり書けますね。

....パターンマッチかわいいよパターンマッチ(*´∀`)
次回はもうちょっと汎用化をすすめてみましょう(*´∀`)

追記(2011/11/20)
Voluntasさんより「req:not_found/1 があるよ」とのアドバイスがありましたのでmochiwebに含まれるmochiweb_request.erlを覗いてみると確かに404を返す為の関数がありました^^;

%% @spec not_found() -> response()
%% @doc Alias for <code>not_found([])</code>.
not_found() ->
    not_found([]).

%% @spec not_found(ExtraHeaders) -> response()
%% @doc Alias for <code>respond({404, [{"Content-Type", "text/plain"}
%% | ExtraHeaders], &lt;&lt;"Not found."&gt;&gt;})</code>.
not_found(ExtraHeaders) ->
    respond({404, [{"Content-Type", "text/plain"} | ExtraHeaders],
             <<"Not found.">>}).

引数にヘッダーへの追加を持たせられるようですが、特に必要なければReq:not_found()を呼べば良さそうですね。
というわけでそれを適用したコードは以下のようになります。

-module(action).

-export([start/1, stop/0]).

start(Port) ->
    mochiweb_http:start([{port, Port}, {loop, fun dispatch/1}]).

stop()->
    mochiweb_http:stop().

%%%===================================================================
%%% Reqest Dispatcher
%%%===================================================================

dispatch(Req) ->
    Path = Req:get(path),

    case string:tokens(Path, "/") of
        [Controller, Action | UrlParams] ->
            handle(Controller, Action, UrlParams, Req);
        [Controller] ->
            handle(Controller, "index", [], Req);
        [] ->
            handle("top", "index", [], Req)
    end.

%%%===================================================================
%%% Request Handlers
%%%===================================================================

handle("top", "index", _, Req) ->
    Req:ok({"text/html", "Welcome!"});

handle("helo", "index", _, Req) ->
    Req:ok({"text/html", "hello! mochiweb beginner! :-)"});

handle("helo", "world", _, Req) ->
    Req:ok({"text/html", "hello! mochiweb beginner! from world! :-)"});

handle(_, _, _, Req) ->
    Req:not_found().

このほうが意図が明確でわかりやすいですね。