Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

rspec-openapiを導入した話

こんにちは、サーバーサイドグループの五十嵐です。
今年の4月に弊社に転職して初めて技術ブログを書きます。
今回はAPIドキュメントの作成ツールとしてGemrspec-openapiを導入した話について書いてみます。

弊社では以前からOpenAPIを導入しドキュメント管理を行なってきました。
しかし、OpenAPIを導入する前からあったAPIに対するドキュメントは不足していたり内容が不十分なものもありました。
このようなツールは導入当初はモチベーションが高くても、年月が経つ内にモチベーションが低下したり、メンバーが変わることで存在自体なかったことになったりはよくあることだと思います。

実際にOpenAPIは全て手で書こうとするとはっきり言って非常にめんどうで、そこそこ工数もかかります。
API新規作成時は自分で必要な情報がわかっているので書きやすいですが、既存のAPIで新しくドキュメントを作成しようとするとかなりの負担です。

そこで、ドキュメント作成の負担をなるべく下げるためにGemrspec-openapiを導入してみることになりました。

rspec-openapiの導入

rspec-openapiはrspecのRequest SpecからOpenAPIを生成するGemで使い方も非常に簡単です。

github.com

インストール

まずはGemをインストールします。
testでしか使わないのでgroup :testに追加しましょう。

  group :test do
    gem "rspec-openapi"
  end

実行

以下のように作成したいAPIに対応するRequest Specを指定して実行します。
普段のspec実行の際に頭にOPENAPI=1と入れるだけでspec実行後にOpenAPIを生成します。

OPENAPI=1 rspec spec/requests/hoge_spec.rb

もちろん1つずつの実行でなくても問題なく実行できます。

OPENAPI=1 rspec spec/requests/

Open APIの確認

実行するとリクエスト情報とレスポンス情報を元にOpenAPIを生成してくれます。
デフォルトではdoc/openapi.yamlに作成されます。
キー情報や型情報だけでなく、実際のレスポンスを元にExampleも生成してくれます。

openapi: 3.0.3
info:
  title: rspec-openapi
paths:
  "/tables":
    get:
      summary: index
      tags:
      - Table
      parameters:
      - name: page
        in: query
        schema:
          type: integer
        example: 1
      - name: per
        in: query
        schema:
          type: integer
        example: 10
      responses:
        '200':
          description: returns a list of tables
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: integer
                    name:
                      type: string
                    # ...

これはサンプルでほんの一部ですがこれを各APIそれぞれ手作業で書いていく・・・
今考えると寒気がします。

rspec-openapiを触ってみて

触ってみての第一印象は「何これ!?便利すぎ!!」でした。
API作成時必ず書くRequest Specから作成してくれるとはなんて素敵なのでしょう!!

今まで既存のAPIのドキュメント作成時はソースを読んでレスポンスを追い、「これとこれがこう言う形で返るよな・・・サンプル値は・・・」と手作業で書いていましたが、rspec-openapiはrspec実行時に頭にOPENAPI=1とつけるだけです。

これに感動しない人はいるでしょうか?いや、いないはずです。

こんなにも便利で素晴らしいGemですが、触ってみていくつか注意点もありました。

注意点

  • specをしっかり書いていないと期待するクオリティーのOpenAPIが生成されない
    rspec-openapiはテストケースとして書いたspecを実行しリクエストのパラメータにしたりレスポンスのサンプル値にします。
    よってテストをしっかり書いておかないと、必須ではないパラメータやレスポンスの記載が漏れてしまうことがあります。

  • 必要のないrequiredが付与される場合がある
    specの実行結果からOpenAPIが作られる都合上、どれが必須なのかは判断仕切れないと思われます。
    テスト時に値があるものにはrequiredがつく傾向があり、テストではparamsを設定したが実際は必須ではない場合はOpenAPI作成後に手動で削除する必要がある場合があります。

  • ステータスコードが重複するテストケースはどれか1つから生成される
    レスポンスのステータスコードが重複するテストケースがある場合、このテストケースでOpenAPIを作成する!という指定はできません。
    指定したい場合は以下のようにテストケースを指定してあげることで解決可能です。

  OPENAPI=1 rspec spec/requests/hoge_spec.rb:10
  • 不要になったエンドポイントやキーの削除、レスポンス結果の更新が行われない
    実装上、一度生成するとキーが同じなら上書きされません。
    よって後から手動で修正しても修正したものが残りますが、doc/openapi.yamlがある状態でspecを修正して再実行したとしても、不要になったエンドポイントやキーの削除、レスポンス結果の更新が行われません。
    一旦既存のファイルを削除することで再作成されますが、手直しで修正した部分も消えてしまうので注意が必要です。

  • $refを使った重複schemaなどの参照は手作業で行う必要がある
    自動作成時はschemaが重複した場合も冗長に生成されてしまいます。
    しかし、手動で$refを使うように修正することで再実行時にはcomponents/schemaを使ってくれます。

    • 以下例

  一度OpenAPIを作成します。
  以下の場合、responseのUserが重複しています。

  paths:
    "/users":
      get:
        responses:
          '200':
            content:
              application/json:
                schema:
                  type: array
                  items:
                    User:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        role:
                          type: array
                          items:
                            type: string
    "/users/{id}":
      get:
        responses:
          '200':
            content:
              application/json:
                schema:
                    User:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        role:
                          type: array
                          items:
                            type: string

  生成されたOpenAPIを以下のように$refを使うように修正します。
  この時、components/schema/Userは自分で書いておく必要はありません。

  paths:
    "/users":
      get:
        responses:
          '200':
            content:
              application/json:
                schema:
                  type: array
                  items:
                    $ref: "#/components/schemas/User"
    "/users/{id}":
      get:
        responses:
          '200':
            content:
              application/json:
                schema:
                  $ref: "#/components/schemas/User"

  この状態で再実行します。
  以下のようにcomponents/schemas/Userが自動生成されます。

  paths:
    "/users":
      get:
        responses:
          '200':
            content:
              application/json:
                schema:
                  type: array
                  items:
                    $ref: "#/components/schemas/User"
    "/users/{id}":
      get:
        responses:
          '200':
            content:
              application/json:
                schema:
                  $ref: "#/components/schemas/User"
  components:
    schemas:
      User:
        type: object
        properties:
          id:
            type: string
          name:
            type: string
          role:
            type: array
            items:
              type: string

  このように$refを使うことで重複箇所を最小化できます。

まとめ

私たちのグループではrspec-openapiは、OpenAPIの土台作りには非常に便利で、そこから軽く手直しをする程度で運用できると結論づけて運用を開始しました。 このおかげで現在はほぼ全てのAPIがドキュメント化され、最新の状態になってきています。 便利なGemを使わせていただき、モチベーションを高く保ち続けることで、しっかりとAPIドキュメント管理を行なっていきたいと思います。