README.md

# SimpleSchema

SimpleSchema は JSON の検証と各データ構造への設定を行うライブラリです。

- [hex.pm](https://hex.pm/packages/simple_schema)

## 動機

HTTP の API サーバを書いていると、よく HTTP の POST リクエストで JSON を受け取ることがありますが、この値が正しいフォーマットかどうかを検証したいことがあります。
検証だけなら、[JSON Schema](http://json-schema.org/) を使うという手があり、幸いなことに Elixir には JSON Schema を実装した [ExJsonSchema](https://github.com/jonasschmidt/ex_json_schema) というライブラリがあります。

しかし、JSON Schema は手で書くのが大変です。もう少し機能を絞った単純なスキーマを使いたいところです。

また、JSON Schema は検証しか行わないため、データにアクセスするのに手間が掛かります。

```elixir
json = Poison.decode!(conn.body_param)
:ok = validate(json)

hp = json["player"]["hp"]
# → json.player.hp と書きたい

datetime = json["datetime"]                       # 文字列を取り出して
{:ok, datetime, _} = DateTime.from_iso8601(value) # DateTime 型に変換する
# → json.datetime した段階で DateTime 型であって欲しい
```

このように、特に `DateTime` に変換するといった処理が必要な場合、検証と同時に変換まで済ませたいのです。

そこで、簡単にスキーマを書けるようして、検証を行い、それらのデータを変換するライブラリとして、SimpleSchema というライブラリを作りました。

## 使い方

以下のように使います。

```elixir
# defschema/1 を使ってスキーマを定義する
defmodule Person do
  import SimpleSchema, only: [defschema: 1]

  defschema [
    name: :string,
    age: {:integer, minimum: 0},
  ]
end

# JSON 文字列をデコードしたデータを…
json = %{
  "name" => "John Smith",
  "age" => 42,
}

# Person と一緒に from_json!/2 すると、Person 構造体に値が設定される
person = SimpleSchema.from_json!(Person, json)

assert person.name == "John Smith"
assert person.age == 42
```

このように、`defschema/1` でスキーマを定義して、`SimpleSchema.from_json!/2` にそのスキーマと JSON オブジェクトを渡すと、JSON オブジェクトを検証し、指定したスキーマにデータを入れてくれます。

JSON オブジェクトが `Person` スキーマを満たしていない場合、以下のようにエラーがでます。

```elixir
bad_json = %{
  "name" => 100, # 文字列ではない
  "age" => -10, # 無効な年齢
  "__additional_key__" => 0, # 余分なキー
}

# from_json/2 は失敗する
{:error, reason} = SimpleSchema.from_json(Person, bad_json)
IO.inspect reason
```

出力:

```
[{"Expected the value to be >= 0", "#/age"},
 {"Type mismatch. Expected String but got Integer.", "#/name"},
 {"Schema does not allow additional properties.", "#/__additional_key__"}]
```

この `Person` を内包するスキーマを定義することもできます。
つまりスキーマはネスト可能です。

```elixir
# Person を内包する Group
defmodule Group do
  import SimpleSchema, only: [defschema: 1]

  defschema [
    group_name: :string,
    persons: [Person],
  ]
end

json = %{
  "group_name" => "A Group",
  "persons" => [%{
    "name" => "John Smith",
    "age" => 42,
  }, %{
    "name" => "YAMADA Taro",
    "age" => 20,
  }],
}

group = SimpleSchema.from_json!(Group, json)

assert group.group_name == "A Group"
assert Enum.fetch!(group.persons, 1).age == 20
```

`Group` スキーマを `defschema/1` で定義していますが、`:group_name` が文字列であり、`:persons` が `Person` の配列であることが、見ればすぐに分かるでしょう。

これによって、共通するスキーマに名前を付けて再利用することができます。

## シンプルスキーマ

もう少し詳細に SimpleSchema の機能を説明します。

SimpleSchema ライブラリが定義している、`SimpleSchema.from_json/2` の第1引数に渡せるスキーマのことを **シンプルスキーマ** と呼びます。
JSON Schema と比べると大分単純で直感的な構文になっているので「シンプル」と名付けています。

例えば、`:integer` はシンプルスキーマです。

```elixir
value = SimpleSchema.from_json!(:integer, 10)
assert value == 10
```

`:integer` シンプルスキーマは、渡された値が整数であるかを確認し、整数であればその値を戻り値にします。
整数に制約を付け加えることも可能です。

```elixir
value = SimpleSchema.from_json!({:integer, minimum: 10, maximum: 20}, 5)
# RuntimeError: [{"Expected the value to be >= 10", "#"}]
```

`{:integer, opts}` という書き方もシンプルスキーマになります。
これは渡された値が整数であり、かつ10から20の範囲内であるかを確認し、正しければその値を戻り値にします。

`%{}` という書き方もシンプルスキーマであり、各フィールドには、更にシンプルスキーマを渡すことができます。

```elixir
schema = %{
  value: {:integer, optional: true},
  point: %{
    x: :integer,
    y: :integer,
  },
}
data = %{
  "point" => %{
    "x" => 10,
    "y" => 20,
  }
}
value = SimpleSchema.from_json!(schema, data)
# value == %{point: %{x: 10, y: 20}}
assert value.point.x == 10
assert value.point.y == 20
```

このシンプルスキーマは、渡された値がマップであるかを確認し、渡された値の各フィールドが、指定したシンプルスキーマのフィールドと合っているかどうか確認します。
正しければ、渡されたマップのキーを atom にした上で戻り値にします。

また、`:value` フィールドに `optional: true` という制約を付与しました。
これはマップのフィールドに渡すシンプルスキーマのみに指定可能で「このフィールドが無くてもエラーにしない」という意味になります。
そのため `data` に `"value"` キーが存在していなくても `SimpleSchema.from_json!/2` が成功しています。

### シンプルスキーマの一覧

シンプルスキーマは、以下のいずれかである必要があります。

- `:boolean` または `{:boolean, opts}`
- `:integer` または `{:integer, opts}`
- `:number` または `{:number, opts}`
- `:null` または `{:null, opts}`
- `:string` または `{:string, opts}`
- `:any` または `{:any, opts}`
- `%{...}` または `{%{...}, opts}`
- `[...]` または `{[...], opts}`
- `SimpleSchema` ビヘイビアを実装したモジュール、または `{Module, opts}`

`opts` には各制約をキーワードリストで指定します。

### 制約の一覧

制約の一覧は以下の通りです。

- `{:nullable, boolean}`: もし `true` なら `nil` を許可する。`:null` 以外のシンプルスキーマに指定可能。
- `{:minimum, integer}`: 最小値。`:integer` と `:number` に指定可能。
- `{:maximum, integer}`: 最大値。`:integer` と `:number` に指定可能。
- `{:min_items, non_neg_integer}`: 最小の要素数。`:array` に指定可能。
- `{:max_items, non_neg_integer}`: 最大の要素数。`:array` に指定可能。
- `{:unique_items, boolean}`: もし `true` なら配列がユニークであることを要求される。`:array` に指定可能。
- `{:min_length, non_neg_integer}`: 最小の長さ。`:string` に指定可能。
- `{:max_length, non_neg_integer}`: 最大の長さ。`:string` に指定可能。
- `{:enum, [...]}`: 要素に指定可能な値のリスト。`:integer` と `:string` に指定可能。
- `{:format, :datetime | :email}`: 事前に定義されたフォーマットで検証する。`:string` に指定可能。
- `{:optional, boolean}`: もし `true` なら、`%{...}` の子要素として必須では無い。`%{...}` の子要素のみ指定可能。
- `{:tolerant, boolean}`: もし `true` なら、生成される JSON Schema に `"additionalProperties"` が設定される。つまり子要素に指定されてない要素を許可するようになる。`%{...}` に指定可能。デフォルトは `false`。
- `{:default, any}`: フィールドのデフォルト値。渡された JSON にこのフィールドが存在しなかった場合はこの値になる。`%{...}` の子要素のみ指定可能。
- `{:field, string}`: 対応する JSON のフィールド名。`%{...}` の子要素のみ指定可能。

## `SimpleSchema` ビヘイビア

`SimpleSchema` ビヘイビアを実装したモジュールは、シンプルスキーマになります。
これを使うことで、特定のスキーマに名前を付けたり、特定の構造体に変換できるようになります。

例えば、`"2017-11-27T11:49:50+09:00"` といった ISO 8601 に従った日付を `DateTime` 型として取得するには、以下のように定義します。

```elixir
defmodule DateTimeSchema do
  @behaviour SimpleSchema

  @impl SimpleSchema
  def schema(_opts) do
    {:string, format: :datetime}
  end

  @impl SimpleSchema
  def from_json(_schema, value, _opts) do
    case DateTime.from_iso8601(value) do
      {:ok, datetime, _} -> {:ok, datetime}
      {:error, reason} -> {:error, reason}
    end
  end

  @impl SimpleSchema
  def to_json(_schema, value, _opts) do
    {:ok, DateTime.to_iso8601(value)}
  end
end
```

`DateTimeSchema` は `SimpleSchema` ビヘイビアを実装しているためシンプルスキーマになります。
そのため、以下のように `SimpleSchema.from_json!/2` に渡すことができます。

```elixir
datetime = SimpleSchema.from_json!(DateTimeSchema, "2017-11-27T11:49:50+09:00")
# datetime == #DateTime<2017-11-27 02:49:50Z>
```

このように `SimpleSchema` ビヘイビアを実装することで、特定のスキーマに `DateTimeSchema` というを付け、`DateTime` 型の構造体に変換して利用できるようになります。
なお、上記の `DateTimeSchema` に相当する機能は既に `SimpleSchema.Type.DateTime` に入っています。

`SimpleSchema` ビヘイビアが要求する関数は、以下の通りです。

```elixir
@callback schema(opts :: Keyword.t) :: simple_schema
@callback from_json(schema :: simple_schema, json :: any, opts :: Keyword.t) :: {:ok, any} | {:error, any}
@callback to_json(schema :: simple_schema, value :: any, opts :: Keyword.t) :: {:ok, any} | {:error, any}
```

`schema/1` で、そのモジュールが要求するシンプルスキーマを定義します。

`from_json/3` で、`value` を任意の型に変換して返します。
`value` は `schema/1` で返したシンプルスキーマによる検証が済んでいて、例えば上記の `DateTimeSchema.from_json/3` に渡された `value` は、`{:string, format: :datetime}` で検証されています。
そのため `value` が文字列であり、`:datetime` のフォーマットであることが保証されています。

ただし、`SimpleSchema.from_json/2` に `optimistic: true` が指定されていた場合、検証を行いません。この場合、正しい値を渡す責任はユーザにあります。

`to_json/3` で、変換された値をシンプルスキーマの満たす文字列に変換します。
`from_json/3` と逆の変換を行います。
この関数は `SimpleSchema.to_json/2` の内部で利用される関数なので、不要であれば常に `{:error, "not implemented"}` でも構いません。

## `defschema/1`

`defschema/1` は、`defstruct/1` による構造体の定義と、`SimpleSchema` ビヘイビアの実装を行います。

```elixir
defmodule Person do
  import SimpleSchema, only: [defschema: 1]

  defschema [
    name: :string,
    age: {:integer, minimum: 0},
  ]
end
```

このコードは、以下の様に変換されます。

```elixir
defmodule Person do
  @enforce_keys [:name, :age]
  defstruct [:name, :age]

  @behaviour SimpleSchema

  @impl SimpleSchema
  def schema(_opts) do
    %{
      name: :string,
      age: {:integer, minimum: 0},
    }
  end

  @impl SimpleSchema
  def from_json(schema, value, _opts) do
    SimpleSchema.Type.json_to_struct(__MODULE__, schema, value)
  end

  @impl SimpleSchema
  def to_json(schema, value, _opts) do
    SimpleSchema.Type.struct_to_json(__MODULE__, schema, value)
  end
end
```

`schema/1` で、`:name` と `:age` を持つマップとしてシンプルスキーマを定義しています。
渡された JSON のオブジェクトがこのシンプルスキーマの構造になっているかを検証した後、`Person.from_json/3` を呼び出して、`value` を `Person` 構造体に変換しています。
JSON のオブジェクトを特定の構造体に変換するためのヘルパーとして `SimpleSchema.Type.json_to_struct/3` があるので、これを使うと簡単に変換できます。