Erlang 用の DynamoDB 風 HTTP API フレームワーク
Tags: erlang
Published: 2020-01-21 02:52:50


## 概要

DynamoDB 風 API の詳細については下記を読んで貰うということで。

[AWS - DynamoDB HTTP API が独特な仕様なので紹介 - Qiita](http://qiita.com/voluntas/items/a3ee5b0e2843d589d45f)

簡単にまとめると ...

- 全てのメソッドは POST
- 全ての URL は /
- x-amz-target というヘッダーがディスパッチ条件
	- このヘッダーは ServiceName_Version.Operation という組み合わせ
- Requst も Response も全て JSON を使う
	
といった大きく 4 つの機能を持っています。個人的にとても気に入っているので、API 設計する時はこの設計手法を使っています。

毎回同じようなのを書いていたので、これはフレームワーク作ろうと思って一念発起して作ることにしました。

## Swidden (焼き畑)

[shiguredo/swidden](https://github.com/shiguredo/swidden)

ということで、作ったのが Swidden という HTTP API フレームワークです。

このフレームワークは JSON Schema を使って、色々うまいことやってくれます。

サンプル例を見てください。

```bash
$ 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 を作りましょう。

```erlang
{<<"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 です、なければ引数なしで実装します。

```erlang
-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 で色々がんばればウェブサイトも作れるかもしれません。

もし興味があったら使ってみてください。