Elixir
Validation
ecto

Elixirで値のバリデーションをEctoで行う

More than 1 year has passed since last update.

Elixirで値の定義やバリデーションをするためにライブラリを探した.
awesome-elixir の validations を探すといくつか見つかる.
hex.pm で validation 検索してもいくつか見つかる.
これらの中では vex というのが汎用的かつ使いやすそうだった.

ここには含まれていないが,それを行えるライブラリがある.ectoだ.

ectoは永続化層(主にDB)への入出力モジュールとして Ecto.RepoEcto.Query を持っており DB アクセスするためのミドルウェアとして捉えられがちではあるが,それ以外にも,スキーマ定義のための Ecto.Schema とバリデーション(など)のための Ecto.Changeset があるので,単なるバリデーションのためのライブラリとして利用することもできる.

もしectoが使えるなら

  • 広く使われているのでバグが少ないことが期待できる
  • (使ったことがあれば)新しいAPIを覚えずにすむ

そこで DB を使わない Ecto の利用について調査する.

コードは https://gist.github.com/niku/42edf138d299b7239d872705079b3dd2 に置いたので clone してすぐに試せる.

準備

依存関係に {:ecto, "~> 2.1"} を追加するだけで OK だ.
DB を利用するときには,config に adapter を定義していたが,今回は不要だ.

mix.exs
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 するとよい.

user.ex`
defmodule User do
  use Ecto.Schema

  schema "users" do
    field :name, :string
    field :age, :integer, default: 0
  end
end
% 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"

バリデーション

Ecto.Changeset

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 以上というバリデーションをかけた.

weather.ex
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? という値があり,バリデーションの結果により truefalse が切り替わていることがわかるだろう.

エラーは 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"]}

まとめ

  1. 既に広く使われていてテストが充分になされている
  2. RDB へのアクセスで使ったことがあるなら,新しく覚えることが少ない

という 2 点から,Elixir で扱うデータのバリデーションには Ecto を使うと便利である.