Swaggerで定義したAPIドキュメントとAPIレスポンスの差異をなくす

# はじめに
VASILYでは、API開発を始める前に、Swaggerを用いてAPIドキュメントを作成しています。
APIドキュメントを作成した後、実際のAPIレスポンスを修正したがドキュメントの更新を忘れ、ドキュメントの定義と実際のレスポンスの間に差異が生じてしまうということがありました。

そこで、今回はcommitteeというgemを用いて、Swaggerで定義したAPIドキュメントと実際のAPIレスポンスとの差異を検知する方法をご紹介します。

# [committee](https://github.com/interagent/committee)とは

[committee](https://github.com/interagent/committee)は、実際のAPIリクエストやレスポンスがスキーマ定義にそっているかをチェックすることができるgemです。
Rackのミドルウェアとして動作します。
バージョン2.0からはJSON Schemaだけでなく、OpenAPI2.0(Swagger)もサポートされるようになったため今回はこちらのgemを使用します。

# committeeのインストール

Gemfileに追記します。

```
gem 'committee'
```

# APIドキュメンテーションを用意する

## Swaggerを書く
SwaggerでAPIドキュメントを記述します。
Swaggerの導入方法、書き方については、下記の記事を参考にして下さい。

[開発効率を上げる！Swaggerで作るWEB APIモック](https://qiita.com/joooee0000/items/2d5935ec30ec73555eb2)

[Swaggerの記法まとめ](https://qiita.com/rllllho/items/53a0023b32f4c0f8eabb)

SwaggerはYAMLとJSONで書くことができます。
今回は、YAMLを採用し、公式ドキュメントでサンプルとして使われている[PetstoreAPI](http://petstore.swagger.io/)を簡略化したものをサンプルAPIドキュメントとして用います。

```YAML
# pet.yaml
swagger: "2.0"
info:
  description: "これはペットストアに関するAPIです。"
  version: "1.0.0"
  title: "Petstore API"
  termsOfService: "http://swagger.io/terms/"
  contact:
    email: "apiteam@swagger.io"
  license:
    name: "Apache 2.0"
    url: "http://www.apache.org/licenses/LICENSE-2.0.html"
produces:
  - "application/json"
consumes:
  - application/x-www-form-urlencoded
paths:
  /pets/{petId}:
    get:
      summary: "ペット情報API"
      description: "指定されたpetIdの情報を返します"
      operationId: 'get_pet_by_id'
      tags:
      - 'pet'
      parameters:
      - name: "petId"
        in: "path"
        description: "取得したいペットのID"
        required: true
        type: "integer"
      responses:
        200:
          description: "成功時のレスポンス"
          schema:
            $ref: '#/definitions/Pet'

definitions:
  Pet:
    type: object
    required:
     - id
     - name
    properties:
      id:
        type: "integer"
      name:
        type: "string"
        example: "doggie"
```

## YAMLをJSONに変換

YAMLで書いたSwaggerをJSONに変換します。
変換には、[swagger-codegen](https://github.com/swagger-api/swagger-codegen)を使用します。
swagger-codegenは、Swaggerで書かれたスキーマ定義からクライアントやサーバーのコードを生成するツールです。
多くの言語やフレームワークに対応しており、Rubyのモックコードも生成できます。
swagger-codegenを使用してYAMLをJSONに変換します。

今回はMacのローカルで生成を行うのでhomebrewでswagger-codegenを入れます。

```
# swagger-codegenをインストール
% brew install swagger-codegen

# yamlからjsonへ変換
% swagger-codegen generate -i pet.yaml -l swagger
```

作成されたJSONが下記になります。

```JSON
{
  "swagger" : "2.0",
  "info" : {
    "description" : "これはペットストアに関するAPIです。",
    "version" : "1.0.0",
    "title" : "Petstore API",
    "termsOfService" : "http://swagger.io/terms/",
    "contact" : {
      "email" : "apiteam@swagger.io"
    },
    "license" : {
      "name" : "Apache 2.0",
      "url" : "http://www.apache.org/licenses/LICENSE-2.0.html"
    }
  },
  "consumes" : [ "application/x-www-form-urlencoded" ],
  "produces" : [ "application/json" ],
  "paths" : {
    "/pets/{petId}" : {
      "get" : {
        "tags" : [ "pet" ],
        "summary" : "ペット情報API",
        "description" : "指定されたpetIdの情報を返します",
        "operationId" : "get_pet_by_id",
        "parameters" : [ {
          "name" : "petId",
          "in" : "path",
          "description" : "取得したいペットのID",
          "required" : true,
          "type" : "integer"
        } ],
        "responses" : {
          "200" : {
            "description" : "成功時のレスポンス",
            "schema" : {
              "$ref" : "#/definitions/Pet"
            }
          }
        }
      }
    }
  },
  "definitions" : {
    "Pet" : {
      "type" : "object",
      "required" : [ "id", "name" ],
      "properties" : {
        "id" : {
          "type" : "integer"
        },
        "name" : {
          "type" : "string",
          "example" : "doggie"
        }
      }
    }
  }
}
```


# request specを書く
APIドキュメントにもとづいたAPIを作成します。
今回はわかりやすくするために、swagger-codegenを使ってモックコードを生成せずに、手動でRailsのコードを作成します。

config/routes.rb に`/pets/#{pet_id}`のルーティングを追加し、JSONを返すだけのControllerを作成します。

```Ruby
# config/routes.rb
resources :pets, only: [:show]
```

```Ruby
# app/controllers/pets_controller.rb
class PetsController < ApplicationController
  def show
    render json: { id: 1, name: 'pochi' }
  end
end
```

APIのレスポンスをテストするために、request specを書きます。
request specは、クライアント側の動作や振る舞い、特定のリクエストに対するHTTPレスポンスをテストします。
詳しい説明については、[rspecのREADME](https://github.com/rspec/rspec-rails#request-specs)を参考にしてください。

PetstoreAPIのrequest specを作成します。

```
% bundle exec rails g rspec:integration Pet
```

上記のコマンドを実行すると、下記のようなspecが生成されます。

```ruby
# spec/requests/pets_spec.rb
require 'rails_helper'

RSpec.describe "Pets", type: :request do
  describe "GET /pets" do
    it "works! (now write some real specs)" do
      get pets_path
      expect(response).to have_http_status(200)
    end
  end
end
```

このrequest specに、committeeの設定を追記します。
今回はわかりやすさを重視して、specファイル内にcommitteeの設定を書きます。
committeeの設定を追加すると下記のようになります。

```ruby
# spec/requests/pet_spec.rb
require 'rails_helper'

RSpec.describe "Pets", type: :request do
  include Committee::Test::Methods
  include Rack::Test::Methods

  def committee_schema
    @committee_schema ||=
      begin
        driver = Committee::Drivers::OpenAPI2.new
        schema = JSON.parse(File.read(schema_path))
        driver.parse(schema)
      end
  end

  def schema_path
    Rails.root.join('swagger.json') 
  end

  describe "GET /pets",  type: :request do
    it "レスポンスがAPIドキュメントと一致する" do
      get '/pets/1'
      assert_schema_conform
    end
  end
end
```

schema_pathメソッドとcommittee_schemaメソッドを追加しました。
どちらも、committeeのメソッドをオーバーライドしています。

schema_pathは、API定義のJSONが置かれているパスを記述します。
committeeのschema_pathは、使用する際にオーバライドする必要があり、オーバーライドしないとエラーが返ります。
[schema_path](https://github.com/interagent/committee/blob/26e811050dabd83cb8b3ccf4b13695ef45452852/lib/committee/test/methods.rb#L67)

committee_schemaは、OpenAPI Specをパースするためにオーバーライドします。
このメソッドはAPIドキュメントを読み込む際に使用されます。
driverを指定しないと、JSON Hyper Schemaのドライバが使われます。
今回は、Swaggerで定義したOpenAPI Specをパースするために、OpenAPIのドライバを使用したいので、committee_schemaメソッドをオーバーライドします。
[committee_schema](https://github.com/interagent/committee/blob/26e811050dabd83cb8b3ccf4b13695ef45452852/lib/committee/test/methods.rb#L54)

```Ruby
  def committee_schema
    @committee_schema ||=
      begin
        driver = Committee::Drivers::OpenAPI2.new
        schema = JSON.parse(File.read(schema_path))
        driver.parse(schema)
      end
  end
```

テスト内に`assert_schema_confirm`を記述することで、APIドキュメントと実際のレスポンスが、一致しているかどうかのテストが実行されます。

APIドキュメント内で、requiredに記述した必須カラムを記述していない場合は、下記のようなエラーが表示されます。

```
 1) Pets GET /pets レスポンスがAPIドキュメントと一致する
     Failure/Error: assert_schema_conform

     Committee::InvalidResponse:
       Invalid response.

       #: failed schema #/properties//pets/{petId}/properties/GET: "name" wasn't supplied.
     # ./spec/requests/pets_spec.rb:22:in `block (3 levels) in <top (required)>'
```


# 最後に
Swaggerと実際のAPIレスポンスを、人が都度修正する運用は辛いですし、どうしても差異が発生しやすくなります。
committeeとrequest specを用いて、ドキュメントとの差異を防いでいきたいです。

# 参考
[スキーマファースト開発のススメ](https://blog.onk.ninja/2017/09/21/schema_first_development)
[Web APIのレスポンスJSONをCommittee + OpenAPIでバリデーションして仕様と実装の乖離を防ぐ](http://blog.kymmt.com/entry/use-committee-and-openapi-to-validate-response-jsons)
[JSON Validation by Committee](https://www.sitepoint.com/json-validation-by-committee/)