📝

committeeで、ファイル分割したOpenAPI定義を使用する方法

2020/11/28に公開
2

committeeは、OpenAPIの定義内容とAPIの挙動に乖離がないかをRSpecで検証するためのGemです。
https://github.com/interagent/committee

ちなみに、Railsでの使用に対応した committee-rails というgemがあります。Rspecとcommitteeで、OpenAPIとの比較用に使うAPIのリクエスト・レスポンスの指定を簡単にやってくれるgemです。
https://github.com/willnet/committee-rails

OpenAPIの定義ファイルが巨大になる時〜

OpenAPIをYAMLに定義していくと、巨大なYAMLファイルになるので、スキーマやレスポンス毎にファイル分割したい時がよくあると思います。

例えば、こういうOpenAPI定義があるとします。

openapi.yaml

openapi: 3.0.1
info:
  title: API Document
  description: >-
    API doc
  version: 1.0.0
paths:
  "api/users/{id}":
    get:
      responses:
        "200":
	  description: return User info
	  content:
	    application/json:
	      schema: $ref: "#/components/schemas/User"
	      
components:
  schemas:
    User: ./schemas/User.yaml

Userというスキーマが利用頻度が高い場合、毎回同じ内容を定義せず、このように別ファイルにするとファイルの肥大化を防げます。
(openapi.yamlと同じ階層に schemas/というディレクトリがある前提)

schemas/User.yaml

type: object
properties:
  id:
    type: integer
  name:
    type: string

OpenAPIの定義に基づいて、APIをRailsのコントローラーで実装するとこうなるでしょう。

class Api::UsersController < ApplicationController
  def show
    user = User.find(params[:id])
    
    render json: user
  end
end

これで、Swagger-UIなどでは、問題なくAPIドキュメントは表示されます。
しかし、以下のようにcommitteeを使ったRSpecを実行する場合は、どうなるでしょうか?

require "spec_helper"

RSpec.describe Api::UsersController, type: :request do
  describe "GET /users/:id" do
    include Committee::Rails::Test::Methods

    def committee_options
      @committee_options ||= { schema_path: Rails.root.join("openapi.yaml").to_s }
    end
  
    it "スキーマ定義とAPIの挙動が同じであること" do
      user_id = 1
      get api_user_path(user_id)

      assert_response_schema_confirm
    end
  end
end

エラーが発生

Failure/Error: assert_response_schema_confirm

     NoMethodError:
       undefined method `any_of' for #<OpenAPIParser::Schemas::Reference:0x00007ffd19c6eaa8>

TL;DR

先に結論を述べると、$ref: 分割したファイルのパス#components/schemas/xxxxのような形式で、パス指定をしないと、committee内部で、使用しているopenapi_parserが外部ファイルのパス参照をしてくれない。

参考: https://swagger.io/docs/specification/components/

そのため$refのパス指定は以下のように修正

...省略
components:
  schemas:
    User:
      $ref: "./schemas/User.yaml#/components/schemas/User"

分割したUser.yamlも、componentsを一番上のキーとして修正しなければならない。

schemasUser.yaml

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string

原因としては、、committeeが内部で、openapiのYAMLをパースするために使用しているopenapi_parserhttps://github.com/ota42y/openapi_parser/blob/f9b57b0faea11a39f1264d260f2bdc9a6f1c54a3/lib/openapi_parser/concerns/findable.rb#L9 で、$ref: ./schemas/User.yamlだと、別ファイルの参照する処理が実行されないためである。

分割された定義ファイルの参照に関するPRもある。
https://github.com/ota42y/openapi_parser/pull/66

  • openapi_parser

https://github.com/ota42y/openapi_parser

最初から、OpenAPIのドキュメント(https://swagger.io/docs/specification/components/)を読んでいれば早く気付けたのに、回り道してGem内部の実装を読んでいた。

公式ドキュメントは、ちゃんと読みましょう(自戒)🙏

Discussion

はっさんブログはっさんブログ

記事の通りに "./schemas/User.yaml#components/schemas/User" としても動かなかったのですが、"./schemas/User.yaml#/components/schemas/User" のようにスラッシュを # の後ろに入れたら動きました。

書き方のサンプルとしては以下の yaml ファイルが参考になりました。
https://github.com/ota42y/openapi_parser/pull/66/files

samuraikunsamuraikun

ご指摘ありがとうございます!記事の方も修正しておきました。