grape-swagger + grape-swagger-entityでRails + GrapeによるWeb APIのドキュメントを自動生成する

## はじめに

Rails 5 APIモード + Grapeで実装された既存のWeb APIに、ドキュメント自動生成の機構を追加するサンプル。

今回は、実装からAPIドキュメントを生成するという前提で記事を書いていく。実装からAPIドキュメントを生成することで、別々に管理される実装とドキュメントが乖離していってしまうのを防ぐことが出来る。特に、個人開発や少人数チームにおいてフロント・バックエンド共に同じ人が書くと言った場合に有効だろう。

今回の記事でベースとする既存のWeb APIは、[Rails 5 APIモード + Grape + Grape::Entityで作るWeb API - Qiita](https://qiita.com/mktakuya/items/117f1563c2b7e8a6e69c)で作成したものとする。


## 前提条件

検証環境は以下の通り。

```
ProductName:	Mac OS X
ProductVersion:	10.14.5
BuildVersion:	18F132
```

```
ruby 2.5.3p105 (2018-10-18 revision 65156) [x86_64-darwin18]
```

```
Rails 5.2.1.1
```

RubyやRailsのバージョンが若干古いのは、ベースとなるプロジェクト（ [Rails 5 APIモード + Grape + Grape::Entityで作るWeb API - Qiita](https://qiita.com/mktakuya/items/117f1563c2b7e8a6e69c) ）をそのまま使っているためだ。


## 方針

- 実装からAPIドキュメントを生成する。
    - `/v1/swagger_docs` でswagger定義ファイルを配信する。
    - swagger定義ファイルの生成には `grape-swagger` と `grape-swagger-entity` gemを利用する。
- `/v1/docs` でSwagger UIを利用したAPIドキュメントの配信を行う。
    - `swagger_ui_engine` gemを利用する。


## 下準備

### Gemのインストール

まずは必要なGemをインストールする。今回利用するのは以下の3つ。

- [grape-swagger](https://github.com/ruby-grape/grape-swagger)
- [grape-swagger-entity](https://github.com/ruby-grape/grape-swagger-entity)
- [swagger_ui_engine](https://github.com/zuzannast/swagger_ui_engine)

`Gemfile` に追記しインストール。

```Gemfile
gem 'grape-swagger', '~> 0.33.0'
gem 'grape-swagger-entity', '~> 0.3.3'
gem 'swagger_ui_engine', '~> 1.1', '>= 1.1.3'
```

```console
$ bundle install
```


### swagger定義ファイルを配信する

まずは、swagger定義ファイルを自動生成し配信するよう設定する。

今回生成したいのはAPI V1のドキュメントなので、V1のRootに追記。
（ `app/api` 以下のディレクトリ構成は [こちら](https://qiita.com/mktakuya/items/117f1563c2b7e8a6e69c#api%E3%81%AE%E6%96%B9%E9%87%9D) を参照。）

```app/api/v1/root.rb
module V1
  class Root < Grape::API
    # 省略

    mount V1::Authors
    mount V1::Books

    # V1::Rootの末尾に追記
    add_swagger_documentation(
      doc_version: '1.0.0',
      info: {
        title: 'grape-on-rails-api',
        description: 'grape-on-rails-apiのAPIドキュメント'
      }
    )
  end
end
```

その後、 `/v1/swagger_doc` にアクセスすると、Swaggerの定義ファイルが配信されているのを確認することが出来る。
末尾に `.json` をつけるとJSON形式でも取得することが出来る。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35647/85196657-bd87-fd43-ed12-91073bb9f941.png)


### SwaggerUIで閲覧できるようにする

YAMLやJSON形式のファイルを人間が直接読むのはつらいので、HTMLの文書で配信する。

まずは `config/initializers` 以下に設定ファイルを作成。swaggerドキュメントのpathを教えてやる。

```config/initializers/swagger_ui_engine.rb
SwaggerUiEngine.configure do |config|
  config.swagger_url = {
    v1: '/v1/swagger_doc'
  }
end
```

その後、 `config/routes.rb` でルーティングの記述をする。

```config/routes.rb
Rails.application.routes.draw do
  # 省略
  mount SwaggerUiEngine::Engine, at: '/v1/docs'
end
```

こんな感じでAPIドキュメントを閲覧できる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35647/2c9025a7-b9c0-83f6-3484-7995bde77577.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35647/535a17e4-2669-3a30-1e9c-ae16240be9d2.png)


## 使い方

ここまでやった手順で生成されるAPIドキュメントだけでも一見便利だけど、どんなレスポンスが返ってくるのかは実際にAPIを叩いてみるまでわからない、というのではAPIドキュメントの意味が無い。

### Entityにドキュメンテーションを追加する

まずは、Entityファイルにドキュメンテーションを追加する。

かんたんな例として `V1::Entities::AuthorEntity` から。

```app/api/v1/entities/author_entity.rb
module V1
  module Entities
    class AuthorEntity < Grape::Entity
      expose :id, documentation: { type: 'integer', required: true }
      expose :name, documentation: { type: 'string', required: true }
    end
  end
end
```

exposeの引数に、 `documentation` というキーで型や必須かどうかを与えてやるだけだ。そのプロパティが必須でない場合は、 required を外せば良い。

`V1::Entities::BookEntity` はこうなる。

```app/api/v1/entities/book_entity.rb
module V1
  module Entities
    class BookEntity < Grape::Entity
      expose :id, documentation: { type: 'integer', required: true }
      expose :title, documentation: { type: 'string', required: true }
      expose :price, documentation: { type: 'integer', required: true }
      expose :tax_included_price, documentation: { type: 'integer', required: true } do |instance, options|
        instance.price * 1.08
      end
      expose :author, using: V1::Entities::AuthorEntity, documentation: { required: true }
    end
  end
end
```

`using` で別のエンティティを指定している場合は、typeはいらないので必須かどうかだけを指定している。


### APIの実装にドキュメンテーションを追加する

次はAPIの実装にドキュメンテーションを追加していく。

コードが長いが、大事なのは desc に渡しているハッシュの部分だ。

```app/api/v1/books.rb
module V1
  class Books < Grape::API
    resources :books do
      desc 'returns all books', {
        is_array: true, # 複数レコードを返すエンドポイントに必要
        success: V1::Entities::BookEntity # 成功時に返すレスポンスのEntity
      }
      get '/' do
        @books = Book.all
        present @books, with: V1::Entities::BookEntity
      end

      desc 'returns a book', {
        success: V1::Entities::BookEntity
      }
      params do
        requires :id, type: Integer
      end
      get '/:id' do
        @book = Book.find(params[:id])
        present @book, with: V1::Entities::BookEntity
      end

      desc 'Create a book', {
        success: V1::Entities::BookEntity
      }
      params do
        requires :title, type: String
        requires :price, type: Integer
        requires :author_id, type: Integer
      end
      post '/' do
        @book = Book.new(
          title: params[:title],
          price: params[:price],
          author_id: params[:author_id]
        )

        if @book.save
          status 201
          present @book, with: V1::Entities::BookEntity
        else
          status 400
          present @book.errors
        end
      end
    end
  end
end
```

descのオプションにハッシュで `is_array` や `success` を与えてやると良い。

同じように `V1::Authors` も実装する。

```app/api/v1/authors.rb
module V1
  class Authors < Grape::API
    resources :authors do
      desc 'returns all authors', {
        is_array: true,
        success: V1::Entities::AuthorEntity
      }
      get '/' do
        @authors = Author.all
        present @authors, with: V1::Entities::AuthorEntity
      end

      desc 'returns an author', {
        success: V1::Entities::AuthorEntity
      }
      params do
        requires :id, type: Integer, desc: 'Author ID'
      end
      get '/:id' do
        @author = Author.find(params[:id])
        present @author, with: V1::Entities::AuthorEntity
      end

      desc 'Create an author', {
        success: V1::Entities::AuthorEntity
      }
      params do
        requires :name, type: String, desc: 'Author name'
      end
      post '/' do
        authenticate!

        @author = Author.new(name: params[:name])

        if @author.save
          status 201
          present @author, with: V1::Entities::AuthorEntity
        else
          status 400
          present @author.errors
        end
      end

      desc 'Delete an author'
      params do
        requires :id, type: Integer, desc: "Author ID"
      end
      delete '/:id' do
        @author = Author.find(params[:id])

        if @author.destroy
          status 204
          present nil
        else
          status 400
          present @author.errors
        end
      end
    end
  end
end
```

### 結果

こんな感じで、どんなレスポンスが返ってくるかや、どんなリクエストを投げれば良いかが提示される。

![スクリーンショット 2019-07-29 13.38.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35647/1fcee6ba-7a28-1cf0-ec92-5894364bf351.png)

![スクリーンショット 2019-07-29 13.39.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35647/020c301e-7b04-575c-c04e-1c2b68ac2af3.png)


## おわりに

以上で、Rails + GrapeによるWeb APIのドキュメントを実装から自動生成することが出来た。

今回はわりと雑に作ったけど、例えばOpenAPI Generatorを使ってSwagger定義からAPIクライアントライブラリを生成したいとかなると、もっと細かく指定しなきゃいけないと思う。エラー用のEntityを作って `failure` に渡してやるとか、複数レコード返す用のEntityを別途作るとか。

詳しくは公式ドキュメントをどうぞ。

* [ruby-grape/grape-swagger: Add OAPI/swagger v2.0 compliant documentation to your grape API](https://github.com/ruby-grape/grape-swagger)
  * [Using an options hash](https://github.com/ruby-grape/grape-swagger#using-an-options-hash-)とか