はじめに
Rails 5 APIモード + Grapeで実装された既存のWeb APIに、ドキュメント自動生成の機構を追加するサンプル。
今回は、実装からAPIドキュメントを生成するという前提で記事を書いていく。実装からAPIドキュメントを生成することで、別々に管理される実装とドキュメントが乖離していってしまうのを防ぐことが出来る。特に、個人開発や少人数チームにおいてフロント・バックエンド共に同じ人が書くと言った場合に有効だろう。
今回の記事でベースとする既存のWeb APIは、Rails 5 APIモード + Grape + Grape::Entityで作るWeb API - Qiitaで作成したものとする。
前提条件
検証環境は以下の通り。
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 )をそのまま使っているためだ。
方針
- 実装からAPIドキュメントを生成する。
-
/v1/swagger_docs
でswagger定義ファイルを配信する。 - swagger定義ファイルの生成には
grape-swagger
とgrape-swagger-entity
gemを利用する。
-
-
/v1/docs
でSwagger UIを利用したAPIドキュメントの配信を行う。-
swagger_ui_engine
gemを利用する。
-
下準備
Gemのインストール
まずは必要なGemをインストールする。今回利用するのは以下の3つ。
Gemfile
に追記しインストール。
gem 'grape-swagger', '~> 0.33.0'
gem 'grape-swagger-entity', '~> 0.3.3'
gem 'swagger_ui_engine', '~> 1.1', '>= 1.1.3'
$ bundle install
swagger定義ファイルを配信する
まずは、swagger定義ファイルを自動生成し配信するよう設定する。
今回生成したいのはAPI V1のドキュメントなので、V1のRootに追記。
( app/api
以下のディレクトリ構成は こちら を参照。)
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形式でも取得することが出来る。
SwaggerUIで閲覧できるようにする
YAMLやJSON形式のファイルを人間が直接読むのはつらいので、HTMLの文書で配信する。
まずは config/initializers
以下に設定ファイルを作成。swaggerドキュメントのpathを教えてやる。
SwaggerUiEngine.configure do |config|
config.swagger_url = {
v1: '/v1/swagger_doc'
}
end
その後、 config/routes.rb
でルーティングの記述をする。
Rails.application.routes.draw do
# 省略
mount SwaggerUiEngine::Engine, at: '/v1/docs'
end
こんな感じでAPIドキュメントを閲覧できる。
使い方
ここまでやった手順で生成されるAPIドキュメントだけでも一見便利だけど、どんなレスポンスが返ってくるのかは実際にAPIを叩いてみるまでわからない、というのではAPIドキュメントの意味が無い。
Entityにドキュメンテーションを追加する
まずは、Entityファイルにドキュメンテーションを追加する。
かんたんな例として V1::Entities::AuthorEntity
から。
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
はこうなる。
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 に渡しているハッシュの部分だ。
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
も実装する。
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
結果
こんな感じで、どんなレスポンスが返ってくるかや、どんなリクエストを投げれば良いかが提示される。
おわりに
以上で、Rails + GrapeによるWeb APIのドキュメントを実装から自動生成することが出来た。
今回はわりと雑に作ったけど、例えばOpenAPI Generatorを使ってSwagger定義からAPIクライアントライブラリを生成したいとかなると、もっと細かく指定しなきゃいけないと思う。エラー用のEntityを作って failure
に渡してやるとか、複数レコード返す用のEntityを別途作るとか。
詳しくは公式ドキュメントをどうぞ。