hiring!はじめに前回の記事では、OpenAPIで新しいウェブAPIを定義する際に、yamlのままで読みやすいようにファイル構成等を工夫した話をしました。今回はそのAPIスキーマを使って、Railsでスキーマ駆動開発を実現するにあたって利用しているツール類についてお話しようと思います。合わせて、その中での利用方法の工夫や、問題となったツールの不具合等についても共有いたします。前回に引き続き、APIGatewayと教材管理サービスの間、バックエンド内部の通信を対象としています。APIGateway(クライアント側)と教材管理サービス(サーバー側)は共にバージョン5系のRailsアプリです。4つのGitレポジトリを使って開発します。サーバー側Railsアプリクライアント側RailsアプリOpenAPIドキュメントopenapi-generatorが生成するAPIクライアントgemそれぞれのレポジトリと利用しているツールの構成の概観はこのような感じです。サーバー側RailsアプリサーバーAPIはgrapeを利用して書かれており、レスポンススキーマの整形にactive_model_serializerを利用しています。Submoduleとして持ちます。クライアント側Railsアプリクライアント側は、後述するように、open-api-generetorで生成されたAPIクライアントgemを使ってサーバーにアクセスします。APIクライアントgemは、通常のgemと同様に、Gemfileを通じて取得されます。開発の流れ現在、OpenAPI関連の開発は次のようなステップで進められています。スキーマ駆動開発を採用しているので、両者は並行して進めることが可能です。OpenAPIドキュメントのレポジトリで、API定義を修正するPRを作成し、レビューの後、masterブランチにマージします。PRが作成されたタイミングのCIで、swagger-cliによるスキーマ定義のバリデーションが行われます。(バリデーションに失敗したらマージ不可)PRがマージされたタイミングのCIで、swagger-cliによって全てのスキーマ定義ファイルが単一のJSONファイルに連結され、作成されたファイルがmasterブランチにコミットされます。PRがマージされたタイミングのCIで、openapi-generatorによってAPIクライアントgemが生成され、別途存在するgem用レポジトリのmasterが更新されます。(ここからサーバー側の実装)APIレスポンスのコンポーネント単位のスキーマがOpenAPI定義に沿うように、active_model_serializerのクラスを実装します。実装したクラスは、json-schemaのgemを使ったテストコードでチェックされます。active_model_serializerのクラスを使って、APIエンドポイントを実装します。実装したエンドポイントは、committee-railsを使ったテストコードでチェックされます。(ここからクライアント側の実装)openapi-generatorで生成されたAPIクライアントgemのモデルクラスに対して、APIスタブ用のfactory_botを定義します。出力がOpenAPI定義に反していないことを確認するために、factoryに対するテストコードを書いています。検証にはAPIクライアントgemのモデルクラスの機能を利用しています。その他必要なクライアント側の実装を行います。サーバーへのアクセスには、openapi-generatorの生成したAPIクライアントgemを利用します。クライアント側のテストコードでは、上記のfactoryを使ってAPIレスポンスのスタブを組み立てます。(ここまでクライアント側の実装)利用ツールswagger-cligithub.comOpenAPI関連のツールは、ものによってyaml形式やファイル分割への対応状況がまちまちなので、どんなツールでも利用可能な単一のJSONファイルの形式に変換するために採用しました。スキーマ定義がOpenAPI仕様に沿って書かれているかのバリデーション機能も備えているので、それも合わせて利用しています。他のツールに比較して、間違っている箇所が把握しやすいエラーメッセージが出力されるように感じます。公式のdockerイメージが配布されており、これを利用することで上記のような各タイミングでCIで自動実行することが簡単にできます。committee-railsgithub.comスキーマ駆動開発の実現のため、可能ならばサーバー側もコードの自動生成を利用したかったのですが、Rails対応で良い感じのものが見つかりませんでした。OpenAPIで広く行われているように、自動テストにCommitteeを用いたスキーマチェックを導入する形でサーバーの実装がOpenAPI定義に沿っていることを担保するようにしました。Submoduleとして持っています。次のようなモジュールを作り、スキーマチェックを行う各specファイルでincludeすることで、自動でファイルパスなどの設定が行われるようにしています。endendスキーマ定義のパース周り2でOpenAPI仕様をカバーしていない部分も多く、次のような不具合に悩まされました。ファイルの分割hachi.hatenablog.comこちらのブログで報告されているように、分割したファイルへの$ref参照周りにバグがあります。同様の問題がgithubのissueで報告されており、こちらでも、上記のブログ記事のように、参照先のファイルをpathsやcomponentsのようなトップレベルから書くことで解決できるとされています。それだとファイルを分割したメリットがだいぶ失われてしまうので、今回は前述のように、swagger-cliで連結したJSON形式の定義ファイルを用いることでテスト時にファイル結合を行う必要がないようにしています。$refとnullableの同時使用$refで参照したスキーマを参照元でnullableにする方法について、OpenAPI仕様では明示的な定められておらず、実現には一工夫が必要となります。nullのスキーマ定義が認められていません。trueと同時に利用しても、nullは許容されません。nullを使わずにnullしか設定できないスキーマを定義してanyOfで並列させる記法の、大きく2つがあります。[null]しかし、Committeeは前者のallOfで包む記法ではnullを許可してくれません。利用するツールがCommitteeだけであれば、後者の方法を使えば良いだけの話ですが、OpenAPIGeneratorには後述のanyOfに関するバグがあったので、ダブルバインドに陥ってしまいました。json-schemagithub.comレスポンスのスキーマチェックは前述のCommitteeで一通りできるのですが、せっかくコンポーネント構成を工夫したAPI定義をしたので、コンポーネント単位でもスキーマチェックができるようにしています。具体的には、前述の開発の流れにあるように、OpenAPIで定義されているコンポーネントの単位でactive_model_serializerのクラスが実装されるので、それに対するユニットテストでOpenAPI定義を利用できるようにします。狭いスコープのテストでスキーマチェックを行うことによって、例えばnil/非nilの場合わけや各enum値が全て正しく出力されるかなど、細かい条件での動作確認がやりやすくなっています。Schemaの検証用gemであるjson-schemaを使ってスキーマ定義と実際の出力の照合を行います。OpenAPI定義のルートにcomponentディレクティブを置かなかったために結合されたJSON形式の定義ファイルから目的のコンポーネントを見つけ出すのが難しかったので、こちらの検証ではJSONファイルではなく、結合前のYAML形式のコンポーネント定義ファイルを直接参照しています。Schemaの記法とは異なるところがあるので、その差異を吸収するために、gemで推奨されている方法で、次の2つの点で、スキーマ定義クラスを拡張しています。trueの形式でnull許容を設定可能にする$refで参照先を指定するときに、参照元ファイルからの相対パスで指定できるようにするただし後者については、参照元ファイルのファイルパスを動的に取得する手段が見つからなかったので、「コンポーネント定義ファイルは全てcomponents/ディレクトリ以下にフラットに配置される」というローカルな定義ルールを利用して、ちょっと強引に実現させました。拡張したスキーマ定義はCommitteeと同様に専用モジュールを用意し、assert_valid_json_schemaという独自のアサーションメソッドを使って簡単にテストが書けるようにしています。client)github.comAPIクライアントの実装は、OpenapiGeneratorが生成するRubyクライアントが本番で利用可能な水準だったので、それを使っています。生成されるコードは.gemspec形式のファイルを含めたgemの形式で出力されるため、OpenAPI定義を管理する場所とは別に新しくGitレポジトリを作り、通常のgemと同じようにGemfileで指定する形で各アプリケーションで利用できるようにしました。開発の流れの通り、gemレポジトリの更新はCIによって自動化されており、特に意識して管理する必要がないようになっています。機能面ではオープンタイムアウトの設定と結果のキャッシュ関連が求めている内容に対して不足していたので、アンチパターンになりますが、生成されたクライアントクラスを継承を使って元クラスの実装の詳細に介入する形で拡張しています。生成されたクライアントを使うにあたっては、次のような不具合に悩まされました。中途半端な型チェック生成されたRubyクライアントは型チェック機能が備わっており、サーバーからのレスポンスがスキーマ定義に沿っていない場合には例外が発生するようになっています。型チェックの実装箇所が少し特殊で、モデルクラス単体で利用したときに型チェックの恩恵を十分に受けるには実装方法に少々工夫が必要となっています。build_from_hashというクラスメソッドを使う」という3つの方法が用意されているのですが、クラスメソッドを使う3番目の方法以外では、セットされる値の検証をしてくれないのです。値をセットした後に検証するためのvalid?のようなメソッドも用意されていますが、nullや最大最小のチェックしかやってくれず、例えばオブジェクト型のプロパティに文字列がセットされていても異常を検知してくれません。APIサーバーからのレスポンスをパースする際にはbuild_from_hashというクラスメソッドが使われており、これをつかって値をセットしたときにしか、型やenumの値の正しさが検証できないようになっています。oneOf/anyOfに非対応anyOfやoneOfを使ったスキーマ定義すると、AnyOfXXXYYYやOneOfXXXYYYのような名前の存在しないクラスを参照するコードが生成されてします。XXXやYYYはanyOfやoneOfで組み合わされた型に応じたクラス名なので、対応するクラス定義を生成するコードが未実装なのでしょうか。anyOfが使えないため、前述の方法でCommitteeに$refの参照先をnull許容させることができず、結果的に$refの参照先をnullableにすること自体を諦める結果になりました。factory_botgithub.com一般的には、ActiveRecordのモデルクラスに対してテスト用のデータを生成するために使用されますが、ここではopenapi-generatorが生成したAPIレスポンスのモデルクラスに対して使用し、アプリケーション内部での自動テストのためのデータの生成にも使用しています。前述の通りopenapi-generatorが生成するモデルクラスは型のサポートが不十分で、普通にfactoryを定義するだけだとOpenAPI定義の更新への追随を忘れ、facotoryと実際のデータに乖離が発生するおそれがあるので、変則的ですが、factoryに対しても下のようなRSpecのテストを書いて、factoryのデフォルトがOpenAPI定義に沿っていることを確認できるようにしています。endend終わりにOpenAPIを用いたスキーマ駆動開発に挑戦してみて、gRPC・GraphQLという他技術と比べたときのエコシステムの弱さを正直感じました。元はAPI仕様の記述を目的として作られた背景があるためか、ドキュメント生成関連の充実ぶりに比べて、コード自動生成関連のツールが少なかったです。サーバーのコード生成についても、スタブとして開発中に一時的に利用するためのものはそれなりにある一方で、定義修正と実装のサイクルを繰り返せて行けそうなツールはほとんど見つかりませんでした。5開発主体についても、OpenAPIのスキーマ仕様については大きなコミュニティが存在しているものの、ツールの開発はほとんどが個人レベルに任されており、GoogleやMetaのような巨大企業が開発したgRPC・GraphQLと比べると保守体制が弱いものが多いように感じます。3.1が公開されて1年以上経つのにそれをサポートしているツールがほとんどない点と、利用するツールよって定義の書き方の細かい部分を調整する必要が出てくる点が特に辛いところです。JSON形式のレスポンスを用いるシンプルさと、サーバー側・クライアント側共に既存の実装を大きく変えることなく導入できる点は、gRPCやGraphQLには真似できない大きな強味なので、OpenAPI関連のエコシステムがもっと発展し、スキーマ駆動開発を導入への障壁が下がって広く導入されていって欲しいと思います。hiring!株式会社ドワンゴの教育事業では、一緒に未来の当たり前の教育をつくるメンバーを募集しています。カジュアル面談も行っています。お気軽にご連絡ください!カジュアル面談応募フォームはこちらwww.nnn.ed.nico開発チームの取り組み、教育事業の今後については、他の記事や採用資料をご覧ください。nullの利用が可能となりましたが、3.1に対応済みのツールがまだほとんどないのが現状です。別言語になりますが、Goのopenapi-codegenというツールは、インターフェース定義を中心に生成される形になっており、欲しかったものに近いと感じました。(3)引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。