committeeのOpenAPI 3対応がでました

committeeのOpenAPI 3対応がでました

committee 3.0.0をリリースしました]。 このバージョンは主にOpenAPI 3対応が入っており、OpenAPI 3の定義ファイルを利用してリクエスト・レスポンスのバリデーションができます。 また、committee-railsも最新のバージョンで3.0.0に対応してるため、railsを利用している人はこちらも導入すると便利に扱えます。

OpenAPI 3を使う場合は様々なオプションが推奨設定になっているため、初めて使う場合はフルにその恩恵を受けられるはずです。

JSON Hyper-SchemaからOpenAPI 3への移行

JSON Hyper-SchemaからOpenAPI 3に上げる場合は変換スクリプトがあるのでこれを使うと便利です。 ただし、仕様上完全に変換することができないため、手で書いた場合に比べて冗長だったり、一部のnull非許可の設定がうまく変換できないことがあります。 また、パスパラメータなどはHyper-Schema上に情報がそもそもないため、追加で登録する必要があったりと、結構癖が強い変換スクリプトになっています。

READMEに書いてありますが、Hyper-SchemaやOpenAPI 2とOpenAPI 3とではデフォルト設定が大幅に変わっています。 committeeは元々Hyper-Schemaベースなのでそのデフォルト設定だとOpenAPI 3の一部の定義を考慮できません。OpenAPI 3を導入する場合は基本的に新規導入のため、最大限利用できるおすすめの設定をデフォルトにしています。 ただし、OpenAPI 3を使わない人に変更を強要するのは避けたいため、2系の最新版との非互換の変更は起きないようになってます。そのため、一度2系の最新版に上げてdeprecatedになったオプション等を修正した状態で3系に上げると安全に上げられます。

実装の裏話

実装時に気になった点や、苦労した点がいくつかあります。

JSON Hyper-SchemaとOpenAPI 3の共存

committeeはOpenAPI 2をサポートしていますが、内部ではJSON Hyper-Schemaの形に変換しています。そのため、committeeの内部はHyper-Schemaの定義ファイルしか想定していませんでした。
https://github.com/interagent/committee/blob/master/lib/committee/drivers/open_api_2.rb#L181

OpenAPI 3はHyper-Schemaに比べてもう少し表現力が高く、かつ定義が厳密になっていたりとだいぶ違うため、変換するのではなくきちんとした処理を書いて共存するようにしました。 具体的にはrack middlewareの層は共通にし、そこから 現在読み込んでいる定義に対してバリデーションを実行する という風に抽象化しました。

例えば以下がHTTP requsetに乗っているデータをバリデーションするファイルです。 このファイルの2系では様々なバリデーションの処理が直接書かれていますが、3系では現在の定義に対応するクラスを取得し、そのクラスのバリデーションメソッドを呼ぶだけになっています。
committee 2系
https://github.com/interagent/committee/blob/2-x/lib/committee/middleware/request_validation.rb#L26
committee 3系
https://github.com/interagent/committee/blob/master/lib/committee/middleware/request_validation.rb#L13

各定義(Hyper-Schema/OpenAPI 3)では、それぞれ全く違う処理を実行していますが、rack middlewareの層からは抽象化されているため、同じメソッドで実行出来るようになっています。

この方針でやったところほぼJSON Hyper-Schema側に影響を与えることなく実装できましたし、 また、3系の開発中にいくつか来た2系への修正や、互換性を保つための変更がほとんど被ることが無かったため、長期の実装にしては衝突が少なめに抑えられて良い結果になったと思います。

怖くないバージョンアップ

committee 3系ではメジャーバージョンアップということもあり、deprecatedなメソッドを一層しました。また、前述のように互換性を維持していますが内部的には大きく変わっているため、JSON Hyper-Schemaのみを想定したものなど、いつくかで廃止しないと問題が起きる部分がありました。

しかし、いきなり3系で大量の変更が必要となるとバージョンアップの難易度が大きすぎて大変です。 そのため互換性を崩すような変更が発生した場合は、2系に対してdeprecated警告と2系でも3系でも問題なく動く機能を提供し、2系で警告をすべて消した状態にすると3系でも問題なく動くような状態を目指しました。

また、deprecatedにする量が大きかった初期化部分では、既存機能より良い初期化オプションを2系に新規に追加し、修正時に良いコードに出来るようにしています。
(ファイルパスを指定すると読み込み、定義の種類を自動で判別する)
https://github.com/interagent/committee#setting-schemas-in-middleware

OpenAPI 3の処理の部分を切り出し

committeeのJSON Hyper-SchemaではHyper-Schemaのバリデーションは別gemで行っています。 これに倣ってOpenAPI 3のgemを作り、そちらでバリデーションを行うようにしました。 既存のgemを使う選択肢もありましたが、データのバリデーションを行ってくれるものはなく、committeeではパラメータの変換も行うため独自に作成しました。 (request parameterは文字列になるが、定義上integerならcommitteeではintegerに変換する処理を行っている)

また、OpenAPI 3にはヘッダの定義も含まれるため、パラメータ以外のバリデーションもgemの方でを行い、committeeはgemに適切なデータを渡すだけになっています。 OpenAPI 3に関する処理はgemで、リクエストパラメータやオプションを考慮する処理はcommitteeで行いgemに適切に渡すだけと、きれいに役割が分けられ安定的になったと思います。

今の所予定には無いですが別の方式でOpenAPI 3のバリデーションを行うような場合もこのgemを利用できるので、かなり良い方針だったと思います。

まとめ

今回のcommitteのメジャーバージョンアップでは、既存ユーザへの影響は最小限に、かつOpenAPI 3を使う場合はデフォルトで最高の状態を得られるように目指しました。
手元でいくつかアップデートした限りではそこそこうまく行っているのですが、他の人がやった場合にどう感じるかは未知なので試していただけると幸いです。