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

Elixirで値の定義やバリデーションをするためにライブラリを探した．
awesome-elixir の [validations](https://github.com/h4cc/awesome-elixir#validations) を探すといくつか見つかる．
hex.pm で [validation](https://hex.pm/packages?_utf8=%E2%9C%93&search=validation&sort=downloads) 検索してもいくつか見つかる．
これらの中では [vex](https://github.com/CargoSense/vex) というのが汎用的かつ使いやすそうだった．

ここには含まれていないが，それを行えるライブラリがある．[ecto](https://github.com/elixir-ecto/ecto)だ．

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

もしectoが使えるなら

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

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

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

## 準備

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

```elixir: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](https://hexdocs.pm/ecto/2.1.3/Ecto.Schema.html) を使う．

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

## バリデーション

[Ecto.Changeset](https://hexdocs.pm/ecto/2.1.3/Ecto.Changeset.html) に

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

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


```iex
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](https://hexdocs.pm/ecto/2.1.3/Ecto.Changeset.html#traverse_errors/2) を使うと表示が容易になる．

```iex
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 を使うと便利である．