prmd を利用して JSON Schema を簡単に管理しよう

## prmd is 何？

[prmd](https://github.com/interagent/prmd) とは、Heroku が提供している OSS で、JSON Schema を簡単に取り扱えるようにする gem です。prmd を利用することで、API のリソース単位で JSON Schema ファイルを管理しつつ、あとで結合して一つの JSON Schema ファイルにしたり、JSON Schema ファイルに間違いがないかどうかを検証したり、JSON Schema から Markdown 形式の仕様書を自動生成したり、といったことが簡単に出来るようになります。

## まずは、インストールしてみよう！

Rails で利用することを勝手に想定しているので、`Gemfile` に追記して `bundle install` するだけで入ります。

```ruby:Gemfile
gem 'prmd', github: 'interagent/prmd'
```

```bash:console
$ bundle install
```

## JSON Schema ってどこにどうやって置いて管理したらいいの？

prmd の README にある [File Layout](https://github.com/interagent/prmd#file-layout) の項で、JSON Schema のオススメのファイル構成について言及されていたので、積極的に真似していくスタイルがヨサソウです。

```
/docs                         # top-level directory for project documentation
  /schema                     # API schema documentation
    /schemata
      /{resource.[json,yml]}  # individual resource schema
    /meta.[json,yml]          # overall API metadata
    /overview.md              # preamble for generated API docs
    /schema.json              # complete generated JSON schema file
    /schema.md                # complete generated API documentation file
```

それぞれのファイルについて、簡単に解説しておきます。

- /docs/schema/schemata/{resource.[json,yml]}
    - API のリソース単位で用意する JSON Schema ファイル
    - prmd を利用することで JSON 形式だけでなく YAML 形式で書くことも出来る
- /docs/schema/meta.[json,yml]
    - API の全体に関わるメタデータを記述するファイル
    - prmd を利用することで JSON 形式だけでなく YAML 形式で書くことも出来る
- /docs/schema/overview.md
    - 仕様書のイントロダクションを記述する Markdown ファイル
    - 仕様書を自動生成するときにこのファイルを指定することによって、仕様書の先頭に結合することが出来る
- /docs/schema/schema.json
    - API の全てのリソースの JSON Schema が結合された全部入りの JSON Schema ファイル
    - 仕様書を自動生成するときの元になる
    - 別途、[committee](https://github.com/interagent/committee) などの gem を入れることで、リクエストやレスポンスのバリデーションにも利用される
- /docs/schema/schema.md
    - prmd を利用して JSON Schema から自動生成した Markdown 形式の仕様書

## JSON Schema の雛形を自動生成しよう

JSON Schema をゼロから書いていくのは、とても心が折れる作業なので、スッと雛形を自動生成してから変更していくスタイルがヨサソウですね。

```bash:console
# JSON 形式で自動生成する
$ prmd init product > docs/schema/schemata/product.json

# YAML 形式で自動生成する
$ prmd init --yaml product > docs/schema/schemata/product.yml
```

ちなみに、既に、リソースが存在しているかどうかは関係無く、予め、prmd で用意されている雛形から自動生成されるようになっていて、以下のような、JSON Schema の雛形が自動生成されます。

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

API のリソース単位で JSON Schema を用意したとしても、JSON Schema を利用してリクエストやレスポンスのバリデーションを行ったり、仕様書を自動生成するためには、最終的に、一つの JSON Schema に結合してあげる必要があります。

### 指定ディレクトリ配下にある JSON Schema を結合する

```bash:console
$ prmd combine docs/schema/schemata/ > docs/schema/schema.json
```

### 指定ディレクトリ配下にある JSON Schema と、指定した メタファイルを結合する

```bash:console
# メタファイルを JSON 形式で指定する場合
$ prmd combine --meta docs/schema/meta.json docs/schema/schemata/ > docs/schema/schema.json

# メタファイルを YAML 形式で指定する場合
$ prmd combine --meta docs/schema/meta.yml docs/schema/schemata/ > docs/schema/schema.json
```

例えば、以下のようなメタファイルがあったとします。

```yml:docs/schema/meta.yml
---
"$schema": http://json-schema.org/draft-04/hyper-schema
title: Sample API JSON Schema
description: In this schema file, we represents the public interface of Sample API in JSON Hyper Schema draft v4.
id: sample
links:
- href: https://example.com
  rel: self
```

先ほど、雛形として自動生成した JSON Schema と、上記のメタファイルを結合すると、以下のような JSON Schema が出来上がります。

```json:docs/schema/schema.json
{
  "$schema": "http://json-schema.org/draft-04/hyper-schema",
  "type": [
    "object"
  ],
  "definitions": {
    "product": {
      "$schema": "http://json-schema.org/draft-04/hyper-schema",
      "title": "FIXME - Product",
      "description": "FIXME",
      "stability": "prototype",
      "strictProperties": true,
      "type": [
        "object"
      ],
      "definitions": {
        "id": {
          "description": "unique identifier of product",
          "readOnly": true,
          "format": "uuid",
          "type": [
            "string"
          ]
        },
        "name": {
          "description": "unique name of product",
          "readOnly": true,
          "type": [
            "string"
          ]
        },
        "identity": {
          "anyOf": [
            {
              "$ref": "#/definitions/product/definitions/id"
            },
            {
              "$ref": "#/definitions/product/definitions/name"
            }
          ]
        },
        "created_at": {
          "description": "when product was created",
          "format": "date-time",
          "type": [
            "string"
          ]
        },
        "updated_at": {
          "description": "when product was updated",
          "format": "date-time",
          "type": [
            "string"
          ]
        }
      },
      "links": [
        {
          "description": "Create a new product.",
          "href": "/products",
          "method": "POST",
          "rel": "create",
          "schema": {
            "properties": {
            },
            "type": [
              "object"
            ]
          },
          "title": "Create"
        },
        {
          "description": "Delete an existing product.",
          "href": "/products/{(%23%2Fdefinitions%2Fproduct%2Fdefinitions%2Fidentity)}",
          "method": "DELETE",
          "rel": "destroy",
          "title": "Delete"
        },
        {
          "description": "Info for existing product.",
          "href": "/products/{(%23%2Fdefinitions%2Fproduct%2Fdefinitions%2Fidentity)}",
          "method": "GET",
          "rel": "self",
          "title": "Info"
        },
        {
          "description": "List existing products.",
          "href": "/products",
          "method": "GET",
          "rel": "instances",
          "title": "List"
        },
        {
          "description": "Update an existing product.",
          "href": "/products/{(%23%2Fdefinitions%2Fproduct%2Fdefinitions%2Fidentity)}",
          "method": "PATCH",
          "rel": "update",
          "schema": {
            "properties": {
            },
            "type": [
              "object"
            ]
          },
          "title": "Update"
        }
      ],
      "properties": {
        "created_at": {
          "$ref": "#/definitions/product/definitions/created_at"
        },
        "id": {
          "$ref": "#/definitions/product/definitions/id"
        },
        "name": {
          "$ref": "#/definitions/product/definitions/name"
        },
        "updated_at": {
          "$ref": "#/definitions/product/definitions/updated_at"
        }
      }
    }
  },
  "properties": {
    "product": {
      "$ref": "#/definitions/product"
    }
  },
  "title": "Sample API JSON Schema",
  "description": "In this schema file, we represents the public interface of Sample API in JSON Hyper Schema draft v4.",
  "id": "sample",
  "links": [
    {
      "href": "https://example.com",
      "rel": "self"
    }
  ]
}
```

メタファイルも一緒に結合されていますね。

## JSON Schema に間違いがないか検証したい

頑張って書き上げた JSON Schema に間違いがないかどうか、検証したいときに使いましょう。

```bash:console
$ prmd verify docs/schema/schema.json
```

例えば、プロパティで参照している定義が見つからない場合は、以下のようなエラーが返って来ます。

```bash:console
schema.json: #: Couldn't resolve pointer "#/definitions/product/definitions/name".
schema.json: #: Couldn't resolve references: #/definitions/product/definitions/name, #/definitions/product/definitions/name.
```
## JSON Schema から仕様書を自動生成したい

JSON Schema さえあれば、Markdown 形式の仕様書を簡単に自動生成することが出来ます。

### 指定した JSON Schema から仕様書を自動生成する

```bash:console
$ prmd doc docs/schema/schema.json > docs/schema/schema.md
```

### 指定した JSON Schema の先頭に、指定した Markdown ファイルを結合して仕様書を自動生成する

API に関する概要などを仕様書の先頭に書きたい場合に使います。

```bash:console
$ prmd doc --prepend docs/schema/overview.md docs/schema/schema.json > docs/schema/schema.md
```

例えば、以下のような Markdown ファイルがあったとします。

```markdown:docs/schema/overview.md
# Sample API JSON Schema

In this schema file, we represents the public interface of Sample API in JSON Hyper Schema draft v4.
```

先ほど、結合した JSON Schema と、上記の Markdown ファイルを指定して仕様書を自動生成すると、以下のような Markdown 形式の仕様書が出来上がります。

<pre>
# Sample API JSON Schema

In this schema file, we represents the public interface of Sample API in JSON Hyper Schema draft v4.

## <a name="resource-product">Product</a>

Stability: `prototype`

FIXME

### Attributes

| Name | Type | Description | Example |
| ------- | ------- | ------- | ------- |
| **created_at** | *date-time* | when product was created | `"2015-01-01T12:00:00Z"` |
| **id** | *uuid* | unique identifier of product | `"01234567-89ab-cdef-0123-456789abcdef"` |
| **name** | *string* | unique name of product | `"example"` |
| **updated_at** | *date-time* | when product was updated | `"2015-01-01T12:00:00Z"` |

### Product Create

Create a new product.

```
POST /products
```


#### Curl Example

```bash
$ curl -n -X POST https://example.com/products \
  -d '{
}' \
  -H "Content-Type: application/json"
```


#### Response Example

```
HTTP/1.1 201 Created
```

```json
{
  "created_at": "2015-01-01T12:00:00Z",
  "id": "01234567-89ab-cdef-0123-456789abcdef",
  "name": "example",
  "updated_at": "2015-01-01T12:00:00Z"
}
```

### Product Delete

Delete an existing product.

```
DELETE /products/{product_id_or_name}
```


#### Curl Example

```bash
$ curl -n -X DELETE https://example.com/products/$PRODUCT_ID_OR_NAME \
  -H "Content-Type: application/json"
```


#### Response Example

```
HTTP/1.1 200 OK
```

```json
{
  "created_at": "2015-01-01T12:00:00Z",
  "id": "01234567-89ab-cdef-0123-456789abcdef",
  "name": "example",
  "updated_at": "2015-01-01T12:00:00Z"
}
```

### Product Info

Info for existing product.

```
GET /products/{product_id_or_name}
```


#### Curl Example

```bash
$ curl -n https://example.com/products/$PRODUCT_ID_OR_NAME
```


#### Response Example

```
HTTP/1.1 200 OK
```

```json
{
  "created_at": "2015-01-01T12:00:00Z",
  "id": "01234567-89ab-cdef-0123-456789abcdef",
  "name": "example",
  "updated_at": "2015-01-01T12:00:00Z"
}
```

### Product List

List existing products.

```
GET /products
```


#### Curl Example

```bash
$ curl -n https://example.com/products
```


#### Response Example

```
HTTP/1.1 200 OK
```

```json
[
  {
    "created_at": "2015-01-01T12:00:00Z",
    "id": "01234567-89ab-cdef-0123-456789abcdef",
    "name": "example",
    "updated_at": "2015-01-01T12:00:00Z"
  }
]
```

### Product Update

Update an existing product.

```
PATCH /products/{product_id_or_name}
```


#### Curl Example

```bash
$ curl -n -X PATCH https://example.com/products/$PRODUCT_ID_OR_NAME \
  -d '{
}' \
  -H "Content-Type: application/json"
```


#### Response Example

```
HTTP/1.1 200 OK
```

```json
{
  "created_at": "2015-01-01T12:00:00Z",
  "id": "01234567-89ab-cdef-0123-456789abcdef",
  "name": "example",
  "updated_at": "2015-01-01T12:00:00Z"
}
```
</pre>

仕様書の先頭に、指定した Markdown ファイルが結合されていますね。

仕様書としては、それぞれのリソースに対して、タイトル、説明、エンドポイント、コピペして直接叩ける Curl のサンプルとレスポンスのサンプルが記載されます。

## Rake タスクを用意して手抜きしよう

毎回、細かいファイルやオプションを指定するのも面倒なので、README の [Use as rake task](https://github.com/interagent/prmd#use-as-rake-task) の項にも記載されているように、Rake タスクを用意して手抜きするとヨサソウです。

```ruby:lib/tasks/schema.rake
require 'prmd/rake_tasks/combine'
require 'prmd/rake_tasks/verify'
require 'prmd/rake_tasks/doc'

schema_root = "docs/schema"

namespace :schema do
  Prmd::RakeTasks::Combine.new do |t|
    t.options[:meta] = "#{schema_root}/meta.yml"
    t.paths << "#{schema_root}/schemata"
    t.output_file = "#{schema_root}/schema.json"
  end

  Prmd::RakeTasks::Verify.new do |t|
    t.files << "#{schema_root}/schema.json"
  end

  Prmd::RakeTasks::Doc.new do |t|
    t.options[:prepend] = ["#{schema_root}/overview.md"]
    t.files = { "#{schema_root}/schema.json" => "#{schema_root}/schema.md" }
  end
end

desc 'Combine & Verify schema & Generate documentation'
task schema: ['schema:combine', 'schema:verify', 'schema:doc']
```

```bash:console
# 結合したいとき
$ rake schema:combine

# 検証したいとき
$ rake schema:verify

# 仕様書を自動生成したいとき
$ rake schema:doc

# 上記全てを実行したいとき
$ rake schema
```

## リンク

- [GitHub](https://github.com/interagent/prmd)
- [RubyGems.org](https://rubygems.org/gems/prmd)
- [The Ruby Toolbox](https://www.ruby-toolbox.com/projects/prmd)

## 合わせて読みたい

- [HerokuのつくってるAPI関係の便利なやつ - ✘╹◡╹✘](http://r7kamura.hatenablog.com/entry/2014/06/03/105041)
- [JSON Schemaを上手く運用出来そうなprmdとその周りのお話 - Qiita](http://qiita.com/taizo/items/2528c16a2a48deb1c2e5)
- [JSON Hyper-Schema からAPIドキュメントとGoのコードを自動生成する | The Wacul Blog](http://blog.wacul.co.jp/blog/2014/10/28/go-rest-api/)
- [HerokuのAPIデザイン | SOTA](http://deeeet.com/writing/2014/06/02/heroku-api-design/)
- [JSON SchemaでAPI開発を自動化する — Commerce Hack](http://tech.degica.com/ja/2015/10/16/json-schema-ja/)
- [Safx: JSONスキーマからGo言語コードを作成して利用してみる](http://safx-dev.blogspot.jp/2014/11/jsongo.html)
- [現実世界のJSON Hyper-Schema / Real World JSON Hyper-Schema - SSSSLIDE](http://sssslide.com/speakerdeck.com/kenchan/real-world-json-hyper-schema)
- [Qiita API v2のJSON Schemaを公開しました - Qiita Blog](http://blog.qiita.com/post/120746062029/json-schema-for-qiita-api-v2)