はじめに
Request specからいい感じのOpenAPIドキュメントを作ってくれるgemのご紹介です。
バージョンアップで動作が変わることもあるので最新の情報はREADMEを参照してください。
検証環境
Rails : 7.0.4
rspec-openapi : 0.7.2
インストールと実行
1. インストール
testでしか使わないので group: test
に追加すると良いでしょう。
group :test do
gem "rspec-openapi"
end
2. 実行
rspecの実行に OPENAPI=1
を追加するだけです。
OPENAPI=1 rspec
オプションがついているだけでrspecの実行に変わりないのでファイル指定やオプションはそのまま使えます。
試していませんがCIで生成させても良いと思います。
OPENAPI=1 rspec spec/requests/path/to
OPENAPI=1 rspec spec/requests/path/to -e 'hogehoge'
3. 生成ファイルの確認
デフォルトでは doc/openapi.yaml
に生成されます。
サンプルspecとその生成例が公式にあるので見ておくとイメージがつきやすいです。
設定やQ&A
README通りのものもありますが、設定しておくと便利なものやハマった事例を紹介します。
requiredが付与されない
rspec-openapiはよろしくOpenAPIドキュメントを生成してくれますがrequiredは付与してくれません。
Request specの実行結果から生成するのでどれが必須なのかは判断できないのだと思われます。
生成後のyamlにrequiredを追記しても消えることはないので生成後に手動で追記しています。
requestBodyがapplication/x-www-form-urlencodedで生成される
ちゃんとparams.to_jsonで送りheadersにContent-Typeを設定しましょう。
summaryやdescriptionがわかりづらい
これらはspecのdescribeやitから生成されるのでこれらを変更しても良いです。
が、specだけ見たときにわかりづらくなるとそれはそれでなのでmetadataを追記してわかりやすくします。
describe 'PUT /api/v1/posts/:id', openapi: {
summary: 'POSTを更新する'
} do ...
"/api/v1/posts/{id}":
put:
summary: POSTを更新する
descriptionも同じように書きたいのですが、似たような形でitに書くとdescribeの情報が上書きされ消えてしまいます。
describe 'PUT /api/v1/posts/:id', openapi: {
summary: 'POSTを更新する'
} do
...
context 'POSTが存在しないとき' do
it '404を返却する', openapi: { description: 'POSTが存在しない' } do ...
"/api/v1/posts/{id}":
put:
summary: update # describeに書いたsummaryが反映されていない
...
responses:
'404':
description: 'POSTが存在しない'
解決方法がわからないのでRSpec::OpenAPI.description_builderと独自metadataで解決しました。
RSpec::OpenAPI.description_builder = -> (example) {
example.metadata[:openapi_description] || example.description
}
describe 'PUT /api/v1/posts/:id', openapi: {
summary: 'POSTを更新する'
} do
...
context 'POSTが存在しないとき' do
it '404を返却する', openapi_description: 'POSTが存在しない' do ...
"/api/v1/posts/{id}":
put:
summary: 'POSTを更新する'
...
responses:
'404':
description: 'POSTが存在しない'
生成yamlをspecごとに分割する
デフォルトだとすべてdoc/openapi.yamlに生成されるのでGitのコンフリクトが起きやすいです。
RSpec::OpenAPI.path
にはブロックを渡せるのでファイルごとに分割できます。
RSpec::OpenAPI.path = -> (example) {
"doc/openapi/#{example.metadata[:file_path].match(%r{api/v1/(.*)_spec.rb})[1]}.yaml"
}
↑のように設定した場合は、 spec/requests/api/vi/xxx_spec.rb
が doc/openapi/xxx.yaml
に出力されます。
specごとは分割しすぎならREADMEのようにパスで分割しても良いと思います。
分割したファイルは openapi-merge などで結合できます。
個々のファイルを解消するのは比較的簡単ですし、それぞれでコンフリクト解消できれば結合ファイルは上書きするだけです。
ステータスコードが重複するspecはどれか1つから生成される
post has_many comments となるようなモデルがあったとき、更新で404を返すのはpostがないとき/commentがないときの2パターンだとします。
Request specもそれぞれ書くと。
describe 'PUT /api/v1/posts/:post_id/comments/:id' do
context 'POSTが存在しないとき' do
it '404を返却する', openapi_description: 'Postが存在しない' do
put "/api/v1/posts/1/comments/1", params: { comment: { text: 'new text' } }.to_json, headers: headers
expect(response).to have_http_status(:not_found)
end
end
context 'Commentが存在しないとき' do
let!(:post) { FactoryBot.create(:post) }
it '404を返却する', openapi_description: 'Commentが存在しない' do
put "/api/v1/posts/#{post.id}/comments/1", params: { comment: { text: 'new text' } }.to_json, headers: headers
expect(response).to have_http_status(:not_found)
end
end
end
OpenAPIはどちらかのspecを元に生成されます。
responses:
'404':
description: Commentが存在しない
content:
application/json:
schema:
type: object
properties:
errors:
type: string
example:
errors: Couldn't find Comment with 'id'=1 [WHERE "comments"."post_id"
= ?]
これはおそらく対処方法がないので手動でよろしく直すしかありません。
同じエラーコードでレスポンスが全く異なるケースはまれでしょうからdescriptionくらいかなと思います。
describe 'PUT /api/v1/posts/:post_id/comments/:id' do
context 'POSTが存在しないとき' do
it '404を返却する', openapi_description: 'PostまたはCommentが存在しない' do
...
context 'Commentが存在しないとき' do
let!(:post) { FactoryBot.create(:post) }
it '404を返却する', openapi_description: 'PostまたはCommentが存在しない' do
responses:
'404':
description: PostまたはCommentが存在しない
exampleが毎回変わる
Fakerを使っているときに起きます。
手っ取り早いのはexampleの生成をやめることです。
# Disable generating `example`
RSpec::OpenAPI.enable_example = false
exampleを生成したいならFakerのseed値を固定する方法がIssueで提案されていました。
環境変数 OPENAPI
があるときはseed値を固定するので常に同じ値が生成されることになります。
ただし、生成するときだけ OPENAPI=1
をつけるようにしないとFakerを使う意味がなくなるので注意が必要です。
終わりに
いくつか注意点はあるもののRequest specを書けばOpenAPIドキュメントが生成されるのは非常に楽です。
またプロダクトコードを直したら大抵はspecも直すのでドキュメントの追従性が高いもの魅力の一つです。
API仕様はコードです、と言わないようにうまく自動生成していきましょう!