committeeを使ってOpenAPI3のスキーマファイルからリクエスト&レスポンスをバリデーションする方法と実際の開発に組み込む際の設定についてまとめます。
OpenAPI3とcommiteeについてはota42yさんの登壇資料が参考になります。また、committeeについての記事はいっぱいあるので、ここでは実際の開発で取り入れるにあたって設定ファイルの書き方に絞って紹介したいと思います。
committeeを使って実現したいこと
- API仕様書を作りたい
- スキーマファーストな開発をして、常に正しい状態にメンテナンスしていきたい
- リクエストのバリデーションなどのRailsが苦手な部分を楽に実装したい
committeeを使う理由
- すでに運用中のサービスに途中から組み込める
- RSpecだけで実行したり、実際のリクエストのバリデーションに使ったりと用途に合わせて組み込みやすい
- 使い方がシンプルで導入が簡単
準備物
- OpenAPI3で記載したスキーマファイル(今回は準備されている前提)
スキーマファイルの管理は手動で管理していく方法とコードから自動生成する方法があります。前者の場合は、Stoplightのようなツールを使えば新しい人のキャッチアップも楽です。後者だとswagger-blocksのようなgemがありますが、OpenAPI3のサポートはまだ実装まで行っていないようです。 コードから自動生成の方がスキーマファイルと実際のコードとの差分が生まれないため、メンテナンスが楽になりますが現状良いgemが見つからなかったので自分は手動で管理しています。
- Gemfile
今回使うgemは以下です。
gem 'committee'
group :test do
gem 'committee-rails'
end
committeeをテストにだけ利用する場合は、:test
配下に書いて問題ないです。
rspecで利用する
committeeをrspecに導入するためにはcommittee-railsが便利です。
rspec_helper.rb
等に以下の設定を入れれば使えるようになります。
RSpec.configure do |config|
config.add_setting :committee_options
config.committee_options = { schema_path: Rails.root.join('schema', 'schema.json').to_s, old_assert_behavior: false }
end
assert_schema_conformを実行すればスキーマを使って検証してくれます。
describe 'request spec' do
include Committee::Rails::Test::Methods
describe 'GET /' do
it 'conform json schema' do
get '/'
assert_schema_conform
end
end
end
設定でold_assert_behaviorをtrueにした場合にはレスポンスしかチェックされずwarningが出る(参照)のでfalseで設定しておくと良いと思います。
全てのrequest specでテストを実行する
常に最新の仕様にスキーマファイルを合わせるために、全てのrequest specで強制的にassert_schema_conformを書くには以下のように設定すれば可能です。
RSpec.configure do |config|
config.include Committee::Rails::Test::Methods
config.add_setting :committee_options
config.committee_options = {
schema_path: Rails.root.join('schema', 'schema.json').to_s,
old_assert_behavior: false
}
config.after(:each) do |example|
next unless example.metadata[:type] == :request
assert_schema_conform unless RSpec.configuration.skip_assert_schema_conform
end
config.before(:each) do |example|
skip_flag = example.metadata[:skip_assert_schema_conform] || false
config.skip_assert_schema_conform = skip_flag
end
end
こっちの記事のようにActionDispatch::Integration::Session
にpretendする方法もありますが、このcommitで依存関係が崩れる1ようになってpretendではエラーが出てしまうため、afterで実行するようにしています。
もし、assert_schema_conformをスキップしたいテストがあった場合には、下記のように記載してスキップ可能です。
describe 'GET /', type: :request, skip_assert_schema_conform: true do
it 'skip conform schema' do
get '/'
end
end
このように設定しておけば、スキーマファイルを書き忘れた場合や、スキーマファイルの値が間違えていた場合などにCIで気づくことができ、常に最新の仕様を反映するように強制することができます。
実運用のサービスで使う
committeeではリクエスト&レスポンスのバリデーションが可能です。これを使えばRailsのController側で細かなparamsのチェックをしなくてよくなるので、テストだけでなく実際のリクエストにも適用するのがおすすめです。
設定は下記の通りで、config/initializers/committee.rb
などのファイルに記述します。
return if Rails.env.test?
file = YAML.load_file(Rails.root.join('schema', 'schema.yml'))
open_api = OpenAPIParser.parse(file)
schema = Committee::Drivers::OpenAPI3::Driver.new.parse(open_api)
Rails.application.config.middleware.insert_before(
ActionDispatch::Executor,
Committee::Middleware::RequestValidation,
error_class: Custom::RequestValidationError,
schema: schema,
strict: !Rails.env.production?
)
Rails.application.config.middleware.insert_after(
ActionDispatch::Callbacks,
Committee::Middleware::ResponseValidation,
error_class: Custom::ResponseValidationError,
schema: schema,
validate_success_only: true
)
rspec実行時にも読み込まれるファイルなので、 return if Rails.env.test?
してますが他にいい方法があれば知りたい...
各オプションについては Committee::Middleware::RequestValidationを参考にしてください。上記ではstrictを本番だけOFFにすることで、開発環境では厳しくジャッジして本番では緩く運用する設定になっています。本番ではどのようなリクエストが来るのかわからないので、ある程度緩く運用したいケースもあるかと思います。
他にはエラー時にcommittee独自のエラーが返ってしまうので、独自のエラークラスを定義した方が良いと思います。
開発中はrspecや動作確認でスキーマファイルの正当性を担保できるので、両方でcommitteeを動作させた方が良いかなと思います。また、本番で動作させることである程度サービスへの攻撃の防御や検知に使えるかなーと思いました。
-
undefined local variable or method 'integration_session' for #<#<Class:0x000056477f0bcee0>:0x00005647831c1fd0>
のエラーが出るようになる。integration_sessionはActionDispatch::Integration::Session
を呼び出しているActionDispatch::Integration::Runner
側で定義されているメソッドなので参照できない。 ↩