Elixirで値の定義やバリデーションをするためにライブラリを探した.
awesome-elixir の validations を探すといくつか見つかる.
hex.pm で validation 検索してもいくつか見つかる.
これらの中では vex というのが汎用的かつ使いやすそうだった.
ここには含まれていないが,それを行えるライブラリがある.ectoだ.
ectoは永続化層(主にDB)への入出力モジュールとして Ecto.Repo と Ecto.Query を持っており DB アクセスするためのミドルウェアとして捉えられがちではあるが,それ以外にも,スキーマ定義のための Ecto.Schema とバリデーション(など)のための Ecto.Changeset があるので,単なるバリデーションのためのライブラリとして利用することもできる.
もしectoが使えるなら
- 広く使われているのでバグが少ないことが期待できる
- (使ったことがあれば)新しいAPIを覚えずにすむ
そこで DB を使わない Ecto の利用について調査する.
コードは https://gist.github.com/niku/42edf138d299b7239d872705079b3dd2 に置いたので clone してすぐに試せる.
準備
依存関係に {:ecto, "~> 2.1"}
を追加するだけで OK だ.
DB を利用するときには,config に adapter を定義していたが,今回は不要だ.
defmodule EctoSandbox.Mixfile do
use Mix.Project
def project do
[app: :ecto_sandbox,
version: "0.1.0",
elixir: "~> 1.4",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
elixirc_paths: ["."],
deps: deps()]
end
def application do
[extra_applications: [:logger]]
end
defp deps do
[{:ecto, "~> 2.1"}]
end
end
定義
Ecto.Schema を使う.
use Ecto.Schema
するとよい.
```elixir:user.ex`
defmodule User do
use Ecto.Schema
schema "users" do
field :name, :string
field :age, :integer, default: 0
end
end
```iex
% git clone https://gist.github.com/niku/42edf138d299b7239d872705079b3dd2
% cd 42edf138d299b7239d872705079b3dd2/
42edf138d299b7239d872705079b3dd2% mix deps.get && iex -S mix
iex(1)> %User{}
%User{__meta__: #Ecto.Schema.Metadata<:built, "users">, age: 0, id: nil,
name: nil}
iex(2)> user = %User{name: "niku", age: 29}
%User{__meta__: #Ecto.Schema.Metadata<:built, "users">, age: 29, id: nil,
name: "niku"}
iex(3)> user.name
"niku"
バリデーション
The first one (訳註: cast/3) is used to cast and validate external parameters, such as parameters sent through a form, API, command line, etc.
The second one (訳註: change/2) is used to change data directly from your application.
と書いてあるので Ecto.Changeset.change/2
を使うとよい.
Changeset を取得するにはモジュールへ changeset/2
という function を準備するのが慣例になっているようだ.
ここでは city
temp_lo
temp_hi
それぞれが必須,prcp
が 0.0 以上というバリデーションをかけた.
defmodule Weather do
use Ecto.Schema
schema "weather" do
field :city
field :temp_lo, :integer
field :temp_hi, :integer
field :prcp, :float, default: 0.0 # 降水量
end
def changeset(weather, params \\ %{}) do
weather
|> Ecto.Changeset.change(params)
|> Ecto.Changeset.validate_required([:city, :temp_lo, :temp_hi])
|> Ecto.Changeset.validate_number(:prcp, greater_than_or_equal_to: 0.0)
end
end
42edf138d299b7239d872705079b3dd2% iex -S mix
iex(1)> # validなデータを与える
iex(2)> weather = Weather.changeset(%Weather{}, %{city: "sapporo", temp_hi: -5, temp_lo: -10, prcp: 2.6})
#Ecto.Changeset<action: nil,
changes: %{city: "sapporo", prcp: 2.6, temp_hi: -5, temp_lo: -10}, errors: [],
data: #Weather<>, valid?: true>
iex(3)> weather.valid?
true
iex(4)> # invalidなデータを与える
iex(5)> weather = Weather.changeset(%Weather{}, %{temp_lo: -5, prcp: -1.0})
#Ecto.Changeset<action: nil, changes: %{prcp: -1.0, temp_lo: -5},
errors: [prcp: {"must be greater than or equal to %{number}",
[validation: :number, number: 0.0]},
city: {"can't be blank", [validation: :required]},
temp_hi: {"can't be blank", [validation: :required]}], data: #Weather<>,
valid?: false>
iex(6)> weather.valid?
false
Weacher.changeset
の結果に valid?
という値があり,バリデーションの結果により true
と false
が切り替わていることがわかるだろう.
エラーは traverse_errors/2 を使うと表示が容易になる.
iex(7)> Ecto.Changeset.traverse_errors(weather, fn {msg, opts} ->
...(7)> Enum.reduce(opts, msg, fn {key, value}, acc ->
...(7)> String.replace(acc, "%{#{key}}", to_string(value))
...(7)> end)
...(7)> end)
%{city: ["can't be blank"], prcp: ["must be greater than or equal to 0.0"],
temp_hi: ["can't be blank"]}
まとめ
- 既に広く使われていてテストが充分になされている
- RDB へのアクセスで使ったことがあるなら,新しく覚えることが少ない
という 2 点から,Elixir で扱うデータのバリデーションには Ecto を使うと便利である.