👻

OpenAPI の API 定義で response schema を強制する rake task を導入する

に公開

既存の API 定義のレスポンススキーマの変更により未修正のファイルを洗い出したい、また、今後の定義作成時に旧スキーマを使わないように強制したい欲が出たので、これを検出する rake task を作成したので紹介する。

作成のきっかけ

API 作成時は OpenAPI の YAML 形式で API 定義を作成しており、これらのステータスコード 401, 403 のレスポンススキーマを変更したいとなった。元々使っていたスキーマは別のステータスコードで引き続き利用されている。

変更対応した後、漏れがないかを grep -n -A 10 '"ステータスコード"' -R path/to/dir | grep -E "旧スキーマ名" で調べて、漏れはなく対応完了となった。だがしかし、同時並行で別の Pull Request が作成されており、そちらの API 定義で旧スキーマを用いた定義が作成されてしまうという事案があったため作成した、という経緯となっている。

API 定義のサンプル。

/api/resources:
  get:
    summary: resourceの一覧を取得する
    description: resource の一覧
    operationId: getResources
    tags:
      - Resource
    responses:
      "200":
        description: resource の一覧
        content:
          application/json:
            schema:
              type: array
              items:
                $ref: "#/components/schemas/Resource"
      "401":
        description: Unauthorized
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Error" # <- このスキーマを別のものに変更する
      "403":
        description: Forbidden
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Error" # <- このスキーマを別のものに変更する
      "500":
        description: Internal server error
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Error"

実装内容

YAML.safe_load_file 等、YAML を YAML として読み込むと対象のステータスコードの定義にたどり着くまでが大変なので、テキストとして扱うことにした。

先程の検証時に使用した grep -A n のコマンドを参考に、対象のステータスコード文字列を検出したらそこから n 行分読み込み、期待しているレスポンススキーマが含まれているかを調べている。含まれなければ、ステータスコードのある行数とエラーメッセージを出力する。

なぜ出力する行数はステータスコードのある行なのか、という問いには「サボったから」という回答をせざるをえない。行判断の処理を入れるとコードがごちゃごちゃするのと、エディタで開いたらレスポンス定義まで見えるでしょ、という期待があるため、さほど重要ではないと考えることにした。

この n (MAX_RESPONSE_BLOCK_LINES) だが、「次のステータスコードの $ref の一行手前までの行数」を示している。-A n で取得した範囲に $ref が複数入っていると誤検知してしまうため、description が複数行、かつ、長いと誤検知してしまう。幸い今回の対象ではそこまで長い description はなかったため n = 10が成立している。

ということで、コードは以下のようになっており、ファイルは lib/tasks/response_schema_checker.rake として保存している。

# frozen_string_literal: true

# 次のレスポンス定義までの行数(responses の次のステータスコードまでの想定行数)
MAX_RESPONSE_BLOCK_LINES = 10

namespace :response_schema_checker do
  desc 'API定義に記述されている 401 と 403 のレスポンススキーマが正しいかをチェックする'
  task check: :environment do
    targets = [
      %w[401 UnauthorizedError],
      %w[403 ForbiddenError]
    ]

    all_errors = targets.flat_map do |status, expected_schema|
      errors = check_response_schema(status, expected_schema)
      output_errors(status, errors, expected_schema) if errors.any?
      errors
    end

    exit(1) if all_errors.any?

    puts "All response schemas are correct!"
  end

  private

  # 特定のHTTPステータスコードのレスポンススキーマをチェック
  # @param status_code [String] チェックするHTTPステータスコード(例: '401', '403')
  # @param expected_schema [String] 期待されるエラースキーマ名(例: 'UnauthorizedError')
  # @return [Array<String>] エラーメッセージの配列
  def check_response_schema(status_code, expected_schema)
    openapi_paths = 'path/to/api_definitions' # これは定数でよさそう

    raise "ディレクトリが見つかりません: #{openapi_paths}" unless Dir.exist?(openapi_paths)

    Dir.glob("#{openapi_paths}/**/*.yml").each_with_object([]) do |file, errors|
      lines = File.readlines(file)

      lines.each.with_index(1) do |line, index|
        # レスポンスステータスコードのキー定義にマッチ(YAMLのキーとして定義されている場合のみ)
        # 例: "401": または '401':
        next unless line.match?(/^\s+["']#{status_code}["']:\s*$/)

        # 次のlines_to_check行をチェック(レスポンススキーマ定義を確認)
        next_lines = lines[index - 1, MAX_RESPONSE_BLOCK_LINES].join
        # index は status_code の記述がある行を示している
        errors << "  * #{file}: #{index}行目 (ステータスコード定義の行) " unless next_lines.include?(expected_schema)
      end
    end
  end

  def output_errors(status_code, errors, response_schema)
    puts
    puts "#{status_code}レスポンススキーマの指定誤りが見つかりました"
    puts "以下のレスポンススキーマを #{response_schema} に書き換えてください。"
    errors.each { |error| puts error }
    puts
  end
end

最初は grep コマンドを %x 相当で実行した結果を集めてなんとかしようと考えていたが、AI Agent 様にレビューを依頼したらポータビリティが落ちるから、と書き直されてしまったので、力を借りつつ書き直したのであった。

実行結果

rake task を実行し、違反がある場合は status 1 で終了し、以下のような出力をする。

$ bin/rails response_schema_checker:check

401レスポンススキーマの指定誤りが見つかりました
以下のレスポンススキーマを UnauthorizedError に書き換えてください。
  * path/to/api_definitions/resources.yml: 31行目 (ステータスコード定義の行)


403レスポンススキーマの指定誤りが見つかりました
以下のレスポンススキーマを ForbiddenError に書き換えてください。
  * path/to/api_definitions/resources.yml: 37行目 (ステータスコード定義の行)

違反がない場合は、以下のような出力をして正常終了する。

$ bin/rails response_schema_checker:check

All response schemas are correct!

まとめ

API 定義のレスポンススキーマの指定を強制する rake task を紹介した。
これを使うことで今後の定義作成時にも誤ったスキーマを使わないようにする効果が期待できるし、チェック対象のステータスコードが増えても対応できる。

対象のステータスコードが増えた場合は探索処理の効率がよくないのだが、漏れを検出するための初手としては十分ではないだろうか。

あしたのチーム Tech Blog

Discussion