この記事は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つ目のエンドポイントでは実際には必須になっていないので注意が必要です。
両方に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内のものが優先され、一致していない場合は、それぞれ出力されるようです。
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で確認してみます。一方は配列内が表示されず、もう一方は表示されるけど階層構造がわかりにくい表示です。
見やすくできないかあがいてみます。全パラメータに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の書き方が悪いのかもしれません。
そしてもう一方は、階層構造がわかりやすくなりましたが、説明が反映されていません。
そこで、ふと他のOpenAPIを解釈できるUIを試してみます。使うのはこちら。
https://github.com/Redocly/redoc
Entityの方は相変わらずですが、説明がちょっとマシになっています。
そして同階層の方はなんと完全に思い通りの表示になっています!!(配列のキーにつけた「届け先情報」という説明もちゃんと表示されています)
このことからわかるのは、実際の表示は使用するUIツールによるので使用するツールでちゃんと表示されるようになることを確認しましょう、ということです。
3. exampleを出力するにはワークアラウンドが必要
exampleを指定しても出力されないのでなぜだろうと思ったら下記のissuesがありました。
現時点でOpenなので解決されるまではissues内に投稿されているワークアラウンドを入れればできるようです。
https://github.com/ruby-grape/grape-swagger/issues/762
おわりに
リクエストパラメータのところのエンティティの使い方なんかはまだちゃんと理解できていないので間違い等あったらご指摘いただければ助かります。