3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Ruby開発Advent Calendar 2022

Day 15

gem 'rspec-openapi' を紹介したい

Last updated at Posted at 2022-12-14

はじめに

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.rbdoc/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仕様はコードです、と言わないようにうまく自動生成していきましょう!

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?