更新: | 2013-03-9 |
---|---|
バージョン: | 0.2.1 |
作者: | @voluntas |
URL: | http://voluntas.github.com/ |
Webmachine のバージョンは 1.9.3 で確認しています
- リソース機能説明を追加する
- RESTful API について詳しくない人が書いているので、間違っていたらごめんなさい。
- 確認は mochiweb を 2.3.2 に上げたオリジナル版で行っています。
Basho Technologies, Inc. が公開している RESTful API 向けの Web フレームワークです。 テンプレートエンジンや O/R マッパーなどこじゃれたモノは一切ついていません。
さらに JSON 変換についても mochiweb のおまけがついているだけです。
ただ、RESTful な API を作成する場合はとても理にかなっています。さらに強力なデバッグ機能がついています。
HTTP サービスはステートフルであるという元に考えられた仕組みです。
- RESTful な API 向けのフレームワーク
- テンプレートエンジン
- データベース連携
- セッション管理
- 認証
- 管理画面
その他諸々何も提供してくれません
Webmachine は単独でも動くように作られていますし、簡単にデモアプリが作れるようなスクリプトも付属しています。
Erlang がインストールされていること前提です
ここでは Basho 公式の Webmachine を使います。
準備:
# まずはディレクトリを作ります $ mkdir try-webmachine $ cd try-webmachine # webmachine を取ってきてコンパイルします $ git clone git://github.com/basho/webmachine.git $ cd webmachine $ make # webmachine プロジェクトを生成します $ ./scripts/new_webmachine.sh snowflake ../ $ cd ../snowflake $ ls -a ./ ../ Makefile README deps/ priv/ rebar* rebar.config src/ start.sh* # webmachine プロジェクトをコンパイルして起動してみます $ make $ ./start.sh
アクセスしてみる:
$ http http://localhost:8000/ HTTP/1.1 200 OK Content-Length: 42 Content-Type: text/html Date: Sat, 05 Jan 2013 07:31:03 GMT Server: MochiWeb/1.1 WebMachine/1.9.2 (someone had painted it blue) <html><body>Hello, new world</body></html>
200 が返ってきていることを確認します。
例を作るために POST 以外を受け付けない仕様にしてみましょう。
$ vim src/snowflake_resource.erl
修正前
%% @author author <author@example.com>
%% @copyright YYYY author.
%% @doc Example webmachine_resource.
-module(snowflake_resource).
-export([init/1, to_html/2]).
-include_lib("webmachine/include/webmachine.hrl").
init([]) -> {ok, undefined}.
to_html(ReqData, State) ->
{"<html><body>Hello, new world</body></html>", ReqData, State}.
修正後
%% @author author <author@example.com>
%% @copyright YYYY author.
%% @doc Example webmachine_resource.
-module(snowflake_resource).
-export([init/1, to_html/2]).
-export([allowed_methods/2]).
-include_lib("webmachine/include/webmachine.hrl").
init([]) -> {ok, undefined}.
allowed_methods(RD, Ctx) ->
{['POST'], RD, Ctx}.
to_html(ReqData, State) ->
{"<html><body>Hello, new world</body></html>", ReqData, State}.
コンパイルして再度起動:
$ ./rebar compile $ ./start.sh
GET を送ってみて怒られる:
$ http http://localhost:8000/ HTTP/1.1 405 Method Not Allowed Allow: POST Content-Length: 0 Date: Sat, 05 Jan 2013 07:39:30 GMT Server: MochiWeb/1.1 WebMachine/1.9.2 (someone had painted it blue)
おまけで Heroku で Webmachine を動かす方法を追記しておきます。
古い記事ですが、まだ動くと思います http://voluntas.hatenablog.com/entry/20111218/1324167999
url: | https://github.com/basho/webmachine/wiki/Dispatching |
---|
priv/dispatch.conf から呼ぶのが標準です。動的に追加削除も可能ではりますこのあたりは後ほど紹介します。
%% ガードなし
{PathSpec, Resource, Args}
%% ガードあり
%% Guard は {module(), atom()} または function() で指定可能です。
{PathSpec, Guard, Resource, Args}
PathSpec の書き方は文字列、atom の二種類だけです。 / で区切られた値を表現します。 atom の定義で特殊なのが '*' という「全部」を定義できます。
まず /spam にマッチするディスパッチルールを書いてみます。
{["spam"], example_resource, []}
PathSpec はリストで表現します。上の URL の場合 /spam/eggs はマッチせず /spam のみがマッチします。
/spam/eggs がマッチするディスパッチルールを書いてみます
{["spam", '*'], example_resource, []}
{["spam", "eggs"], example_resource, []}
'*' は残り全部という意味です。もう一つそこに入る値を指定して取得するという方法もあります。
{["spam", "eggs", bacon], example_resource, []}
この場合 bacon という atom にマッチする値が wrq:path_info(bacon, RD) で取得できます。
/spam/eggs/ham という URL の場合は "ham" が戻り値になります。
もう一つリソースモジュールを指定する前にガードを指定出来ます。
このガードは is_post/1 など ReqData に対してフィルタリングする仕組みです。
ただ is_post するくらいなら allowed_methods で十分だと思ったりしないでもないです ... 。
公式ドキュメントでは priv/dispatch.conf にディスパッチルールを定義する事を推奨しています。 ただ、これ Webmachine 側で上手いことやってくれるわけでは無いんです。
以下のコードを時前で書いて取得する必要があります。デフォルトでは dispatch.conf みるようにしてくれてもイイ気がしないでも無かったりしますが、もともとは組み込み前提で考えられている事もあるのでそこは自分で指定してということなんでしょうね。ただ便利関数としては用意していい気がします。
公式に書いてある関数
{ok, Dispatch} = file:consult(
filename:join([filename:dirname(code:which(?MODULE)),
"..", "priv", "dispatch.conf"])),
chef_wm から実際の例を引っ張ってきました。
https://github.com/opscode/chef_wm/blob/master/priv/dispatch.conf
{["cookbooks"], chef_wm_cookbooks, []}.
{["cookbooks", qualifier], chef_wm_cookbooks, []}.
app_wm というアプリケーションを作っている場合は、app_wm_sup に設定するのが良いでしょう。
-module(app_wm_sup).
-behaviour(supervisor).
-export([start_link/0]).
-export([init/1]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
{ok, Ip} = application:get_env(app_wm, ip),
{ok, Port} = application:get_env(app_wm, port),
{ok, Dispatch} = file:consult(
filename:join([filename:dirname(code:which(?MODULE)),
"..", "priv", "dispatch.conf"])),
WebConfig = [{ip, Ip},
{port, Port},
{log_dir, "priv/log"},
{dispatch, Dispatch}],
Web = {webmachine_mochiweb,
{webmachine_mochiweb, start, [WebConfig]},
permanent, 5000, worker, dynamic},
{ok, { {one_for_one, 5, 10}, [Web]} }.
%% ディスパッチルールを追加
webmachine_router:add_route/1
%% ディスパッチルールを削除
webmachine_router:remove_route/1
%% リソースディスパッチを削除する
webmachine_router:remove_resource/1
%% 現在のディスパッチルールを取得する
webmachine_router:get_route/0
remove_resource はリソースを削除します。 リソースとは単にモジュールの事を指していると考えて問題ありません。
三種類のディスパッチルールを追加します
> webmachine_router:add_route({["spam"], spam_resource, []}).
> webmachine_router:add_route({["eggs"], spam_resource, []}).
> webmachine_router:add_route({["bacon"], bacon_resource, []}).
その後 spam モジュールに対してに対してリソースを削除します
> webmachine_router:remove_resource(spam_resource).
削除後のディスパッチルールは以下の通りになります
> webmachine_router:add_route({["bacon"], bacon_resource, []}).
リソース毎に一気に消せる機能だと思って問題ありません。
ディスパッチした URL から情報を取得するのにいくつか便利な関数が用意されています。
wrq のエクスポートされてる関数一覧
-export([create/4, create/5,load_dispatch_data/7]).
-export([method/1,scheme/1,version/1,peer/1,disp_path/1,path/1,raw_path/1,
path_info/1,response_code/1,req_cookie/1,req_qs/1,req_headers/1,
req_body/1,stream_req_body/2,resp_redirect/1,resp_headers/1,
resp_body/1,app_root/1,path_tokens/1, host_tokens/1, port/1,
base_uri/1]).
-export([path_info/2,get_req_header/2,do_redirect/2,fresh_resp_headers/2,
get_resp_header/2,set_resp_header/3,set_resp_headers/2,
set_disp_path/2,set_req_body/2,set_resp_body/2,set_response_code/2,
merge_resp_headers/2,remove_resp_header/2,
append_to_resp_body/2,append_to_response_body/2,
max_recv_body/1,set_max_recv_body/2,
get_cookie_value/2,get_qs_value/2,get_qs_value/3,set_peer/2,
add_note/3, get_notes/1]).
'*' にマッチした残りの URL 返ってきます
ディスパッチルール: | ["spam", "eggs", '*'] |
---|---|
URL: | /spam/eggs/a/b |
"a/b" = wrq:disp_path(RD)
値が無かった場合は空文字列(空リスト)が返ってきます
ディスパッチルール: | ["spam", "eggs", '*'] |
---|---|
URL: | /spam/eggs/ |
"" = wrq:disp_path(RD)
atom で定義した値を引数に指定することでその値が取得できます。
ディスパッチルール: | ["spam", "eggs", bacon, ham] |
---|---|
URL: | /spam/eggs/a/b |
"a" = wrq:path_info(bacon, RD)
atom で定義した値を proplists 形式で取得できます。
ディスパッチルール: | ["spam", "eggs", bacon, ham] |
---|---|
URL: | /spam/eggs/a/b |
[{bacon, "a"}, {ham, "b"}] = wrq:path_info(RD)
パラメータを取得できます
ディスパッチルール: | ["spam", "eggs", '*'] |
---|---|
URL: | /spam/eggs/a/b?spam=eggs&ham=bacon |
"eggs" = wrq:get_qs_value("spam", RD)
メソッド名を取得できます。戻り値は atom で 'POST' や 'PUT' などです。
'GET' = wrq:method(RD)
プロトコルのスキーマを取得できます。戻り値は http や https といった atom です。
https = wrq:method(RD)
スキーマのバージョンを取得できます。戻り値は {1,0} や {1,1} のような二桁の tuple です。
{1,1} = wrq:version(RD)
-record(wm_reqdata, {method, scheme, version, peer, wm_state,
disp_path, path, raw_path, path_info, path_tokens,
app_root,response_code,max_recv_body, max_recv_hunk,
req_cookie, req_qs, req_headers, req_body,
resp_redirect, resp_headers, resp_body,
host_tokens, port, notes
}).
supervisor にぶら下げる
application_sup にそのままぶら下げるのが普通か
WMConfig = [{ip, Ip},
{port, Port},
{log_dir, LogDir},
{dispatch, ?DISPATCH}],
WM = {webmachine_mochiweb,
{webmachine_mochiweb, start, [WMConfig]},
permanent, 5000, worker, dynamic},
{ok, {{one_for_one, 10, 10}, [WM]}}.
デフォルトは webmachine_logger が呼ばれます。そのため log_dir で指定した場所に "access.log" として出力されます。
%% wm_logger というモジュールを新しく設定する
application:set_env(webmachine, webmachine_logger_module, wm_logger)
webmachine_logger_module に指定することで新しいロガーを設定できます。ロガーはいくつかルールがあります。webmachine の logger は webmachine_sup にぶら下がるためです。
必須実装のコールバックをとりあえず書いておく
-behaviour(gen_server).
-callback start_link([string()]) -> {ok, pid()} | {error, term()}.
-callback log_access(#wm_log_data{}) -> ok.
ロガーのテンプレート
-module(wm_logger).
-behaviour(gen_server).
-export([start_link/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-export([log_access/1]).
-include("webmachine_logger.hrl").
-record(state, {}).
-spec log_access(#wm_log_data{}) -> ok.
log_access(#wm_log_data{}=D) ->
gen_server:cast(?MODULE, {log_access, D}).
start_link(BaseDir) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [BaseDir], []).
init([_BaseDir]) ->
{ok, #state{}}.
handle_call(_Msg, _From, State) ->
{noreply, State}.
handle_cast({log_access, LogData}, State) ->
%% ここにログをどうするか書く
{noreply, State};
handle_cast(_Msg, State) ->
{noreply, State}.
%% この部分は消してはいけない
handle_info({_Label, {From, MRef}, get_modules}, State) ->
From ! {MRef, [?MODULE]},
{noreply, State};
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
-record(wm_log_data,
{resource_module :: atom(),
start_time :: tuple(),
method :: atom(),
headers,
peer,
path :: string(),
version,
response_code,
response_length,
end_time :: tuple(),
finish_time :: tuple(),
notes}).
webmachine_error_handler がデフォルトで、render_errror/3 が呼び出される。 この Req は mocwhieb の Req なので、それ相応の実装が必要。
404 や 500 の際に返ってくる文字列が定義できる。
エラーハンドラーの変更は error_handler の値を変更する
application:set_env(webmachine, error_handler, wm_error_handler)
例として戻り値を HTML ではなく JSON で返すサンプルを追加します。
戻り値を全て json に変更するエラーハンドラー
-module(wm_error_handler).
-export([render_error/3]).
render_error(Code, Req, Reason) ->
case Req:has_response_body() of
{true,_} -> Req:response_body();
{false,_} -> render_error_body(Code, Req:trim_state(), Reason)
end.
render_error_body(Code, Req, _Reason) ->
{ok, ReqState} = Req:add_response_header("Content-Type", "application/json"),
ReasonPhrase = httpd_util:reason_phrase(Code),
Result = mochijson2:encode({struct, [{<<"reason_phrase">>, ReasonPhrase},
{<<"code">>, Code}]}),
{Result, ReqState}.
URL: | https://github.com/basho/webmachine/wiki/Resource-Functions |
---|
リソース関数の正常な戻り値は {Result, RD, Ctx} というタプルです。
- Result はリソース関数毎に指定されている戻り値を指定します。
- RD は #wm_reqdata{} というレコードです。
- Ctx なんでも入れられますが普通は #ctx{} というレコードを作ってそこを自由にいじっていきます。
戻り値としてそれ以外に {halt, Code} と {error, Term} があります。
- {error, Term}
- この値が戻されると 500 が返ります。Term の部分が 500 のページの Body に含まれます。
- {halt, Code}
- 強制的にリクエストを戻します。戻すときのコードを指定することが出来ます。
タイプ: | boolean() |
---|
- 指定したリソースが存在するかどうか
- false の場合は 404
- リソース確認してあるなしで。
- false の場合でも PUT の場合は大丈夫
タイプ: | boolean() |
---|
- まず一番最初に呼ばれる
- ここで今後使う情報を Ctx に入れておくのが良い
タイプ: | boolean() | AuthHead :: iodata() |
---|
認証結果を戻します。
デフォルトは true なので常に認証が成功している状態
AuthHead は WWW-Authenticate ヘッダーの中身を戻せます
タイプ: | boolean() |
---|
タイプ: | boolean() |
---|
POST が送られてきた場合そのリソースが存在しない場合の処理
タイプ: | boolean() |
---|
壊れているヘッダーなどが無いかをチェックする
タイプ: | boolean() |
---|
URI が長すぎないかどうかの確認
タイプ: | boolean() |
---|
送られてきたコンテントタイプが処理出来るかどうか
タイプ: | boolean() |
---|
ヘッダーのチェック
タイプ: | boolean() |
---|
長さチェック
タイプ: | [header()] |
---|
オプションメソッド
タイプ: | [atom()] |
---|
受け付けるメソッドを定義する
注意点としてはメソッド名は atom で指定する事
allowed_method(RD, Ctx) -> {['GET', 'POST', 'PUT', 'DELETE'], RD, Cxt}
このメソッド以外を送ると 405 Method Not Allowed が返される
タイプ: | boolean() |
---|
DELETE メソッドの場合はここに来る
タイプ: | boolean() |
---|
- delete_resource が true で返された後の処理を書ける
タイプ: | boolean() |
---|
タイプ: | Path :: string() |
---|
post_is_create が true を返したとき Location に入れる Path を指定する
タイプ: | boolean() |
---|
提供
[{content_type, function atom}]
デフォルトは [{"text/html", to_html}] が呼ばれる。
to_html は定義されているわけでは無い。
content_types_provided(RD, Ctx) ->
{[{"application/json", to_json}, {"text/html", to_html}], RD, Ctx}.
Content-Type 毎に指定される関数のの戻り値は {Result :: iolist(), RD, Ctx} を返す必要がある
受信
コンテンツタイプ毎にどの関数を呼ぶか指定します
content_types_accepted(RD, Ctx) ->
{[{"application/json", from_json}], RD, Ctx}.
Content-Type 毎に指定される関数のの戻り値は {true, RD, Ctx} を返す必要がある
no_charset | [{Charset, CharsetConverter}]
[{Encoding, Encoder}]
デフォルトは [{"identity", fun(X) -> X end}]
サンプル
[{“identity”, fun(X) -> X end}, {“gzip”, fun(X) -> zlib:gzip(X) end}]
[HeaderName :: string()]
デフォルトヘッダー以外にヘッダーを付与することが出来る
boolean()
デフォルトは false
重複していた場合の処理を追加する。コンフリクトした場合は 409 Conflict が返る。
タイプ: | boolean() |
---|---|
デフォルト: | false |
戻り値が複数存在するため自動で判定出来ない旨を返す。300 Multiple Choices が返る。
boolean()
{true, MovedURI :: string()} | false
{true, MovedURI :: string()} | false
タイプ: | undefined | {{YYYY,MM,DD},{Hour,Min,Sec}} |
---|---|
デフォルト: | undefined |
タイプ: | undefined | {{YYYY,MM,DD},{Hour,Min,Sec}} |
---|---|
デフォルト: | undefined |
タイプ: | undefined | ETag :: string() |
---|---|
デフォルト: | undefined |
タイプ: | boolean() |
---|---|
デフォルト: | true |
Webmachine の魅力の一つに強力なデバッグ機能が存在します。
キャンバスを使って経路情報を表示してくれます。
簡単な例を見てみましょう。user のリソースにデータを追加してみます。
デバッグモードを使うには二つの作業があります。
まずデバッグ対象リソースモジュールの init/1 をいじる必要があります
init(Args) ->
%% デバッグモード
{{trace, "/tmp"}, Args}.
%% こちらが正常モード
%% {ok, Args}.
さらにコンソールに以下のディスパッチルールを追加する必要があります
wmtrace_resource:add_dispatch_rule("wmtrace", "/tmp").
そうしたら PUT を投げてみましょう:
$ http PUT localhost:8080/users/1234 password=pass -j HTTP/1.1 201 Created Content-Length: 36 Content-Type: application/json Date: Sat, 05 Jan 2013 06:45:04 GMT Location: /users/1234 Server: MochiWeb/2.3.2 WebMachine/1.9.2 (someone had painted it blue) { "password": "pass", "user_id": "1234" }
正常にリソースが追加されました。
トレースする画面は以下の URL になります:
http://localhost:8080/wmtrace/
トレースに成功していると以下のようなリンクが生成されます
トレース結果が表示されます。リソース関数がどのような判定を行ったのか、さらにどのようなリクエストが来たのか、最終的にどんな経路で 201 Created に行き着いたのかなどがわかります。
それぞれの判定ポイントにてどのリソース関数が呼ばれているのかも見ることが出来ます。
全体像
全体像が細かくてよくわからないと思うので拡大して二分割してみました
この機能を使うことでデバッグがかなりやりやすくなります。
rebar の deps に webmachine を指定して使っている例
https://github.com/voluntas/snowflake/tree/feature/webmachine
- webmachine
- folsom_webmachine
- riak_kv
- riak_core
- Web Development with Webmachine for Erlang
- Webmachine
- http://www.erlang-factory.com/upload/presentations/60/sheehy_factory-webmachine.pdf
- Erlang Factory でのプレゼン
- Webmachine: a Practical Executable Model of HTTP
- cowboy
- https://github.com/extend/cowboy
- cowboy_rest である程度まで使える
- webmachine-ruby
- https://github.com/seancribbs/webmachine-ruby
- トレースまで使える完全互換
- Resources, For Real This Time
- dj-webmachine
- https://github.com/benoitc/dj-webmachine
- Django のミドルウェアとして使える
- pywebmachine
- https://github.com/davisp/pywebmachine
- サンプル実装レベル