DEV Community

voluntas
voluntas

Posted on

1 2

Erlang 用の DynamoDB 風 HTTP API フレームワーク

概要

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

AWS - DynamoDB HTTP API が独特な仕様なので紹介 - Qiita

簡単にまとめると ...

  • 全てのメソッドは POST
  • 全ての URL は /
  • x-amz-target というヘッダーがディスパッチ条件
    • このヘッダーは ServiceName_Version.Operation という組み合わせ
  • Requst も Response も全て JSON を使う

といった大きく 4 つの機能を持っています。個人的にとても気に入っているので、API 設計する時はこの設計手法を使っています。

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

Swidden (焼き畑)

shiguredo/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
Enter fullscreen mode Exit fullscreen mode

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}]}]}.
Enter fullscreen mode Exit fullscreen mode

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}
    }
}
Enter fullscreen mode Exit fullscreen mode

アプリに組み込む

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().
Enter fullscreen mode Exit fullscreen mode

設定する必要があるのは、ポート番号とターゲットヘッダーのヘッダー名です。指定したヘッダー名の値がディスパッチに使われます。

あとは 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.
Enter fullscreen mode Exit fullscreen mode

戻り値がポイントです。戻りには 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]}.
Enter fullscreen mode Exit fullscreen mode

あとは rebar swidden_doc とやるとドキュメントが生成されます。

生成される markdown はこんな感じです。JSON Schema が表示されます。

https://github.com/shiguredo/swidden/blob/develop/examples/spam/api_docs/spam.md

まとめ

マニアックなフレームワークでかなりオレオレではありますが、作ってる側の視点ではありますが、シンプルで使いやすいです。

もともと HTTP API を持つネットワークサーバに組み込む用途で開発しました。ただ JSON を返すことから JS で色々がんばればウェブサイトも作れるかもしれません。

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

Image of Datadog

How to Diagram Your Cloud Architecture

Cloud architecture diagrams provide critical visibility into the resources in your environment and how they’re connected. In our latest eBook, AWS Solution Architects Jason Mimick and James Wenzel walk through best practices on how to build effective and professional diagrams.

Download the Free eBook

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay