{"id":19308595,"url":"https://github.com/shiguredo/swidden","last_synced_at":"2025-04-22T13:32:20.353Z","repository":{"id":22879955,"uuid":"26227972","full_name":"shiguredo/swidden","owner":"shiguredo","description":"ヘッダーベース HTTP API フレームワーク","archived":false,"fork":false,"pushed_at":"2025-02-22T07:34:40.000Z","size":24532,"stargazers_count":14,"open_issues_count":0,"forks_count":2,"subscribers_count":9,"default_branch":"develop","last_synced_at":"2025-04-20T11:04:15.729Z","etag":null,"topics":["erlang","http-server","json-schema"],"latest_commit_sha":null,"homepage":null,"language":"Erlang","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/shiguredo.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGES.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2014-11-05T16:16:00.000Z","updated_at":"2025-02-22T07:33:51.000Z","dependencies_parsed_at":"2024-11-10T00:15:38.645Z","dependency_job_id":null,"html_url":"https://github.com/shiguredo/swidden","commit_stats":{"total_commits":424,"total_committers":6,"mean_commits":70.66666666666667,"dds":"0.14386792452830188","last_synced_commit":"5f01285bae8f63695c1ba63451a33a6947c68fa1"},"previous_names":[],"tags_count":102,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shiguredo%2Fswidden","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shiguredo%2Fswidden/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shiguredo%2Fswidden/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shiguredo%2Fswidden/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/shiguredo","download_url":"https://codeload.github.com/shiguredo/swidden/tar.gz/refs/heads/develop","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250246636,"owners_count":21398917,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["erlang","http-server","json-schema"],"created_at":"2024-11-10T00:15:35.631Z","updated_at":"2025-04-22T13:32:20.005Z","avatar_url":"https://github.com/shiguredo.png","language":"Erlang","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ヘッダーベース HTTP API フレームワーク\n\n## 概要\n\nAWS DynamoDB や Kinesis などの API の形式に影響を受けた HTTP API ライブラリです。\n\n[DynamoDB や Route53 などの AWS API が独特な仕様なので紹介](https://gist.github.com/voluntas/811240c5b6a169ae1c6ac401e0197417)\n\n特徴は以下の通りの部分です\n\n- 指定したヘッダー名の値を使いディスパッチする\n    - デフォルトのヘッダー名は X-Swd-Target\n    - このヘッダー名は好きに変更できる\n- URI は / のみ\n- メソッドは POST のみ\n- ヘッダー値は *サービス_バージョン.オペレーション* という形式\n    - X-Swd-Target: Spam_20141101.CreateUser\n    - X-Swd-Target: SpamAdmin_20141101.GetMetrics\n- 入り口と出口が JSON\n- 何も送らなければ Body は空になる\n\n## サンプル\n\nユーザ追加 API 例\n\n```\n$ http POST 127.0.0.1:5000/ \"x-spam-target:Spam_20141101.CreateUser\" username=yakihata password=nogyo -vvv\nPOST / HTTP/1.1\nAccept: application/json\nAccept-Encoding: gzip, deflate\nConnection: keep-alive\nContent-Length: 45\nContent-Type: application/json; charset=utf-8\nHost: 127.0.0.1:5000\nUser-Agent: HTTPie/0.8.0\nx-spam-target: Spam_20141101.CreateUser\n\n{\n    \"password\": \"nogyo\",\n    \"username\": \"yakihata\"\n}\n\nHTTP/1.1 200 OK\nconnection: keep-alive\ncontent-length: 2\ncontent-type: application/json\ndate: Sun, 02 Nov 2014 18:53:09 GMT\nserver: Cowboy\n```\n\nユーザ取得 API 例\n\n```\n$ http POST 127.0.0.1:5000/ \"x-spam-target:Spam_20141101.GetUser\" username=yakihata -vvv\nPOST / HTTP/1.1\nAccept: application/json\nAccept-Encoding: gzip, deflate\nConnection: keep-alive\nContent-Length: 24\nContent-Type: application/json; charset=utf-8\nHost: 127.0.0.1:5000\nUser-Agent: HTTPie/0.8.0\nx-spam-target: Spam_20141101.GetUser\n\n{\n    \"username\": \"yakihata\"\n}\n\nHTTP/1.1 200 OK\nconnection: keep-alive\ncontent-length: 20\ncontent-type: application/json\ndate: Sun, 02 Nov 2014 18:53:33 GMT\nserver: Cowboy\n\n{\n    \"password\": \"nogyo\"\n}\n```\n\n## 目的\n\nネットワークサーバの組込用 HTTP API を想定して作られています。そのため、ブラウザには優しい仕様になっていません。\n\nただ、ヘッダー + JSON なので、JS で操作するのは難しくないのかもしれません。\n\nいくつもネットワークサーバを作っていると API の仕組みを統一したくなってきたため作りました。\n\n- 組込が簡単なこと\n- 使い方が同じなこと\n- データベースとの連携が簡単に行えること\n- バージョンにより API が気軽に増やせること\n\nあたりを意識して作っています。といってもぱくりですが。\n\n## 細かい話\n\n- ヘッダー名は自分で指定可能\n    - 判定ヘッダーのデフォルトは X-Swd-Target となる\n    - name は好きにして良い amz が源流、自社名の省略が良いか X-Abc-Def とかでもよい\n        - X- は付けた方がいい\n    - 先頭大文字 PascalCase がいいかも\n    - ただHTTP ヘッダーは大文字小文字無視するのでどうでもいい\n- ヘッダー値は Service_Version.Operation という構成\n    - Service 名は先頭大文字 DynamoDB や OpsWorks など DataPipeline ように PascalCase で\n    - Version は 20120810 で YYYYMMDD に\n    - Operation 名も PascalCase で CreateTrail や ActivatePipline など **動詞** を使う\n        - Add / Create / Delete / Describe / List / Get / Merge / Put / Remove / Split など\n        - リスト表示の時は ListUsers と複数形にする\n- ユーザレイヤーのエラーは 400 のみで 4xx や 5xx はすべて swidden が生成する\n- priv/swidden/dispatch.conf にルーティングを記述する\n    - dispatch.conf は Erlang Term で記述\n- JSON  Schema は priv/swidden/schmeas/:service/:version/:operation.json となる\n    - URL の service, operation は PascalCase から snake_case に自動で変換される\n- JSON Schema はライブラリの都合で Draft3 対応のみ\n- dispatch.conf に書かれている API のスキーマがが読み込めない場合はエラーとなる\n\n## 導入方法\n\nまずは自分のアプリを作成します。\n\nrebar を使っていれば create-app を使うのが簡単です\n\n```\n$ rebar create-app appid=spam\n```\n\n### rebar.config deps へ swidden を設定する\n\nその後 rebar.config の deps に以下の設定をします\n\n```\n{deps,\n  [\n   {swidden,\n    \".*\", {git, \"git@github.com:shiguredo/swidden.git\", {tag, \"4.0.0\"}}}\n  ]\n}.\n```\n\n### priv ディレクトリ\n\npriv ディレクトリの下に swidden ディレクトリを作ります\n\nその下にスキーマファイルとディスパッチファイルを置きます。\n\n#### dispatch.conf\n\nディスパッチファイルの名前は dispatch.conf 固定です。\n\n```\nspam/priv/swidden/dispatch.conf\n```\n\n- サービス名はヘッダー値の Service_Version.Operation の Service にあたります\n    - binary 型の PascalCase で指定します\n- バージョンはヘッダー値の Service_Version.Operation の Version にあたります\n    - binary 型の YYYYMMDD で指定します\n- オペレーション名はヘッダー値の Service_Version.Operation の Operation にあたります\n    - binary 型の PascalCase で指定します\n\n以下は dispatch.conf の構造です。\n\n```\n{サービス名1, [\n    {バージョン1,\n        [{オペレーション名1, モジュール名1},\n         {オペレーション名2, モジュール名1}]},\n    {バージョン2\n        [{オペレーション名1, モジュール名2}]}]}.\n{サービス名1, [\n    {バージョンX,\n        [{オペレーション名3, モジュール名3}]}]}.\n```\n\nモジュール名はそのオペレーションが実装されているモジュールを指定します。\nモジュールが spam_user でオペレーションが CreateUser の場合は spam_user:create_user/1 が呼び出されます。\n\n実際の値で埋めた設定が以下の通りになります。\n\n```erlang\n{\u003c\u003c\"Spam\"\u003e\u003e, [\n    {\u003c\u003c\"20141101\"\u003e\u003e,\n        [{\u003c\u003c\"GetUser\"\u003e\u003e, spam_user},\n         {\u003c\u003c\"CreateUser\"\u003e\u003e, spam_user},\n         {\u003c\u003c\"UpdateUser\"\u003e\u003e, spam_user}\n         {\u003c\u003c\"DeleteUser\"\u003e\u003e, spam_user}]},\n    {\u003c\u003c\"20150701\"\u003e\u003e,\n        [{\u003c\u003c\"CreateUser\"\u003e\u003e, spam_user_with_group}]}]}.\n{\u003c\u003c\"SpamAdmin\"\u003e\u003e, [\n    {\u003c\u003c\"20141101\"\u003e\u003e,\n        [{\u003c\u003c\"GetMetrics\"\u003e\u003e, spam_admin}]}]}.\n```\n\nハンドリングはすべて dispatch.conf に書かれている通りに動作します。\n\n#### schemas\n\nschemas の構造は以下の通りです\n\n```\nspam/priv/swidden/schemas/\u003cservice_name\u003e/\u003cversion\u003e/\u003cschema\u003e.json\n```\n\n以下は注意点です\n\n- service_name は dispatch.conf のサービス名を snake_case にしたものが使われる\n- schema は dispatch.conf のオペレーション名を snake_case にしたものが使われる\n- dispatch.conf に設定されているオペレーションの JSON Schema が存在しない場合はエラーになる\n\n以下は dispatch.conf に設定した分のスキーマ一覧です\n\n- spam/priv/swidden/schemas/spam/20141101/get_user.json\n- spam/priv/swidden/schemas/spam/20141101/create_user.json\n- spam/priv/swidden/schemas/spam/20141101/update_user.json\n- spam/priv/swidden/schemas/spam/20141101/delete_user.json\n- spam/priv/swidden/schemas/spam/20150701/create_user.json\n- spam/priv/swidden/schemas/spam_admin/20141101/get_metrics.json\n\nこれで priv 以下の設定は終わりです。\n\n##### get_user.json の JSON Schema 例\n\n```\n{\n    \"properties\": {\n        \"username\": {\"type\": \"string\", \"required\": true}\n    }\n}\n```\n\n### 実際にアプリに組み込む\n\nswidden:start/1 の引数は自分の作成しているアプリの名前です。\n\nこのアプリの名前を使って spam/priv/ のパスを探し出します。\n\nアプリ起動時に swidden:start/1 を実行すればアプリの起動時に自動で dispatch.conf や JSON Schema を読み込みます。\n\n```erlang\n-module(spam_app).\n\n-behaviour(application).\n\n-export([start/2, stop/1]).\n\nstart(_StartType, _StartArgs) -\u003e\n    {ok, _Pid} = swidden:start(spam, [{port, 5000}, {header_name, \u003c\u003c\"x-spam-target\"\u003e\u003e}]),\n\n    ok = spam_user:start(),\n\n    spam_sup:start_link().\n\nstop(_State) -\u003e\n    ok = swidden:stop(),\n    ok.\n```\n\ndispatch.conf で設定したモジュールの例です\n\n戻り値に swidden:success/0,1 と swidden:failure/1 を使用します。\n\n```erlang\n-module(spam_user).\n\n-export([start/0]).\n-export([get_user/1, create_user/1, update_user/1, delete_user/1]).\n\n-define(TABLE, spam_user_table).\n\n\nstart() -\u003e\n    _Tid = ets:new(?TABLE, [set, public, named_table]),\n    ok.\n\n\nget_user(#{\u003c\u003c\"username\"\u003e\u003e := Username}) -\u003e\n    case ets:lookup(?TABLE, Username) of\n        [] -\u003e\n            swidden:failure(\u003c\u003c\"MissingUserException\"\u003e\u003e);\n        [{Username, Password}] -\u003e\n            %% proplists を戻せば JSON で返ります\n            swidden:success([{password, Password}]);\n        [{Username, Password, _Group}] -\u003e\n            %% spam_user_with_group 対応\n            swidden:success(#{password =\u003e Password})\n    end.\n\n\ncreate_user(#{\u003c\u003c\"username\"\u003e\u003e := Username, \u003c\u003c\"password\"\u003e\u003e := Password}) -\u003e\n    case ets:insert_new(?TABLE, {Username, Password}) of\n        true -\u003e\n            swidden:success();\n        false -\u003e\n            swidden:failure(\u003c\u003c\"DuplicateUserException\"\u003e\u003e)\n    end.\n\n\nupdate_user(#{\u003c\u003c\"username\"\u003e\u003e := Username, \u003c\u003c\"password\"\u003e\u003e := Password}) -\u003e\n    case ets:lookup(?TABLE, Username) of\n        [] -\u003e\n            swidden:failure(\u003c\u003c\"MissingUserException\"\u003e\u003e);\n        [{Username, _OldPassword}] -\u003e\n            true = ets:insert(?TABLE, {Username, Password}),\n            swidden:success();\n        [{Username, _OldPassword, Group}] -\u003e\n            %% spam_user_with_group 対応\n            true = ets:insert(?TABLE, {Username, Password, Group}),\n            swidden:success()\n    end.\n\n\ndelete_user(JSON) -\u003e\n    Username = proplists:get_value(\u003c\u003c\"username\"\u003e\u003e, JSON),\n    case ets:lookup(?TABLE, Username) of\n        [] -\u003e\n            swidden:failure(\u003c\u003c\"MissingUserException\"\u003e\u003e);\n        _ -\u003e\n            true = ets:delete(?TABLE, Username),\n            swidden:success()\n    end.\n```\n\n完全版はこのリポジトリの examples/spam にありますのでそちらを参照してください\n\n#### swidden:success/0,1\n\nswidden:success/0,1 は処理が成功したときに使用します。\n\n- success/0 は特に返す値がない場合使用します\n    - Body が空で戻ります\n- success/1 は戻したい JSON (proplists) を引数に指定します\n\n#### swidden:failure/1\n\nswidden:failure/1 は処理が失敗したときに使用します。\n\n引数には binary 型でエラーの文字列を入れてください。\n\nたとえばユーザが存在しなかった時は \u003c\u003c\"MissingUserException\"\u003e\u003e などです。\n\n戻りは {\"error_type\": \"MissingUserException\"} となります。\n\n### swidden:failure/2\n\nswidden:failure/1 は処理が失敗したときに使用し、Type 意外に Reason が指定できます。\n\nReason は自由にユーザが決めて良い値です。 Reason はマップを使用してください。\n\nたとえば Reason に #{code := 500} というのを入れた場合\n\n戻りは {\"error_type\": \"MissingUserException\", \"error_reason\": {\"code\": 500}} となります。\n\n#### 動作確認\n\nexamples/spam で make; make dev を実行します。\n\n```\n$ make; make dev\n```\n\n```\n$ dev/spam/bin/spam\nErlang/OTP 17 [erts-6.2] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]\n\nEshell V6.2  (abort with ^G)\n(spam@127.0.0.1)1\u003e\n```\n\nユーザを追加してみます\n\n```\n$ http POST 127.0.0.1:5000/ \"x-spam-target:Spam_20141101.CreateUser\" username=yakihata password=nogyo -vvv\nPOST / HTTP/1.1\nAccept: application/json\nAccept-Encoding: gzip, deflate\nConnection: keep-alive\nContent-Length: 45\nContent-Type: application/json; charset=utf-8\nHost: 127.0.0.1:5000\nUser-Agent: HTTPie/0.8.0\nx-spam-target: Spam_20141101.CreateUser\n\n{\n    \"password\": \"nogyo\",\n    \"username\": \"yakihata\"\n}\n\nHTTP/1.1 200 OK\nconnection: keep-alive\ncontent-length: 2\ncontent-type: application/json\ndate: Sun, 02 Nov 2014 18:53:09 GMT\nserver: Cowboy\n```\n\nユーザを確認してみます\n\n```\n$ http POST 127.0.0.1:5000/ \"x-spam-target:Spam_20141101.GetUser\" username=yakihata -vvv\nPOST / HTTP/1.1\nAccept: application/json\nAccept-Encoding: gzip, deflate\nConnection: keep-alive\nContent-Length: 24\nContent-Type: application/json; charset=utf-8\nHost: 127.0.0.1:5000\nUser-Agent: HTTPie/0.8.0\nx-spam-target: Spam_20141101.GetUser\n\n{\n    \"username\": \"yakihata\"\n}\n\nHTTP/1.1 200 OK\nconnection: keep-alive\ncontent-length: 20\ncontent-type: application/json\ndate: Sun, 02 Nov 2014 18:53:33 GMT\nserver: Cowboy\n\n{\n    \"password\": \"nogyo\"\n}\n```\n\nこのライブラリを使えばコスト低く JSON Schema を使った HTTP API が作成できます。\n\n## 利用したいサービスを指定したい場合\n\nそのポートで利用するサービスを固定したい場合は swidden:start する際の引数に [{services, [\u003c\u003c\"Spam\"\u003e\u003e]}] とサービスを指定することで、そのサービスだけが有効になります。\n\n```\n{ok, _} = swidden:start(spam, [{port, 3000}, {services, [\u003c\u003c\"Spam\"\u003e\u003e]}]),\n{ok, _} = swidden:start(spam, [{port, 5000}, {services, [\u003c\u003c\"SpamAdmin\"\u003e\u003e]}]),\n```\n\nSpam は 3000 番ポートで、 SpamAdmin は 5000 番ポートで有効になります。\n\n## 送信の時の Body が空の場合\n\nたとえば ListUsers などの一覧取得の場合はもしかすると Body を空で送信する場合が出てくるかもしれません。\n\nその場合は以下のようにしてください\n\n- JSON Schema は用意するが {} と設定する\n- 呼び出される関数は引数なしで実装する\n\n```\nlist_users() -\u003e\n    Users = [ #{username =\u003e Username,\n               {password =\u003e Password} || {Username, Password} \u003c- ets:tab2list(?TABLE) ],\n    swidden:success(Users).\n```\n\n## リダイレクト\n\n```\nget_user(Json) -\u003e\n    %% 転送したい先の Location を渡す\n    swidden:redirect(Location).\n\n```\n\n## TODO\n\n- 認証機能\n- Response に対する JSON Schema によるバリデーション\n- dispatch.conf の Map 化\n\n## 既知の問題\n\n今のところなし\n\n## ライセンス\n\n```\nCopyright 2016-2024, Shiguredo Inc.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n   http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshiguredo%2Fswidden","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fshiguredo%2Fswidden","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshiguredo%2Fswidden/lists"}