概要
普段UZUMAKIではRailsのお仕事が多いのですが、ここではElixirベースのフレームワークであるPhoenixを使ったAPI開発のノウハウを共有していきたいと思っています。チュートリアルではあまり紹介されないテストの方法や、定番のライブラリの紹介・設定も説明します。
ElixirはErlang VM上で動く関数型言語で、RailsコミッターのJoséさんが作ったプログラミング言語です。
Elixirの特徴としては、
- 並列処理を安定的に実行できる
- 耐障害性が高い
- シンプルでとっつきやすい言語仕様
などが挙げられます。
詳しくは以下の辺りに分かり易く纏まっています。
性能面ではRailsを利用しているプロジェクトと比べて、サーバー台数を半分程度で抑えられているという事例もあるようです。
Elixirは構文がRubyに似てるところもありRubyistにとってはとっつきやすいかもしれません。
今回はdockerを使ってPhoenixのAPIサーバーを構築し、さらにElixir + Phoenix開発では必須と言っても良い便利なツールなどを紹介していければと思っています。
この記事がElixirとPhoenixに興味を持つきっかけになれば嬉しいです。
目次
対象読者
- RubyやRailsに少し飽きてきて他の言語やフレームワークにも触れてみたいと思っている人
- ElixirやPhoenixに興味はあったけど実際動かすまではやってない人
PR
UZUMAKIではアジャイル開発で新規事業の開発から、大規模Webアプリケーションのアーキテクチャ更新などの開発をしています。
お問い合わせはUZUMAKIのHPのお問合せフォームから
本文
サーバー構築
まずはdockerを使いPhoenixサーバーが応答を返すところまで構築します。
(※ Docker for Mac 以外での動作確認はしていません。)
環境
MacBook Pro (Intel, 2020)
OS: macOS Monterey 12.6
Docker version 20.10.17
サーバー構成
言語: Elixir 1.14
フレームワーク: Phoenix 1.6.15
DB: mysql 8
作業ディレクトリ作成
$ mkdir todo-phoenix
$ cd todo-phoenix
Dockerfileとdocker-compose.ymlを用意
Dockerfile
FROM elixir:1.14-alpine
RUN apk update && apk add \
inotify-tools git build-base npm bash
RUN mix do local.hex --force, local.rebar --force, archive.install --force hex phx_new
WORKDIR /app
docker-compose.yml
version: '3'
services:
web:
build:
context: .
dockerfile: ./Dockerfile
tty: true
environment:
- MIX_ENV=dev
- PORT=4000
- DB_HOST=db
links:
- db
ports:
- 4000:4000
command: mix phx.server
volumes:
- .:/app
db:
image: mysql:8
environment:
- MYSQL_ROOT_PASSWORD=password
ports:
- 3306:3306
volumes:
- mysql_volume:/var/lib/mysql
volumes:
mysql_volume:
build
$ docker-compose build
ElixirとPhoenixのバージョンを確認。
# Elixir
$ docker compose run --rm web elixir -v
[+] Running 2/2
⠿ Volume "todo-phoenix_mysql_volume" Created 0.0s
⠿ Container todo-phoenix-db-1 Created 0.1s
[+] Running 1/1
⠿ Container todo-phoenix-db-1 Started 0.4s
Erlang/OTP 25 [erts-13.0.4] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit]
Elixir 1.14.0 (compiled with Erlang/OTP 25)
# Phoenix
docker compose run --rm web mix phx.new -v
[+] Running 1/0
⠿ Container todo-phoenix-db-1 Running 0.0s
Phoenix installer v1.6.15
Phoenixプロジェクト作成
$ docker-compose run web mix phx.new . --app todo_phoenix --database mysql --no-assets --no-html --no-gettext
# The directory /app already exists. Are you sure you want to continue? [Yn] Y
# Fetch and install dependencies? [Yn] Y
phx.newのoptionsはこちらを参照してください。
プロジェクト作成後のディレクトリ構造は以下のようになっています。
├── Dockerfile
├── README.md
├── _build # 階層が深いので省略。ビルドしたファイルを格納
├── config
│ ├── config.exs
│ ├── dev.exs
│ ├── prod.exs
│ ├── runtime.exs
│ └── test.exs
├── deps # 階層が深いので省略。ライブラリファイルを格納
├── docker-compose.yml
├── lib
│ ├── todo_phoenix
│ │ ├── application.ex
│ │ ├── mailer.ex
│ │ └── repo.ex
│ ├── todo_phoenix.ex
│ ├── todo_phoenix_web
│ │ ├── controllers
│ │ ├── endpoint.ex
│ │ ├── router.ex
│ │ ├── telemetry.ex
│ │ └── views
│ │ ├── error_helpers.ex
│ │ └── error_view.ex
│ └── todo_phoenix_web.ex
├── mix.exs
├── mix.lock
├── priv
│ └── repo
│ ├── migrations
│ └── seeds.exs
└── test
├── support
│ ├── conn_case.ex
│ └── data_case.ex
├── test_helper.exs
└── todo_phoenix_web
├── controllers
└── views
└── error_view_test.exs
ディレクトリ構造に関してはこちらに詳しく書かれています。
設定変更
config/dev.exs
の以下を修正します。
@@ -3,8 +3,8 @@
# Configure your database
config :todo_phoenix, TodoPhoenix.Repo,
username: "root",
- password: "",
- hostname: "localhost",
+ password: "password", # dockerの設定情報に合わせてDBの接続情報を編集
+ hostname: "db", # dockerの設定情報に合わせてDBの接続情報を編集
database: "todo_phoenix_dev",
stacktrace: true,
show_sensitive_data_on_connection_error: true,
@@ -19,7 +19,7 @@
config :todo_phoenix, TodoPhoenixWeb.Endpoint,
# Binding to loopback ipv4 address prevents access from other machines.
# Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
- http: [ip: {127, 0, 0, 1}, port: 4000],
+ http: [ip: {0, 0, 0, 0}, port: 4000], # コンテナの外からアクセスできるようにする
check_origin: false,
code_reloader: true,
debug_errors: true,
DB作成
$ docker-compose run --rm web mix ecto.create
Creating todo-phoenix_web_run ... done
The database for TodoPhoenix.Repo has been created
動作確認
Phoenix Serverから200のHTTPステータスが返ることを確認します。
# コンテナを起動
$ docker-compose up
# 別窓でアクセス確認
$ curl -I http://localhost:4000/dashboard/home
HTTP/1.1 200 OK
API作成
ここからは実際に簡単なAPIを作成し動かすところまでやっていきます。
今回はtodoアプリケーションのtask追加と取得ができるようにします。
スキーマ作成
以下の情報でスキーマを作成していきます。
テーブル名: todo_tasks
カラム: content, state
$ docker-compose run --rm web mix phx.gen.schema Todo.Task todo_tasks content:string state:integer
Creating todo-phoenix_web_run ... done
* creating lib/todo_phoenix/todo/task.ex
* creating priv/repo/migrations/20221030080244_create_todo_tasks.exs
コマンドを実行するとスキーマファイルとマイグレーションファイルの2ファイルが作成されます。
スキーマファイルを開いてみます。
lib/todo_phoenix/todo/task.ex
defmodule TodoPhoenix.Todo.Task do
use Ecto.Schema
import Ecto.Changeset
schema "todo_tasks" do
field :content, :string
field :state, :integer
timestamps()
end
@doc false
def changeset(task, attrs) do
task
|> cast(attrs, [:content, :state])
|> validate_required([:content, :state])
end
end
カラムの型や簡単なバリデーションが定義されていることが分かります。
マイグレーション実行
$ docker-compose run --rm web mix ecto.migrate
Creating todo-phoenix_web_run ... done
Compiling 1 file (.ex)
Generated todo_phoenix app
08:07:15.936 [info] == Running 20221030080244 TodoPhoenix.Repo.Migrations.CreateTodoTasks.change/0 forward
08:07:16.000 [info] create table todo_tasks
08:07:16.050 [info] == Migrated 20221030080244 in 0.0s
mysqlに接続し、テーブルを確認してみます。
$ mysql -h 127.0.0.1 -P 3306 -u root -p todo_phoenix_dev
mysql> desc todo_tasks;
+-------------+-----------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+-----------------+------+-----+---------+----------------+
| id | bigint unsigned | NO | PRI | NULL | auto_increment |
| content | varchar(255) | YES | | NULL | |
| state | int | YES | | NULL | |
| inserted_at | datetime | NO | | NULL | |
| updated_at | datetime | NO | | NULL | |
+-------------+-----------------+------+-----+---------+----------------+
5 rows in set (0.01 sec)
テーブルが作成されていますね!
ではいよいよコーディングをしていきましょう。
コンテキスト作成
コンテキストとは関連する関数を纏める専用モジュールです。詳しくはこちらを参照してください。
ちなみに先に出たスキーマは、リレーション定義も書くのでRailsのモデルに似ていますが、ビジネスロジックに関してはスキーマではなくコンテキストに書くことが多いです。
コンテキストを作成する前にlib下のディレクトリ構造を見てみます。
(Phoenixでは主にlib下をさわります。
lib
├── todo_phoenix
│ ├── application.ex
│ ├── mailer.ex
│ ├── repo.ex
│ └── todo
│ └── task.ex
├── todo_phoenix.ex
├── todo_phoenix_web
│ ├── controllers
│ ├── endpoint.ex
│ ├── router.ex
│ ├── telemetry.ex
│ └── views
│ ├── error_helpers.ex
│ └── error_view.ex
└── todo_phoenix_web.ex
5 directories, 11 files
アプリケーション名(todo_phoenix)の下には先ほど確認したスキーマ定義ファイル(task.ex)がありますね。
また、controllerやviewはtodo_phoenix_web下に配置する構造になっています。
今回コンテキストはtodo_phoenix下にtodo.exという名前で作成してみます。
lib/todo_phoenix/todo.ex
defmodule TodoPhoenix.Todo do
import Ecto.Query, warn: false
alias TodoPhoenix.Repo
alias TodoPhoenix.Todo.Task
@doc """
タスク一覧を取得する。
"""
def list_tasks do
Repo.all(Task)
end
@doc """
タスクを作成する。
"""
def create_task(attrs \\ %{}) do
%Task{}
|> Task.changeset(attrs)
|> Repo.insert()
end
end
コンソールから動作を確認してみます。
iexコンソールへの接続
# コンテナに接続
$ docker exec -it todo-phoenix_web_1 bash
$ iex -S mix
Erlang/OTP 25 [erts-13.0.4] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit]
Compiling 1 file (.ex)
Generated todo_phoenix app
Interactive Elixir (1.14.0) - press Ctrl+C to exit (type h() ENTER for help)
iexコンソール
# コンテキストに定義した関数を使いtaskを取得
iex(1)> TodoPhoenix.Todo.list_tasks
[debug] QUERY OK source="todo_tasks" db=0.5ms decode=12.6ms queue=17.3ms idle=1085.5ms
SELECT t0.`id`, t0.`content`, t0.`state`, t0.`inserted_at`, t0.`updated_at` FROM `todo_tasks` AS t0 []
↳ anonymous fn/4 in :elixir.eval_external_handler/1, at: src/elixir.erl:298
[]
# 登録が無いので空の配列を返す
# コンテキストに定義した関数を使いtaskを作成
iex(2)> TodoPhoenix.Todo.create_task(%{content: "hoge", state: 0})
[debug] QUERY OK db=4.0ms queue=1.0ms idle=1991.9ms
INSERT INTO `todo_tasks` (`content`,`state`,`inserted_at`,`updated_at`) VALUES (?,?,?,?) ["hoge", 0, ~N[2022-10-30 10:27:00], ~N[2022-10-30 10:27:00]]
↳ anonymous fn/4 in :elixir.eval_external_handler/1, at: src/elixir.erl:298
{:ok,
%TodoPhoenix.Todo.Task{
__meta__: #Ecto.Schema.Metadata<:loaded, "todo_tasks">,
id: 1,
content: "hoge",
state: 0,
inserted_at: ~N[2022-10-30 10:27:00],
updated_at: ~N[2022-10-30 10:27:00]
}}
iex(3)> TodoPhoenix.Todo.list_tasks
[debug] QUERY OK source="todo_tasks" db=2.9ms queue=0.1ms idle=1543.0ms
SELECT t0.`id`, t0.`content`, t0.`state`, t0.`inserted_at`, t0.`updated_at` FROM `todo_tasks` AS t0 []
↳ anonymous fn/4 in :elixir.eval_external_handler/1, at: src/elixir.erl:298
[
%TodoPhoenix.Todo.Task{
__meta__: #Ecto.Schema.Metadata<:loaded, "todo_tasks">,
id: 1,
content: "hoge",
state: 0,
inserted_at: ~N[2022-10-30 10:27:00],
updated_at: ~N[2022-10-30 10:27:00]
}
]
# 正常に登録されている
ついでにtask.stateをEnumの管理に変更してみます。
lib/todo_phoenix/todo/task.ex
# 修正前
field :state, :integer
# 修正後
field :state, Ecto.Enum, values: [new: 0, doing: 1, done: 2], default: :new
再度コンソールから実行してみましょう。
# 既にコンソールを開いてる場合は以下で再コンパイルをお願いします。
iex(4)> recompile
# stateはアトム型で指定できる
iex(5)> TodoPhoenix.Todo.create_task(%{content: "fuga", state: :doing})
[debug] QUERY OK db=5.4ms queue=1.2ms idle=1078.8ms
INSERT INTO `todo_tasks` (`content`,`state`,`inserted_at`,`updated_at`) VALUES (?,?,?,?) ["fuga", :doing, ~N[2022-10-30 11:04:25], ~N[2022-10-30 11:04:25]]
↳ anonymous fn/4 in :elixir.eval_external_handler/1, at: src/elixir.erl:298
{:ok,
%TodoPhoenix.Todo.Task{
__meta__: #Ecto.Schema.Metadata<:loaded, "todo_tasks">,
id: 2,
content: "fuga",
state: :doing,
inserted_at: ~N[2022-10-30 11:04:25],
updated_at: ~N[2022-10-30 11:04:25]
}}
# stateを指定しない場合デフォルト値のnewが入る
iex(6)> TodoPhoenix.Todo.create_task(%{content: "piyo"})
[debug] QUERY OK db=8.4ms queue=1.8ms idle=1398.0ms
INSERT INTO `todo_tasks` (`content`,`state`,`inserted_at`,`updated_at`) VALUES (?,?,?,?) ["piyo", :new, ~N[2022-10-30 11:04:32], ~N[2022-10-30 11:04:32]]
↳ anonymous fn/4 in :elixir.eval_external_handler/1, at: src/elixir.erl:298
{:ok,
%TodoPhoenix.Todo.Task{
__meta__: #Ecto.Schema.Metadata<:loaded, "todo_tasks">,
id: 3,
content: "piyo",
state: :new,
inserted_at: ~N[2022-10-30 11:04:32],
updated_at: ~N[2022-10-30 11:04:32]
}}
railsでもEnumを使うことが多いですがPhoenixでも同じように使えますね。
さて、コンソールで確認した限りコンテキストの動作に問題は無さそうです。
次はコントローラ、ビューを作成していきます。
コントローラ、ビュー作成
lib/todo_phoenix_web/controllers/task_controller.ex
defmodule TodoPhoenixWeb.TaskController do
use TodoPhoenixWeb, :controller
alias TodoPhoenix.Todo
alias TodoPhoenix.Todo.Task
def index(conn, _params) do
tasks = Todo.list_tasks()
render(conn, "index.json", tasks: tasks)
end
def create(conn, %{"task" => task_params}) do
case Todo.create_task(task_params) do
{:ok, %Task{}} ->
send_resp(conn, :created, "")
{:error, _} ->
send_resp(conn, :unprocessable_entity, "")
end
end
def create(conn, _), do: send_resp(conn, :bad_request, "")
end
lib/todo_phoenix_web/views/task_view.ex
defmodule TodoPhoenixWeb.TaskView do
use TodoPhoenixWeb, :view
alias TodoPhoenixWeb.TaskView
def render("index.json", %{tasks: tasks}) do
%{data: render_many(tasks, TaskView, "task.json")}
end
def render("task.json", %{task: task}) do
%{
id: task.id,
content: task.content,
state: task.state
}
end
end
lib/todo_phoenix_web/router.ex
scope "/api", TodoPhoenixWeb do
pipe_through :api
resources "/tasks", TaskController, only: [:create, :index] # 追加
end
API動作確認
動作確認する前に、分かり易いようにコンテキストのテスト時に作成したtodo_tasksのデータを予め削除しておきましょう。
また、コンテナが起動してない場合はdocker-compose up
で起動してください。
# GET tasks
curl http://localhost:4000/api/tasks | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 11 100 11 0 0 53 0 --:--:-- --:--:-- --:--:-- 56
{
"data": []
}
# データは空
# POST tasks
$ curl -v http://localhost:4000/api/tasks -H "Content-Type: application/json" -d '{"task":{"content": "foo"}}'
* Trying 127.0.0.1:4000...
* Connected to localhost (127.0.0.1) port 4000 (#0)
> POST /api/tasks HTTP/1.1
> Host: localhost:4000
> User-Agent: curl/7.79.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 27
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 201 Created
# GET tasks
curl http://localhost:4000/api/tasks | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 50 100 50 0 0 277 0 --:--:-- --:--:-- --:--:-- 294
{
"data": [
{
"content": "foo",
"id": 4,
"state": "new"
}
]
}
# データが作成されている
# POST tasks
curl -v http://localhost:4000/api/tasks -H "Content-Type: application/json" -d '{"task":{"content": "bar"}}'
* Trying 127.0.0.1:4000...
* Connected to localhost (127.0.0.1) port 4000 (#0)
> POST /api/tasks HTTP/1.1
> Host: localhost:4000
> User-Agent: curl/7.79.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 27
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 201 Created
# GET tasks
curl http://localhost:4000/api/tasks | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 90 100 90 0 0 511 0 --:--:-- --:--:-- --:--:-- 545
{
"data": [
{
"content": "foo",
"id": 4,
"state": "new"
},
{
"content": "bar",
"id": 5,
"state": "new"
}
]
}
# 追加で1件データが作成されている
良さそうですね!
次はテストを書いていきます。
テスト
設定変更
config/test.exs
の以下を修正します。
@@ -7,8 +7,8 @@
# Run `mix help test` for more information.
config :todo_phoenix, TodoPhoenix.Repo,
username: "root",
- password: "",
- hostname: "localhost",
+ password: "password", # dockerの設定情報に合わせてDBの接続情報を編集
+ hostname: "db", # dockerの設定情報に合わせてDBの接続情報を編集
database: "todo_phoenix_test#{System.get_env("MIX_TEST_PARTITION")}",
pool: Ecto.Adapters.SQL.Sandbox,
pool_size: 10
fixturesを準備
$ mkdir test/support/fixtures
test/support/fixtures/todo_fixtures.ex
defmodule TodoPhoenix.TodoFixtures do
def task_fixture(attrs \\ %{}) do
{:ok, task} =
attrs
|> Enum.into(%{
content: "hoge",
state: :new
})
|> TodoPhoenix.Todo.create_task()
task
end
end
コンテキストテスト作成
$ mkdir test/todo_phoenix
test/todo_phoenix/todo_test.exs
defmodule TodoPhoenix.TodoTest do
use TodoPhoenix.DataCase
import TodoPhoenix.TodoFixtures
alias TodoPhoenix.Todo
alias TodoPhoenix.Todo.Task
describe "task.list_tasks" do
test "taskが配列で取得できること" do
task = task_fixture()
assert Todo.list_tasks() == [task]
end
end
describe "task.create_task" do
@valid_attrs %{content: "bar", state: :doing}
@invalid_attrs %{content: nil}
test "taskが登録できること" do
assert {:ok, %Task{} = task} = Todo.create_task(@valid_attrs)
assert task.content == "bar"
assert task.state == :doing
end
test "パラメータが不正な場合、Changesetエラーが返却されること" do
assert {:error, %Ecto.Changeset{}} = Todo.create_task(@invalid_attrs)
end
end
end
テスト実行
# コンテナ内で実行しています
# 初回はコンパイルが走ってテスト実行まで時間がかかります。
$ MIX_ENV=test mix test test/todo_phoenix/todo_test.exs
...
Finished in 0.4 seconds (0.00s async, 0.4s sync)
3 tests, 0 failures
コントローラテスト作成
test/todo_phoenix_web/controllers/task_controller_test.exs
defmodule TodoPhoenixWeb.TaskControllerTest do
use TodoPhoenixWeb.ConnCase
@create_attrs %{
content: "bar",
state: :new
}
@invalid_attrs %{content: nil}
@blank_params %{}
setup %{conn: conn} do
{:ok, conn: put_req_header(conn, "accept", "application/json")}
end
describe "index" do
test "ステータスコード200を返却すること", %{conn: conn} do
conn = get(conn, Routes.task_path(conn, :index))
assert json_response(conn, :ok)["data"] == []
end
end
describe "create task" do
test "登録に成功した場合、ステータスコード201を返却すること", %{conn: conn} do
conn = post(conn, Routes.task_path(conn, :create), task: @create_attrs)
assert response(conn, :created)
end
test "パラメータが不正な場合、ステータスコード422を返却すること", %{conn: conn} do
conn = post(conn, Routes.task_path(conn, :create), task: @invalid_attrs)
assert response(conn, :unprocessable_entity)
end
test "パラメータが空の場合、ステータスコード400を返すこと", %{conn: conn} do
conn = post(conn, Routes.task_path(conn, :create), @blank_params)
assert response(conn, :bad_request)
end
end
end
テスト実行
$ MIX_ENV=test mix test test/todo_phoenix_web/controllers/task_controller_test.exs
....
Finished in 0.7 seconds (0.00s async, 0.7s sync)
4 tests, 0 failures
全てのテストを実行
$ MIX_ENV=test mix test
.........
Finished in 0.9 seconds (0.4s async, 0.4s sync)
9 tests, 0 failures
# 内2つはデフォルトで実装されているテストです。
ここまででAPIサーバーの構築、テストの実装まで行いました。
次はElixir + Phoenix開発では必須のライブラリ(hex)を導入してみます。
Credo
credoはrailsではお馴染みのrubocopのような構文チェックツールです。
では導入していきます。
mix.exs
defp deps do
[
{:phoenix, "~> 1.6.15"},
{:phoenix_ecto, "~> 4.4"},
{:ecto_sql, "~> 3.6"},
{:myxql, ">= 0.0.0"},
{:phoenix_live_dashboard, "~> 0.6"},
{:swoosh, "~> 1.3"},
{:telemetry_metrics, "~> 0.6"},
{:telemetry_poller, "~> 1.0"},
{:jason, "~> 1.2"},
{:plug_cowboy, "~> 2.5"}, # カンマを追加
{:credo, "~> 1.6", only: [:dev, :test], runtime: false}, # 追加
]
end
インストール
$ docker-compose run --rm web mix deps.get
実行する前に、警告を出してくれることを期待してコード中にデバッグ文を入れてみます。
lib/todo_phoenix_web/controllers/task_controller.ex
12 def create(conn, %{"task" => task_params}) do
13 IO.inspect(task_params) # こちらを追加
実行
$ docker-compose run --rm web mix credo
結果
先ほど入れたデバッグ文は[W]でちゃんと警告してくれてますね。
Dialyzer
Elixirはrubyと同じく動的型付け言語ですが、Typespecsという型と仕様を宣言するための記法を提供しており、dialyzerはそちらをチェクしてくれるツールです。
では導入していきます。
mix.exs
defp deps do
[
{:phoenix, "~> 1.6.15"},
{:phoenix_ecto, "~> 4.4"},
{:ecto_sql, "~> 3.6"},
{:myxql, ">= 0.0.0"},
{:phoenix_live_dashboard, "~> 0.6"},
{:swoosh, "~> 1.3"},
{:telemetry_metrics, "~> 0.6"},
{:telemetry_poller, "~> 1.0"},
{:jason, "~> 1.2"},
{:plug_cowboy, "~> 2.5"},
{:credo, "~> 1.6", only: [:dev, :test], runtime: false},
{:dialyxir, "~> 1.0", only: [:dev], runtime: false}, # 追加
]
end
インストール
$ docker-compose run --rm web mix deps.get
型を定義
lib/todo_phoenix/todo/task.ex
defmodule TodoPhoenix.Todo.Task do
use Ecto.Schema
import Ecto.Changeset
@type t :: %__MODULE__{} # 追加。これを追加することでmodule名.t()で他から参照できるようになります。
schema "todo_tasks" do
lib/todo_phoenix/todo.ex
defmodule TodoPhoenix.Todo do
import Ecto.Query, warn: false
alias TodoPhoenix.Repo
alias TodoPhoenix.Todo.Task
@doc """
タスク一覧を取得する。
"""
@spec list_tasks() :: [Task.t()] # 追加
def list_tasks do
Repo.all(Task)
end
@doc """
タスクを作成する。
"""
@spec create_task(map()) :: {:ok, Task.t()} # 追加
def create_task(attrs \\ %{}) do
%Task{}
|> Task.changeset(attrs)
|> Repo.insert()
end
end
実行
$ docker-compose run --rm web mix dialyzer
結果
エラーになりましたね。エラー内容をみると、コントローラの16行目でerrorのパターンマッチの記載が有るが、コンテキスト側の返却型定義にerrorパターンが無いと警告されていることが分かります。
lib/todo_phoenix_web/controllers/task_controller.ex
13 case Todo.create_task(task_params) do
14 {:ok, %Task{}} ->
15 send_resp(conn, :created, "")
16 {:error, _} ->
17 send_resp(conn, :unprocessable_entity, "")
18 end
コンテキストのcreate_task
関数のspecを以下のように複数の返却型があるように修正してみましょう。
この関数はRepo.insert()の結果を返してるのでそちらを参考にエラーパターンを追加します。
lib/todo_phoenix/todo.ex
@doc """
タスクを作成する。
"""
@spec create_task(map()) :: {:ok, Task.t()} | {:error, Ecto.Changeset.t()} # エラーパターンを追加
def create_task(attrs \\ %{}) do
%Task{}
|> Task.changeset(attrs)
|> Repo.insert()
end
再度実行してみます。
$ docker-compose run --rm web mix dialyzer
Creating todo-phoenix_web_run ... done
Compiling 1 file (.ex)
Finding suitable PLTs
Checking PLT...
[:asn1, :castore, :compiler, :connection, :cowboy, :cowboy_telemetry, :cowlib, :crypto, :db_connection, :decimal, :ecto, :ecto_sql, :eex, :elixir, :jason, :kernel, :logger, :mime, :myxql, :phoenix, :phoenix_ecto, :phoenix_html, :phoenix_live_dashboard, :phoenix_live_view, :phoenix_pubsub, :phoenix_template, :phoenix_view, :plug, :plug_cowboy, :plug_crypto, :public_key, :ranch, :runtime_tools, :ssl, :stdlib, :swoosh, :telemetry, :telemetry_metrics, :telemetry_poller, :xmerl]
PLT is up to date!
No :ignore_warnings opt specified in mix.exs and default does not exist.
Starting Dialyzer
[
check_plt: false,
init_plt: '/app/_build/dev/dialyxir_erlang-25.0.4_elixir-1.14.0_deps-dev.plt',
files: ['/app/_build/dev/lib/todo_phoenix/ebin/Elixir.TodoPhoenix.Application.beam',
'/app/_build/dev/lib/todo_phoenix/ebin/Elixir.TodoPhoenix.Mailer.beam',
'/app/_build/dev/lib/todo_phoenix/ebin/Elixir.TodoPhoenix.Repo.beam',
'/app/_build/dev/lib/todo_phoenix/ebin/Elixir.TodoPhoenix.Todo.Task.beam',
'/app/_build/dev/lib/todo_phoenix/ebin/Elixir.TodoPhoenix.Todo.beam', ...],
warnings: [:unknown]
]
Total errors: 0, Skipped: 0, Unnecessary Skips: 0
done in 0m3.12s
done (passed successfully)
警告無く完了しましたね。
このようにtypespec + dialyzer を使用することで、型安全で可読性の高いコードを書いていけそうです。
まとめ
ElixirとPhoenixを使ってサクッとAPIサーバーを作成することができました。
静的解析ツールのcredoやdialyzerはCIで設定すれば便利そうですね。
Elixirは今年仕事で少し書く機会がありましたが、パイプライン演算子やパターンマッチなど、凄く魅力的で書いてて楽しい言語です。
最近話題になっているリアルタイムUIが作れるLiveViewも面白そうですね。
やってみた系の記事では以下が詳しく書かれてそうです。
そして2022年11月現在、WEB+DB PRESS最新号の Vol.131では、何とRustと並んで表紙にElixirが載っています。
ElixirとPhoenixは今後ますます盛り上がってくるのではないかと思っています。
参考リンク
- Phoenix v1.6 hexdocs 日本語訳
- 【メモ】私が愛する Elixir/Erlang の楽しさと辛さ
- 並列プログラミング言語 Elixir (エリクサー) におけるソフトウェアテスト〜基礎から最新展望まで
- 『ロマサガRS』は如何にリリース直後のアクセスラッシュを乗り切ったのか…クイックスケールの要点やElixirを活用した技術を解説
- 様々な開発マシンでもPhoenixが動くDockerfile/docker-composeの作り方
- docker composeでphoenixアプリを立ち上げるまで
- Elixir+Phoenix - running a dev setup inside docker
- Phoenix 1.3のディレクトリ構造とContext
- ElixirでDialyzerを用いた静的解析を行い、型(Typespec)を最大限に活用する
- 【感謝記念】Phoenix 1.4でElixirしか書いてないが React的SPA/リアルタイムUIが作れる「LiveView」(APIとJavaScriptは書いていない)
PR
XではUZUMAKIの新しい働き方や日常の様子を紹介!ぜひフォローをお願いします!
noteではUZUMAKIのメンバー・クライアントインタビュー、福利厚生を紹介!
UZUMAKIではRailsエンジニアを絶賛募集中です。
↓の記事を読んでご興味を持っていただいた方は、ぜひ応募よろしくお願いします!
是非応募宜しくおねがいします!