簡単な API サーバーを作る必要があったので、Elixir の WAF である Phoenix を使ってみました。API バージョンでスコープを分けた RESTful API 構成にしようとしたのですが、なんだか結構ハマってしまったので簡単な手順を残します。
title
と completed
という2つのフィールドを持つ todo
という単純なリソースを例とします。
アプリケーションの作成
phoenix.new
タスクで新規の Phoenix アプリケーションを作成します。このとき、今回は REST API リソースを構築するので、アセットを管理するためのツールである Brunch
は不要です。したがって --no-brunch
オプションをつけて実行します。
$ mix phoenix.new rest_api_example --no-brunch
これで、Phoenix アプリケーションの雛形が作成されるので、まずは出力された指示にしたがってデータベースの作成をします。
ここで、以下のエラーが出力されて、データベースの作成に失敗することあります。
** (Mix) The database for repo HelloPhoenix.Repo couldn't be created, reason given: psql: FATAL: role "postgres" does not exist
これは、Phoenix が利用する postgres
というロールが Postgres データベースに存在しないことによるエラーです。したがって、その場合には CREATEDB
権限を持った postgres
ロールを作成しておきます (この設定は config/dev.ex
などの設定を変更することによって変更することができるようです) 。
$ psql -d postgres
=# CREATE ROLE postgres LOGIN CREATEDB;
データベースが作られたら、デフォルトで生成されるものの中で、使わない不要なファイルを削除しておきます。以下の通り、今回は利用しない page
リソースのファイルを削除してください。
$ rm web/controllers/page_controller.ex web/views/page_view.ex test/controllers/page_controller_test.exs test/views/page_view_test.exs
これで、アプリケーションが作成され、REST API リソースを実装していく準備ができました。
REST API リソースの実装
Phoenix はリソースを生成するための scaffolding 機能を持っていて、mix
タスクによって実行できます。今回は API リソースの実装をするので、phoenix.gen.json
タスクを使います。
$ mix phoenix.gen.json Todo todos title:string completed:boolean
これを実行すると、以下のファイルが生成されます。
-
priv/repo/migrations/YYYYMMDDHHMMSS_create_todo.exs
: マイグレーションファイル -
web/models/todo.ex
: モデル -
test/models/todo_test.exs
: モデルのテストファイル -
web/controllers/todo_controller.ex
: コントローラー -
web/views/todo_view.ex
: ビューファイル -
test/controllers/todo_controller_test.exs
: コントローラーのテストファイル -
web/views/changeset_view.ex
: Ecto モデルのチェンジセットファイル (まだよくわかってない…)
リソースのルーティング
タスクを実行したときに出力された通り、web/router.ex
に todos
リソースのルーティング設定をします。ただ、今回は v1
のようにバージョニングされた API リソースとするため、出力されたルーティング設定をそのまま記載するのではなく、バージョンごとにスコープを分けたルーティングにします。
web/router.ex
を以下のように修正します。
defmodule RestApiExample.Router do
use RestApiExample.Web, :router
pipeline :api do
plug :accepts, ["json"]
end
scope "/", RestApiExample do
pipe_through :api
scope "/v1", V1, as: :v1 do
resources "/todos", TodoController
end
end
end
API リソースでは利用しない設定をざっくり削除して、必要な設定のみを追加しています。
ルーティングの設定は phoenix.routes
タスクで確認できます。
$ mix phoenix.routes
v1_todo_path GET /v1/todos RestApiExample.V1.TodoController :index
v1_todo_path GET /v1/todos/:id/edit RestApiExample.V1.TodoController :edit
v1_todo_path GET /v1/todos/new RestApiExample.V1.TodoController :new
v1_todo_path GET /v1/todos/:id RestApiExample.V1.TodoController :show
v1_todo_path POST /v1/todos RestApiExample.V1.TodoController :create
v1_todo_path PATCH /v1/todos/:id RestApiExample.V1.TodoController :update
PUT /v1/todos/:id RestApiExample.V1.TodoController :update
v1_todo_path DELETE /v1/todos/:id RestApiExample.V1.TodoController :delete
ルーティングの設定が済んだので、ここでデータベースのマイグレーションをしておきます。
$ mix ecto.migrate
バージョニングのスコープへの適合
リソースファイルの生成とルーティングの設定をしましたが、生成されたファイルの構成は今回のバージョニングのスコープと一致していません。そのスコープに適合させるため、以下のようにディレクトリ構成を変更します。
$ mkdir -p web/controllers/v1
$ mv web/controllers/todo_controller.ex web/controllers/v1
$ mkdir -p web/views/v1
$ mv web/views/todo_view.ex web/views/v1
$ mkdir -p test/controllers/v1
$ mv test/controllers/todo_controller_test.exs test/controllers/v1/
また、モジュール名も合わせて変更しておく必要があります。
web/controllers/v1/todo_controller.ex
defmodule RestApiExample.V1.TodoController do
.
end
test/controllers/v1/todo_controller_test.exs
defmodule RestApiExample.V1.TodoControllerTest do
...
end
web/views/v1/todo_view.ex
defmodule RestApiExample.V1.TodoView do
...
end
バージョニングのスコープに合わせた修正ができたので、一度テストを実行してみます。
$ mix test
だらららら〜、とコンパイルのログが流れて…げ、コンパイルエラーになったぞ。
** (CompileError) test/controllers/v1/todo_controller_test.exs:14: function todo_path/2 undefined
(stdlib) lists.erl:1336: :lists.foreach/2
(stdlib) erl_eval.erl:657: :erl_eval.do_apply/6
この辺りが今回一番ハマったところなんですが、これは、出力されているように、todo_path/2
というヘルパーが定義されていない、というエラーです。なぜ定義されていないかというと、ルーティングで v1
というスコープを指定すると、as: :v1
というオプションを指定したことになり、その場合には、todo_path/2
ヘルパーには v1_
という prefix を付ける必要があるようです。したがって、テストを修正する必要があります。したがって、test/controllers/v1/todo_controller_test.exs
の todo_path
をすべて v1_todo_path
に変更してください。
修正してもう一度 mix test
を実行すると…
** (UndefinedFunctionError) undefined function: RestApiExample.TodoView.__resource__/0 (module RestApiExample.TodoView is not available)
またもエラー…この原因はどうやら、ビューのコードの中で render_many/2
と render_one/2
を実行しているところがあるんですが、今回のようにバージョニングのスコープを指定している場合、これらが更にその内部で実行している Phoenix.View.render_many/3
と Phoenix.View.render_many/3
が期待通りに動作しないことにあるようです。したがって、以下のように、ビューファイルの中で直接それらを実行するように修正する必要があります。
web/views/v1/todo_view.ex
defmodule RestApiExample.V1.TodoView do
use RestApiExample.Web, :view
def render("index.json", %{todos: todos}) do
%{data: render_many(todos, RestApiExample.V1.TodoView, "todo.json")}
end
def render("show.json", %{todo: todo}) do
%{data: render_one(todo, RestApiExample.V1.TodoView, "todo.json")}
end
def render("todo.json", %{todo: todo}) do
%{id: todo.id}
end
end
これでやっとテストが通るようになりました。
$ mix test
.............
Finished in 0.7 seconds (0.4s on load, 0.2s on tests)
13 tests, 0 failures
この辺りのエラーに関しては、以下の記事を参考にしました。この記事がなければ投げ出していたかもしれません。感謝。
Building a versioned REST API with Phoenix Framework
ビューの JSON を修正
今のビューの設定だと、Todo リソースを参照したときの JSON がその ID だけしか含まなくて寂しいので、title
などもわかるように修正します。
web/views/v1/todo_view.ex
defmodule RestApiExample.V1.TodoView do
...
def render("todo.json", %{todo: todo}) do
%{id: todo.id, title: todo.title, completed: todo.completed}
end
end
API にアクセスする
それでは、超簡単なものですがこれで REST API ができたので、早速アクセスしてみましょう。
サーバーの起動
$ mix phoenix.server
Todo リソースの作成
$ curl -H "Content-Type: application/json" -X POST -d '{"todo":{"title":"Buy a Wii U","completed":true}}' http://localhost:4000/v1/todos
{"data":{"title":"Buy a Wii U","id":1,"completed":true}}
$ curl -H "Content-Type: application/json" -X POST -d '{"todo":{"title":"Get A+","completed":false}}' http://localhost:4000/v1/todos
{"data":{"title":"Get A+","id":2,"completed":false}}
すべての Todo リソースの取得
$ curl -H "Content-Type: application/json" http://localhost:4000/v1/todos
{"data":[{"title":"Buy a Wii U","id":1,"completed":true},{"title":"Get A+","id":2,"completed":false}]}
ID を指定して取得
$ curl -H "Content-Type: application/json" http://localhost:4000/v1/todos/2
{"data":{"title":"Get A+","id":2,"completed":false}}
おわりに
ショボいながらも、Phoenix をこれではじめてまともに触ったのですが、Rails を使ったことがあるのなら全体の構造は初見でも大体把握できるし、ドキュメントや情報も思った以上に充実している印象でした。ただ、Phoenix はまだ新しく頻繁に互換性の無い変更が入るのと、まー当然だけど Elixir のパターンマッチとかをちゃんと理解してないと簡単な機能とかテストの実装もできないですね。また、今回のようなバージョニングだと、今後バージョンアップのたびに大量のコピペコードが登場することになるだろうと思うので、本気で構築するならもっと良い方法を検討する必要があるでしょう。