前置き
OpenAPIスキーマからAPIクライアントを作りたい。そういう場合 OpenAPITools/openapi-generator を使って生成することになるが、スキーマがあまりにもでかすぎると困る。
- yamlだとファイルがでかすぎるとうまく読めない
- Pythonのようなインタプリタ型のクライアントだとモジュールのロードに時間がかかる
一例として、以下のAPIスキーマを見てほしい。以下はネットワーク仮想化製品であるVMware NSX-TのAPIスキーマであるが、nsx_policy_api.yml を見ると、テキストファイルなのに12MGもある。jsonファイルですら8.6MBである。
こういった外部製品のAPIクライアントを作ることを考えたとき、実際には特定のAPIしか叩かないだろう。もしその特定のAPIのスキーマと関連リソースだけを抽出したスキーマを作れば、軽量なAPIクライアントが作成できることが期待される。
というわけで本記事では、OpenAPIのスキーマから必要なリソースと関連リソースだけを抽出した新しいスキーマを生成するスクリプトを作成する。
設計
今回の目的は「特定のAPIと関連リソースのみを抽出する」ことである。
入出力
そのため、入力と出力はそれぞれ次のようになる。
- 入力:
- OpenAPI定義ファイル
- OpenAPI定義ファイルに含まれるAPIパスのリスト
- 出力: OpenAPIスキーマ
- 最もファイルサイズを占めているであろう以下の属性には必要最低限のものが入っている状態となっている
pathsdefinitions(v2.0のみ)parameters(v2.0のみ)responses(v2.0のみ)components(v3.0のみ)
- OpenAPI には2.0と3.0があるが、特に取り決めない
- 最もファイルサイズを占めているであろう以下の属性には必要最低限のものが入っている状態となっている
インターフェース
APIパスのリストは、改行区切りで標準入力から受け取るものとする。
| |
API関連リソースを抽出するロジック
OpenAPIでは、$ref: "#/{attr...}" のような構文で、別の場所で定義されている要素を参照できる。
| |
このような $ref: ... があるため、完全なAPIスキーマとして抽出するためには、$ref が参照する先のリソースも漏らさずスキーマとして抽出する必要がある。
$ref: "#/key1/key2/... という構文自体は JSON Reference、 JSON Pointer で定義されている仕様らしい。しかし今回はそこまで頑張って規格どおりにパースする必要はないので、単に、
$ref: "#/key1/key2/...が見つかった場合、d[key1][key2][...]...を参照する
という処理を入れて取り込んでいく。処理のイメージとしては以下である。
- APIパスの要素を走査していく
$refをキーとする要素が見つかった場合、それが参照している要素を走査- さらにその中で
$refをキーとする要素が見つかった場合、さらに参照している要素を走査…
返却値としては、APIパスから到達可能なオブジェクト、またはJSON Pointerの集合を返せばよい。これはいわゆる NixでいうClosure(閉包) を求める問題に帰着される。
なんだか面白くなってきたので、もう少しちゃんと定式化してみる。
| |
ここまでいくとあとはほとんど実装するだけとなる。
実装
API関連リソースを抽出するロジックの実装
まず閉包を求めるロジックは以下のように書ける。
| |
テストコード
| |
CLIの実装
閉包を求める関数を用いて、実際にOpenAPIスキーマを抽出するCLIを書いてみよう。
| |
動作確認
抽出スクリプトの実行
APIのパスのリストをファイルに書いておいて、それを食わせて生成する。
| |
8.5Mのjsonが89Kまで小さくなった。またjqで中身をのぞいてみても、関連のAPIとそのリソースしか無さそう。
| |
openapi-generator-cliを用いたpythonクライアントの生成
試しに openapi-generator-cli でクライアントを生成できるか試してみる。
| |
| |
READMEを見ると、ちゃんと指定したAPIパスに関するメソッドだけが定義されていることがわかる。
※ 見やすさのため glow で出力している
| |
(おまけ)便利CLIの実装
本筋とは関係ない便利CLIコマンドの実装。
APIメソッドの一覧を出力
APIメソッドの量が多すぎてdocsから探すの大変、という場合に使えそう。
| |
実行例。
| |
複数のAPIをマージする
同じエンドポイントだが、basePathによって利用するAPIが分かれており、かつOpenAPI定義も別々のファイルに分かれている。でも同じAPIクライアントとして作りたいから、それらをマージして1つのOpenAPI定義として作りたい。
例えばNSX-Tの例でいうと、
/api/v1でコールするAPIはnsx_api.json/policy/api/v1でコールするAPIはnsx_policy_api.json
と分かれている。これをまとめたOpenAPIスキーマを作成して1つのクライアントを作りたい。
そこで、以下の方針でマージするコマンドを作ってみる。
- basePathを
pathsのほうに寄せる - dictやlistの場合は要素をマージする。そうでない場合は上書き
- 同名のリソースが存在した場合、同じ構造かどうかのチェックをする
- ただし
infoは例外的に上書きしてしまう
| |
これで実行してみると、 /api/v1 付きのものと /policy/api/v1 付きのものが混合して出力されていることがわかる。
| |
おわりに
OpenAPIスキーマをこねて必要最低限だけ取り出すスクリプトを実装した。
正直こんなの最近なら生成AIで一瞬で生成してもらえるが、今回みたいな「閉じた構造を生成する」という観点は別にOpenAPIの話に限らず、知的な面白さがあったので自分で考えて手で実装した。