Phoenix でバージョニングした REST API を構築する

簡単な API サーバーを作る必要があったので、Elixir の WAF である Phoenix を使ってみました。API バージョンでスコープを分けた RESTful API 構成にしようとしたのですが、なんだか結構ハマってしまったので簡単な手順を残します。  

`title` と `completed` という2つのフィールドを持つ `todo` という単純なリソースを例とします。

## アプリケーションの作成

`phoenix.new` タスクで新規の Phoenix アプリケーションを作成します。このとき、今回は REST API リソースを構築するので、アセットを管理するためのツールである `Brunch` は不要です。したがって `--no-brunch` オプションをつけて実行します。

```text
$ mix phoenix.new rest_api_example --no-brunch
```

これで、Phoenix アプリケーションの雛形が作成されるので、まずは出力された指示にしたがってデータベースの作成をします。  

ここで、以下のエラーが出力されて、データベースの作成に失敗することあります。

```text
** (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` などの設定を変更することによって変更することができるようです) 。

```text
$ psql -d postgres
```

```text
=# CREATE ROLE postgres LOGIN CREATEDB;
```

データベースが作られたら、デフォルトで生成されるものの中で、使わない不要なファイルを削除しておきます。以下の通り、今回は利用しない `page` リソースのファイルを削除してください。

```text
$ 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` タスクを使います。

```text
$ 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`** を以下のように修正します。

```elixir
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` タスクで確認できます。

```text
$ 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
```

ルーティングの設定が済んだので、ここでデータベースのマイグレーションをしておきます。

```text
$ mix ecto.migrate
```

### バージョニングのスコープへの適合

リソースファイルの生成とルーティングの設定をしましたが、生成されたファイルの構成は今回のバージョニングのスコープと一致していません。そのスコープに適合させるため、以下のようにディレクトリ構成を変更します。

```text
$ 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**

```elixir
defmodule RestApiExample.V1.TodoController do
.
end
```

**test/controllers/v1/todo_controller_test.exs**

```elixir
defmodule RestApiExample.V1.TodoControllerTest do
...
end
```

**web/views/v1/todo_view.ex**

```elixir
defmodule RestApiExample.V1.TodoView do
...
end
```

バージョニングのスコープに合わせた修正ができたので、一度テストを実行してみます。

```test
$ mix test
```

だらららら〜、とコンパイルのログが流れて…げ、コンパイルエラーになったぞ。

```text
** (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` を実行すると…

```text
** (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`**

```elixir
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
```

これでやっとテストが通るようになりました。

```text
$ 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](https://renatomoya.github.io/2015/05/09/Building-a-versioned-REST-API-with-Phoenix-Framework.html)

### ビューの JSON を修正

今のビューの設定だと、Todo リソースを参照したときの JSON がその ID だけしか含まなくて寂しいので、`title` などもわかるように修正します。  

**`web/views/v1/todo_view.ex`**

```elixir
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 ができたので、早速アクセスしてみましょう。

### サーバーの起動

```text
$ mix phoenix.server
```

### Todo リソースの作成

```text
$ 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}}
```

```text
$ 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 リソースの取得

```text
$ 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 を指定して取得

```text
$ curl -H "Content-Type: application/json" http://localhost:4000/v1/todos/2
{"data":{"title":"Get A+","id":2,"completed":false}}
```

## おわりに

ショボいながらも、Phoenix をこれではじめてまともに触ったのですが、Rails を使ったことがあるのなら全体の構造は初見でも大体把握できるし、ドキュメントや情報も思った以上に充実している印象でした。ただ、Phoenix はまだ新しく頻繁に互換性の無い変更が入るのと、まー当然だけど Elixir のパターンマッチとかをちゃんと理解してないと簡単な機能とかテストの実装もできないですね。また、今回のようなバージョニングだと、今後バージョンアップのたびに大量のコピペコードが登場することになるだろうと思うので、本気で構築するならもっと良い方法を検討する必要があるでしょう。