概要
DynamoDB 風 API の詳細については下記を読んで貰うということで。
AWS - DynamoDB HTTP API が独特な仕様なので紹介 - Qiita
簡単にまとめると ...
- 全てのメソッドは POST
- 全ての URL は /
- x-amz-target というヘッダーがディスパッチ条件
- このヘッダーは ServiceName_Version.Operation という組み合わせ
- Requst も Response も全て JSON を使う
といった大きく 4 つの機能を持っています。個人的にとても気に入っているので、API 設計する時はこの設計手法を使っています。
毎回同じようなのを書いていたので、これはフレームワーク作ろうと思って一念発起して作ることにしました。
Swidden (焼き畑)
ということで、作ったのが Swidden という HTTP API フレームワークです。
このフレームワークは JSON Schema を使って、色々うまいことやってくれます。
サンプル例を見てください。
$ http POST 127.0.0.1:5000/ "x-spam-target:Spam_20141101.CreateUser" username=yakihata password=nogyo -vvv
POST / HTTP/1.1
Accept: application/json
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 45
Content-Type: application/json; charset=utf-8
Host: 127.0.0.1:5000
User-Agent: HTTPie/0.8.0
x-spam-target: Spam_20141101.CreateUser
{
"password": "nogyo",
"username": "yakihata"
}
HTTP/1.1 200 OK
connection: keep-alive
content-length: 2
content-type: application/json
date: Sun, 02 Nov 2014 18:53:09 GMT
server: Cowboy
x-spam-target というヘッダー名でディスパッチします。ここでは Spam というサービス名で 20141101 というバージョン、そして CreateUser というオペレーションを実行しています。
この API を実装するにはいくつかの手順があります。それを紹介していきます。
実装方法
Erlang のアプリを作ったら、priv/swidden/ というフォルダを掘ります。
Dispatch.conf
そこで dispatch.conf を作りましょう。
{<<"Spam">>, [
{<<"20141101">>,
[{<<"GetUser">>, spam_user},
{<<"CreateUser">>, spam_user},
{<<"UpdateUser">>, spam_user}
{<<"DeleteUser">>, spam_user}]},
{<<"20150701">>,
[{<<"CreateUser">>, spam_user_with_group}]}]}.
dispatch.conf の中身はこんな感じです。サービス、バージョン、オペレーション、あとはそのオペレーションが実装されているモジュールを指定します。バージョンは複数指定可能です。
この dispatch.conf がルーティングの全てです。
JSON Schema
それぞれのリクエストに対してバリデーションしたいですよね。ということでバリデーションを書きます。
priv/swidden/schemas というフォルダで掘ります。その後 schemas/サービス名/バージョン/ というフォルダ掘りましょう。もしサービス名が SpamEgg の場合はスネークケースで spam_egg に変換しましょう。
今回だとサービスは Spam でバージョンは 20141101 と 20150701 なので二つフォルダを掘ります。
- priv/swidden/schemas/spam/20141101/
- priv/swidden/schemas/spam/20150701/
その後はそれぞれに JSON Schema をセットします。
dispatch.conf で設定した GetUser は get_user.json というスネークケースに変換しましょう。
- priv/swidden/schemas/spam/20141101/get_user.json
- priv/swidden/schemas/spam/20141101/create_user.json
- priv/swidden/schemas/spam/20141101/update_user.json
- priv/swidden/schemas/spam/20141101/delete_user.json
- priv/swidden/schemas/spam/20150701/create_user.json
5 つの JSON Schema が用意されました。JSON Schema はライブラリの都合で Draft3 です ... 。
ちなみに JSON Schema の例はこんな感じです。
{
"properties": {
"username": {"type": "string", "required": true}
}
}
アプリに組み込む
Swidden をアプリに組み込みます。Swidden はアプリ起動時にスタートする必要があります。
start(_StartType, _StartArgs) ->
{ok, _Pid} = swidden:start(spam, [{port, 5000}, {header_name, <<"x-spam-target">>}]),
ok = spam_user:start(),
spam_sup:start_link().
設定する必要があるのは、ポート番号とターゲットヘッダーのヘッダー名です。指定したヘッダー名の値がディスパッチに使われます。
あとは dispatch.conf で指定したモジュールを実装します。spam_user モジュールを実装します。ちなみに dispatch.conf で指定したオペレーションのスネークケースの関数がそのまま呼ばれます。
引数は JSON があるのであれば JSON です、なければ引数なしで実装します。
-module(spam_user).
-export([start/0]).
-export([get_user/1, create_user/1, update_user/1, delete_user/1]).
-define(TABLE, spam_user_table).
start() ->
_Tid = ets:new(?TABLE, [set, public, named_table]),
ok.
get_user(JSON) ->
Username = proplists:get_value(<<"username">>, JSON),
case ets:lookup(?TABLE, Username) of
[] ->
swidden:failure(<<"MissingUserException">>);
[{Username, Password}] ->
%% proplists を戻せば JSON で返ります
swidden:success([{password, Password}]);
[{Username, Password, _Group}] ->
%% spam_user_with_group 対応
swidden:success([{password, Password}])
end.
create_user(JSON) ->
Username = proplists:get_value(<<"username">>, JSON),
Password = proplists:get_value(<<"password">>, JSON),
case ets:insert_new(?TABLE, {Username, Password}) of
true ->
swidden:success();
false ->
swidden:failure(<<"DuplicateUserException">>)
end.
update_user(JSON) ->
Username = proplists:get_value(<<"username">>, JSON),
Password = proplists:get_value(<<"password">>, JSON),
case ets:lookup(?TABLE, Username) of
[] ->
swidden:failure(<<"MissingUserException">>);
[{Username, _OldPassword}] ->
true = ets:insert(?TABLE, {Username, Password}),
swidden:success();
[{Username, _OldPassword, Group}] ->
%% spam_user_with_group 対応
true = ets:insert(?TABLE, {Username, Password, Group}),
swidden:success()
end.
delete_user(JSON) ->
Username = proplists:get_value(<<"username">>, JSON),
case ets:lookup(?TABLE, Username) of
[] ->
swidden:failure(<<"MissingUserException">>);
_ ->
true = ets:delete(?TABLE, Username),
swidden:success()
end.
戻り値がポイントです。戻りには swidden:success/0,1 と swidden:failure/1 を使用します。人によっては気持ち悪いかもしれませんが、戻り値を固定させるためにこのような実装になっています。
戻り値を JSON で返したい場合は swidden:success([{ham, <<"bacon">>]) のように proplist で引数に渡します。ちなみに生の JSON には jsone を使って変換されます。swidden:success() の場合は body が空で 200 が返ります。
400 の失敗を返したい場合は swidden:failure(<<"DuplicateUserName">>) のように引数にバイナリを指定してください。
swidden の使い方自体はこれで終わりです。特に難しくありません。API が独特ですが慣れると楽ですのでオススメです。
おまけ
swidden には rebar plugin が入っており、自動でドキュメントを作ってくる機能があります。
rebar.config に以下を追加します。
{plugins, [rebar_swidden_plugin]}.
あとは rebar swidden_doc とやるとドキュメントが生成されます。
生成される markdown はこんな感じです。JSON Schema が表示されます。
https://github.com/shiguredo/swidden/blob/develop/examples/spam/api_docs/spam.md
まとめ
マニアックなフレームワークでかなりオレオレではありますが、作ってる側の視点ではありますが、シンプルで使いやすいです。
もともと HTTP API を持つネットワークサーバに組み込む用途で開発しました。ただ JSON を返すことから JS で色々がんばればウェブサイトも作れるかもしれません。
もし興味があったら使ってみてください。
Top comments (0)