Rubyの prmd gem でYAMLのスキーマバリデーションをする

## <i class='fa fa-question-circle'></i> Prmd
Prmd は JSON Schema の scaffold, 正当性チェック, ドキュメント生成を行うツールです。

[interagent/prmd - GitHub](https://github.com/interagent/prmd)

## <i class='fa fa-download'></i> インストール
```sh
$ gem install prmd
```

## <i class='fa fa-flask'></i> サンプル
名前と年齢を保持する Person を定義してバリデーションしてみます。

### <i class='fa fa-wrench'></i> init
テンプレートを生成します

```sh
$ prmd init --yaml person -o person.yml
```

* 生成されたファイル

```yaml:person.yml
---
"$schema": http://json-schema.org/draft-04/hyper-schema
title: FIXME - Person
description: FIXME
stability: prototype
strictProperties: true
type:
- object
definitions:
  id:
    description: unique identifier of person
    readOnly: true
    format: uuid
    type:
    - string
  name:
    description: unique name of person
    readOnly: true
    type:
    - string
  identity:
    anyOf:
    - "$ref": "/schemata/person#/definitions/id"
    - "$ref": "/schemata/person#/definitions/name"
  created_at:
    description: when person was created
    format: date-time
    type:
    - string
  updated_at:
    description: when person was updated
    format: date-time
    type:
    - string
links:
- description: Create a new person.
  href: "/persons"
  method: POST
  rel: create
  schema:
    properties: {}
    type:
    - object
  title: Create
- description: Delete an existing person.
  href: "/persons/{(%2Fschemata%2Fperson%23%2Fdefinitions%2Fidentity)}"
  method: DELETE
  rel: destroy
  title: Delete
- description: Info for existing person.
  href: "/persons/{(%2Fschemata%2Fperson%23%2Fdefinitions%2Fidentity)}"
  method: GET
  rel: self
  title: Info
- description: List existing persons.
  href: "/persons"
  method: GET
  rel: instances
  title: List
- description: Update an existing person.
  href: "/persons/{(%2Fschemata%2Fperson%23%2Fdefinitions%2Fidentity)}"
  method: PATCH
  rel: update
  schema:
    properties: {}
    type:
    - object
  title: Update
properties:
  created_at:
    "$ref": "/schemata/person#/definitions/created_at"
  id:
    "$ref": "/schemata/person#/definitions/id"
  name:
    "$ref": "/schemata/person#/definitions/name"
  updated_at:
    "$ref": "/schemata/person#/definitions/updated_at"
id: schemata/person
```

## <i class='fa fa-edit'></i> 生成したテンプレートを編集
```yaml:person.yml
---
id: schemata/person
"$schema": http://json-schema.org/draft-04/hyper-schema
title: Person
description: 人
stability: prototype
strictProperties: true
type:
- object
definitions:
  name:
    description: name of person
    type:
    - string
  age:
    description: age of person
    pattern: ^[0-9]{1,3}$
    type:
    - string
    - "null"
  identity:
    "$ref": "/schemata/person#/definitions/name"
required: ["name"]
properties:
  name:
    "$ref": "/schemata/person#/definitions/name"
  age:
    "$ref": "/schemata/person#/definitions/age"
```

### <i class='fa fa-file-text'></i> METAファイルを作成
```yaml:meta.yml
---
"$schema": http://json-schema.org/draft-04/hyper-schema
title: Sample API JSON Schema
description: sample schema
id: sample
links:
- href: https://example.com
  rel: self
```

## <i class='fa fa-cubes'></i> 結合する
YAMLを結合します

```sh
$ prmd combine --meta meta.yml person.yml > schema.json
```

* 出力結果を確認

```json:schema.json
{
  "$schema": "http://json-schema.org/draft-04/hyper-schema",
  "type": [
    "object"
  ],
  "definitions": {
    "person": {
      "$schema": "http://json-schema.org/draft-04/hyper-schema",
      "title": "Person",
      "description": "人",
      "stability": "prototype",
      "strictProperties": true,
      "type": [
        "object"
      ],
      "definitions": {
        "name": {
          "description": "name of person",
          "type": [
            "string"
          ]
        },
        "age": {
          "description": "age of person",
          "pattern": "^[0-9]{1,3}$",
          "type": [
            "string",
            "null"
          ]
        },
        "identity": {
          "$ref": "#/definitions/person/definitions/name"
        }
      },
      "required": [
        "name"
      ],
      "properties": {
        "name": {
          "$ref": "#/definitions/person/definitions/name"
        },
        "age": {
          "$ref": "#/definitions/person/definitions/age"
        }
      }
    }
  },
  "properties": {
    "person": {
      "$ref": "#/definitions/person"
    }
  },
  "title": "Sample API JSON Schema",
  "description": "sample schema",
  "id": "sample",
  "links": [
    {
      "href": "https://example.com",
      "rel": "self"
    }
  ]
}
```

### <i class='fa fa-file-text'></i> ドキュメントを生成
```sh
$ prmd doc schema.json > schema.md
```

* 出力結果を確認

```markdown:schema.md
## <a name="resource-person">Person</a>

Stability: `prototype`

人

### Attributes

| Name | Type | Description | Example |
| ------- | ------- | ------- | ------- |
| **age** | *string* | age of person<br/> **pattern:** `^[0-9]{1,3}$` | `"example"` |
| **name** | *string* | name of person | `"example"` |
```

### <i class='fa fa-check-circle'></i> JSON Schema としての正当性をチェックする
```sh
$ prmd verify schema.json
```

わざとエラーを発生させてみます。

```yaml
age:
  description: age of person
  pattern: ^\[0-9]{1,3}$
  type:
  # number を不正な値 invalid に変更する
  - invalid
```

```sh
$ prmd verify schema.json
schema.json: #/definitions/person/definitions/age/type: failed schema #/properties/type: No subschema in "anyOf" matched.
schema.json: #: failed schema #: Not all subschemas of "allOf" matched.
schema.json: #/definitions/person/definitions/age/type: failed schema #/properties/type: No subschema in "anyOf" matched.
schema.json: #/definitions/person: failed schema #/properties/definitions/additionalProperties: Not all subschemas of "allOf" matched.
schema.json: #/definitions/person/definitions/age/type: failed schema #/properties/type: No subschema in "anyOf" matched.
schema.json: #/definitions/person/definitions/age: failed schema #/properties/definitions/additionalProperties: Not all subschemas of "allOf" matched.
```

### <i class='fa fa-check-circle'></i> json-schema gem でバリデーションを実行する
* テストデータ1 : 正常系

```yaml:person1.yml
person:
  name: tanaka1
  age: "34"
```

* テストデータ2 : ageのパターンエラー

```yaml:person2.yml
person:
  name: tanaka
  age: "1000"
```

* テストデータ3 : name の null エラー

```yaml:person3.yml
person:
  name:
  age: "20"
```

* テストデータ4 : 正常系（age は null 許容）

```yaml:person4.yml
person:
  name: suzuki
  age:
```

* テストデータ5 : name の必須エラー

```yaml:person5.yml
person:
  age:
```

* テストコード

```ruby:test.rb
require "json-schema"
require 'pp'
require 'yaml'
require 'json'

schema = JSON.parse(File.read('schema.json'))
jsons = (1..5).map{|e|YAML.load_file("person#{e}.yml").to_json}
jsons.each do |json|
  begin
    pp json
    pp JSON::Validator.validate!(schema, json)
  rescue JSON::Schema::ValidationError => e
    pp e.message
  end
end
```

* 実行結果

```sh
$ ruby test.rb
"{\"person\":{\"name\":\"tanaka1\",\"age\":\"34\"}}"
true
"{\"person\":{\"name\":\"tanaka\",\"age\":\"1000\"}}"
"The property '#/person/age' value \"1000\" did not match the regex '^[0-9]{1,3}$'"
"{\"person\":{\"name\":null,\"age\":\"20\"}}"
"The property '#/person/name' of type NilClass did not match one or more of the following types: string"
"{\"person\":{\"name\":\"suzuki\",\"age\":null}}"
true
"{\"person\":{\"age\":null}}"
"The property '#/person' did not contain a required property of 'name'"
```

## <i class="fa fa-book" style="font-size:1em;"></i> 外部資料
* [interagent/prmd - GitHub](https://github.com/interagent/prmd)
* [prmd を利用して JSON Schema を簡単に管理しよう - fakestarbaby - Qiita](http://qiita.com/fakestarbaby/items/90144d8bc3dead46e65c)