ヘッダーベース HTTP API フレームワーク

概要

AWS DynamoDB や Kinesis などの API の形式に影響を受けた HTTP API ライブラリです。

DynamoDB や Route53 などの AWS API が独特な仕様なので紹介

特徴は以下の通りの部分です

サンプル

ユーザ追加 API 例

$ 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

ユーザ取得 API 例

$ http POST 127.0.0.1:5000/ "x-spam-target:Spam_20141101.GetUser" username=yakihata -vvv
POST / HTTP/1.1
Accept: application/json
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 24
Content-Type: application/json; charset=utf-8
Host: 127.0.0.1:5000
User-Agent: HTTPie/0.8.0
x-spam-target: Spam_20141101.GetUser

{
    "username": "yakihata"
}

HTTP/1.1 200 OK
connection: keep-alive
content-length: 20
content-type: application/json
date: Sun, 02 Nov 2014 18:53:33 GMT
server: Cowboy

{
    "password": "nogyo"
}

目的

ネットワークサーバの組込用 HTTP API を想定して作られています。そのため、ブラウザには優しい仕様になっていません。

ただ、ヘッダー + JSON なので、JS で操作するのは難しくないのかもしれません。

いくつもネットワークサーバを作っていると API の仕組みを統一したくなってきたため作りました。

あたりを意識して作っています。といってもぱくりですが。

細かい話

導入方法

まずは自分のアプリを作成します。

rebar を使っていれば create-app を使うのが簡単です

$ rebar create-app appid=spam

rebar.config deps へ swidden を設定する

その後 rebar.config の deps に以下の設定をします

{deps,
  [
   {swidden,
    ".*", {git, "git@github.com:shiguredo/swidden.git", {tag, "4.0.0"}}}
  ]
}.

priv ディレクトリ

priv ディレクトリの下に swidden ディレクトリを作ります

その下にスキーマファイルとディスパッチファイルを置きます。

dispatch.conf

ディスパッチファイルの名前は dispatch.conf 固定です。

spam/priv/swidden/dispatch.conf

以下は dispatch.conf の構造です。

{サービス名1, [
    {バージョン1,
        [{オペレーション名1, モジュール名1},
         {オペレーション名2, モジュール名1}]},
    {バージョン2
        [{オペレーション名1, モジュール名2}]}]}.
{サービス名1, [
    {バージョンX,
        [{オペレーション名3, モジュール名3}]}]}.

モジュール名はそのオペレーションが実装されているモジュールを指定します。 モジュールが spam_user でオペレーションが CreateUser の場合は spam_user:create_user/1 が呼び出されます。

実際の値で埋めた設定が以下の通りになります。

{<<"Spam">>, [
    {<<"20141101">>,
        [{<<"GetUser">>, spam_user},
         {<<"CreateUser">>, spam_user},
         {<<"UpdateUser">>, spam_user}
         {<<"DeleteUser">>, spam_user}]},
    {<<"20150701">>,
        [{<<"CreateUser">>, spam_user_with_group}]}]}.
{<<"SpamAdmin">>, [
    {<<"20141101">>,
        [{<<"GetMetrics">>, spam_admin}]}]}.

ハンドリングはすべて dispatch.conf に書かれている通りに動作します。

schemas

schemas の構造は以下の通りです

spam/priv/swidden/schemas/<service_name>/<version>/<schema>.json

以下は注意点です

以下は dispatch.conf に設定した分のスキーマ一覧です

これで priv 以下の設定は終わりです。

get_user.json の JSON Schema 例
{
    "properties": {
        "username": {"type": "string", "required": true}
    }
}

実際にアプリに組み込む

swidden:start/1 の引数は自分の作成しているアプリの名前です。

このアプリの名前を使って spam/priv/ のパスを探し出します。

アプリ起動時に swidden:start/1 を実行すればアプリの起動時に自動で dispatch.conf や JSON Schema を読み込みます。

-module(spam_app).

-behaviour(application).

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

start(_StartType, _StartArgs) ->
    {ok, _Pid} = swidden:start(spam, [{port, 5000}, {header_name, <<"x-spam-target">>}]),

    ok = spam_user:start(),

    spam_sup:start_link().

stop(_State) ->
    ok = swidden:stop(),
    ok.

dispatch.conf で設定したモジュールの例です

戻り値に swidden:success/0,1 と swidden:failure/1 を使用します。

-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(#{<<"username">> := Username}) ->
    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(#{<<"username">> := Username, <<"password">> := Password}) ->
    case ets:insert_new(?TABLE, {Username, Password}) of
        true ->
            swidden:success();
        false ->
            swidden:failure(<<"DuplicateUserException">>)
    end.


update_user(#{<<"username">> := Username, <<"password">> := Password}) ->
    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.

完全版はこのリポジトリの examples/spam にありますのでそちらを参照してください

swidden:success/0,1

swidden:success/0,1 は処理が成功したときに使用します。

swidden:failure/1

swidden:failure/1 は処理が失敗したときに使用します。

引数には binary 型でエラーの文字列を入れてください。

たとえばユーザが存在しなかった時は <<”MissingUserException”>> などです。

戻りは {“error_type”: “MissingUserException”} となります。

swidden:failure/2

swidden:failure/1 は処理が失敗したときに使用し、Type 意外に Reason が指定できます。

Reason は自由にユーザが決めて良い値です。 Reason はマップを使用してください。

たとえば Reason に #{code := 500} というのを入れた場合

戻りは {“error_type”: “MissingUserException”, “error_reason”: {“code”: 500}} となります。

動作確認

examples/spam で make; make dev を実行します。

$ make; make dev
$ dev/spam/bin/spam
Erlang/OTP 17 [erts-6.2] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Eshell V6.2  (abort with ^G)
(spam@127.0.0.1)1>

ユーザを追加してみます

$ 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

ユーザを確認してみます

$ http POST 127.0.0.1:5000/ "x-spam-target:Spam_20141101.GetUser" username=yakihata -vvv
POST / HTTP/1.1
Accept: application/json
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 24
Content-Type: application/json; charset=utf-8
Host: 127.0.0.1:5000
User-Agent: HTTPie/0.8.0
x-spam-target: Spam_20141101.GetUser

{
    "username": "yakihata"
}

HTTP/1.1 200 OK
connection: keep-alive
content-length: 20
content-type: application/json
date: Sun, 02 Nov 2014 18:53:33 GMT
server: Cowboy

{
    "password": "nogyo"
}

このライブラリを使えばコスト低く JSON Schema を使った HTTP API が作成できます。

利用したいサービスを指定したい場合

そのポートで利用するサービスを固定したい場合は swidden:start する際の引数に [{services, [<<”Spam”>>]}] とサービスを指定することで、そのサービスだけが有効になります。

{ok, _} = swidden:start(spam, [{port, 3000}, {services, [<<"Spam">>]}]),
{ok, _} = swidden:start(spam, [{port, 5000}, {services, [<<"SpamAdmin">>]}]),

Spam は 3000 番ポートで、 SpamAdmin は 5000 番ポートで有効になります。

送信の時の Body が空の場合

たとえば ListUsers などの一覧取得の場合はもしかすると Body を空で送信する場合が出てくるかもしれません。

その場合は以下のようにしてください

list_users() ->
    Users = [ #{username => Username,
               {password => Password} || {Username, Password} <- ets:tab2list(?TABLE) ],
    swidden:success(Users).

リダイレクト

get_user(Json) ->
    %% 転送したい先の Location を渡す
    swidden:redirect(Location).

TODO

既知の問題

今のところなし

ライセンス

Copyright 2016-2024, Shiguredo Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.