LoginSignup
8
2

More than 3 years have passed since last update.

Rails + Grape + Grape SwaggerでちゃんとOpenAPI/Swaggerドキュメンテーションしようとしたらハマったこと

Posted at

この記事はCBcloud Advent Calendar 2020 の12日目の記事です。

Rails + Grape + Grape Swaggerで、実際に業務でOpenAPIドキュメントを生成してメンバーに共有しようとしたときにハマったことについて紹介します。

まずはおさらい: OpenAPI、Swaggerってなんだっけ?

OpenAPI

OpenAPIのリポジトリには下記のように記述されています。
https://github.com/OAI/OpenAPI-Specification

The OpenAPI Specification (OAS) defines a standard, programming language-agnostic interface description for HTTP APIs, which allows both humans and computers to discover and understand the capabilities of a service without requiring access to source code, additional documentation, or inspection of network traffic.

つまりOpenAPIは、プログラミング言語に依存しないHTTP APIのインタフェースを記述するための仕様です。
その仕様に従い記述されたドキュメントは、人が読んでも理解できるし、それを解析していい感じのUIを提供することもできるようになっている、ということです。

Swagger

Swaggerのページには下記のように記述されています。
https://swagger.io/about/

Swagger is a powerful yet easy-to-use suite of API developer tools for teams and individuals, enabling development across the entire API lifecycle, from design and documentation, to test and deployment.
〜略〜
Swagger started out as a simple, open source specification for designing RESTful APIs in 2010. Open source tooling like the Swagger UI, Swagger Editor and the Swagger Codegen were also developed to better implement and visualize APIs defined in the specification.
〜略〜
In 2015, the Swagger project was acquired by SmartBear Software. The Swagger Specification was donated to the Linux foundation and renamed the OpenAPI

つまりSwaggerはAPI開発者のためのツール群を提供するプロジェクトということですね。そして、SwaggerのAPI定義のための仕様がLinux foundationに寄付され、OpenAPIに改名されたとのことです。

この記事が対象とする範囲

上記を踏まえ、この記事でやろうとしていること(そしてハマったこと)は、Rails + Grapeの構成で開発しているプロジェクトで、Grape SwaggerのDSLを使ってOpenAPI/Swagger仕様に沿ったJSONを生成することです。
また、その生成したJSONを読み込んで見やすくしてくれるツールで見たときに、意図した構成として表示されることを目指します(そしてハマります)

使用したgem

ドキュメント生成にあたり使用したgemは下記の通りです。

gem 'grape' # RESTful APIを開発するためのDSLを備えたフレームワーク
gem 'grape-swagger' # Grape APIからのドキュメント生成
gem 'grape-entity' # Grapeフレームワークにレスポンス整形のツールを加える
gem 'grape-swagger-entity' # grape-entityからのドキュメント生成

設定と使い方は各リポジトリをご参照のこと。
https://github.com/ruby-grape/grape
https://github.com/ruby-grape/grape-swagger
https://github.com/ruby-grape/grape-entity
https://github.com/ruby-grape/grape-swagger-entity

ハマったこと

1. descブロックのparamsと、descと同階層のparamsは別物

descと同階層のparamsではバリデーションをしてくれる

Grapeを使っているとき、下記のような書き方をすることがあると思います。
これはuser_nameは必須パラメータであることを指しており、実際にリクエストボディを空にしてリクエストを投げると、400エラーになってuser_name is missingとして返してくれます。

class Users < Grape::API
  resources :params_in_same_layer do
    desc 'descと同じ階層にparamsを書いた場合'
    params do
      requires :user_name, type: String, documentation: { desc: 'ユーザ名', type: 'string' }
      optional :address, type: String, documentation: { desc: '届け先情報', type: 'string' }
    end
    post do
      present hoge: 'fuga'
    end
  end
end

descブロック内のparamsではバリデーションはしてくれない

次にGrape Swaggerでドキュメンテーションしていこうとすると、下記のような記述方法が出てきます。
これにも先程と同様にリクエストボディ無しでリクエストを投げると、今度は400エラーにはならず201が返ってきます。

class Users < Grape::API
  resources :params_whitin_desc_block do
    desc 'descブロック内にのみparamsを書いた場合' do
      params SimpleUserParamsEntity.documentation
    end
    post do
      present hoge: 'fuga'
    end
  end
end

class SimpleUserParamsEntity < Grape::Entity
  expose :user_name, documentation: { desc: 'ユーザ名(エンティティで定義)', type: 'string', required: true }
  expose :address, documentation: { desc: '住所(エンティティで定義)', type: 'string' }
end

ただしUIで見ると見た目はほぼ同じ

上記をSwagger UIで見たときにどのような差があるか比べてみます。
どちらもユーザ名が必須であることは表現できています。しかし2つ目のエンドポイントでは実際には必須になっていないので注意が必要です。

image.png
image.png

両方にparamsを書いたら、キーが一致していればdesc内のドキュメンテーションが優先されるが、一致していないものはそれぞれ出力される

それでは両方に書くとどうなるでしょうか。さらにそれぞれでしか定義されていない、age、blood_typeというキーを追加してみました。

class Users < Grape::API
  resources :params_in_same_layer_and_desc_block do
    desc 'descブロック内と、descの同階層の両方にparamsを書いた場合' do
      params SimpleUserParamsEntity.documentation
    end
    params do
      requires :user_name, type: String, documentation: { desc: 'ユーザ名', type: 'string' }
      optional :address, type: String, documentation: { desc: '届け先情報', type: 'string' }
      optional :age, type: Integer, documentation: { desc: '年齢', type: 'string' }
    end
    post do
      present hoge: 'fuga'
    end
  end
end

class SimpleUserParamsEntity < Grape::Entity
  expose :user_name, documentation: { desc: 'ユーザ名(エンティティで定義)', type: 'string', required: true }
  expose :address, documentation: { desc: '住所(エンティティで定義)', type: 'string' }
  expose :blood_type, documentation: { desc: '血液型(エンティティで定義)', type: 'string' }
end

Swagger UIで見るとこうなります。キーが一致している場合は、ドキュメンテーションはdesc内のものが優先され、一致していない場合は、それぞれ出力されるようです。

image.png

2. 複雑な構成のリクエストをUIでちゃんと表示しようと頑張ったが、結局は使用するUIの挙動次第

複雑なパラメータのパターンでUIがどうなるか確かめてみます。またdescブロック内に記述した場合と同階層に書いたエンドポイントを用意します。

class Users < Grape::API
  resources :complex_params_in_same_layer do
    desc 'descと同じ階層にparamsを書いた場合'
    params do
      requires :user_name, type: Integer, documentation: { desc: 'ユーザ名', type: 'string' }
      optional :addresses, type: Array[JSON], documentation: { desc: '届け先情報', type: 'array', collectionFormat: 'multi' } do 
        requires :name, type: String, documentation: { desc: '届け先名', type: 'string' }
        requires :address, type: String, documentation: { desc: '届け先住所', type: 'string' }
        requires :tags, type: Array[JSON], documentation: { desc: 'タグ', type: 'array', collectionFormat: 'multi' } do
          optional :name, type: String, documentation: { desc: 'タグ名', type: 'string'}
        end
      end
    end
    post do
      present hoge: 'fuga'
    end
  end

  resources :complex_params_in_desc_block do
    desc 'descブロック内にのみparamsを書いた場合' do
      params ComplexUserParamsEntity.documentation
    end
    post do
      present hoge: 'fuga'
    end
  end
end

class ComplexUserParamsEntity < Grape::Entity
  class TagEntity < Grape::Entity
    expose :name, documentation: { desc: 'タグ名(エンティティで定義)', type: 'string' }
  end

  class AddressEntity < Grape::Entity
    expose :name, documentation: { desc: '届け先名(エンティティで定義)', type: 'string' }
    expose :address, documentation: { desc: '届け先住所(エンティティで定義)', type: 'string' }
    expose :tags, documentation: { desc: 'タグ(エンティティで定義)', type: 'array', is_array: true }, using: TagEntity
  end

  expose :user_name, documentation: { desc: 'ユーザ名(エンティティで定義)', type: 'string' }
  expose :addresses, documentation: { desc: '届け先情報(エンティティで定義)', type: 'array', is_array: true }, using: AddressEntity
end

Swagger UIで確認してみます。一方は配列内が表示されず、もう一方は表示されるけど階層構造がわかりにくい表示です。

image.png
image.png

見やすくできないかあがいてみます。全パラメータにparam_type: 'body'を追加してみます。

class Users < Grape::API
  resources :complex_params_in_same_layer do
    desc 'descと同じ階層にparamsを書いた場合'
    params do
      requires :user_name, type: Integer, documentation: { desc: 'ユーザ名', type: 'string', param_type: 'body' }
# 以下略

再びSwagger UIで確認します。すると、Entityで定義した方は相変わらず階層構造が失われています。Entityの書き方が悪いのかもしれません。
そしてもう一方は、階層構造がわかりやすくなりましたが、説明が反映されていません。

image.png
image.png

そこで、ふと他のOpenAPIを解釈できるUIを試してみます。使うのはこちら。
https://github.com/Redocly/redoc

Entityの方は相変わらずですが、説明がちょっとマシになっています。
そして同階層の方はなんと完全に思い通りの表示になっています!!(配列のキーにつけた「届け先情報」という説明もちゃんと表示されています)

image.png

image.png

このことからわかるのは、実際の表示は使用するUIツールによるので使用するツールでちゃんと表示されるようになることを確認しましょう、ということです。

3. exampleを出力するにはワークアラウンドが必要

exampleを指定しても出力されないのでなぜだろうと思ったら下記のissuesがありました。
現時点でOpenなので解決されるまではissues内に投稿されているワークアラウンドを入れればできるようです。
https://github.com/ruby-grape/grape-swagger/issues/762

おわりに

リクエストパラメータのところのエンティティの使い方なんかはまだちゃんと理解できていないので間違い等あったらご指摘いただければ助かります。

8
2
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
8
2