最強のOpenAPI TypeScript Code Generatorを作った(Playgroundを追記)
自称最強(震え)のジェネレーターを作りました。
追記
- 2020/01/13 12:00頃
- かなり雑ですがPlaygroundを作りました。Remoteの
$ref
は使えません。 - 例3を追加しました。
- かなり雑ですがPlaygroundを作りました。Remoteの
作ったやつ
- https://github.com/Himenon/openapi-typescript-code-generator
- https://www.npmjs.com/package/@himenon/openapi-typescript-code-generator
モチベーション
OpenAPI Specification(以下 OAS)からTypeScriptのコードを生成するためにはいくつかのライブラリが存在したので最初はこれらを使ってみました。
- https://github.com/OpenAPITools/openapi-generator
- https://github.com/ferdikoomen/openapi-typescript-codegen
- https://github.com/Microsoft/typed-rest-client
- https://github.com/mtennoe/swagger-typescript-codegen
しかしながら、複雑度が増すとうまく機能しない点が出てきました。
OASがシンプルなうちは小回りが効くので良かったのですが、分割したファイル数が50を超え、
それらが一つの型定義に集約されるとその型定義がとても使いづらくなる問題が発生しました。
また、特定のライブラリに依存したAPI Clientを生成するものがほとんどで、実際のプロジェクトに導入するときの明らかな障害になります。
fetchで統一してるのに、axiosいれる?とかその逆も然り。
さらに、自作のテンプレートが書けるとのことで実際に書いてみると、API Clientの部分的な変更や、Mustash記法などさまざまな形式で提供されているのでこれに対する学習コストが高く、保守コストの観点から流布させるのがとても難しかった。
だから作った。というのが今回のモチベーションです。
設計コンセプト
GitHubのREADMEにも書いてますが、以下を指針としてライブラリを設計して開発しました、
- 型定義ファーストであること
- 型定義に実体が含まれないこと(型定義部分を
.js
に変換したとき、ファイルサイズが 0 になること) - ディレクトリ構造が型定義の構造に写像されること
- どの API クライアントライブラリにも依存しないこと
- TypeScript AST による拡張ができること
- OpenAPI の仕様に準拠すること
- 1 ファイル化することにより、ポータビリティを保つこと
これを実現させるために、TypeScript ASTをフルに利用してCode Generatorを作成しました。
TypeScript ASTを利用すると、oneOf
やallOf
を共用型は交差型に変換することも(かなり)容易でした。
また、ディレクトリ構造をnamespace
へと変換し、その構造を型定義の構造へ写像することによって参照構造がかなり明確になりました。
この参照解決の実装に関してはかなり労力を割いて作ったので、ぜひ試してほしい機能の一つです。
具体例
2つほど例を示しておきます。実際に使ってみると威力がわかるかと思います。
(すくなくとも記事中で説明すると半端ない量になるのでドキュメント化すると心が折れます)
例1 - oneOf
の例
# spec.yml
components:
schemas:
OneOfType:
oneOf:
- type: string
- type: number
- type: object
変換後
export namespace Schemas {
export type OneOfType = string | number | {};
}
例2 - 階層構造の例
# spec.yml
components:
schemas:
RemoteRefString:
$ref: "./components/schemas/Level1/RemoteBoolean.yml"
# ./components/schemas/Level1/RemoteBoolean.yml
type: boolean
変換後
export namespace Schemas {
export namespace Level1 {
export type RemoteBoolean = boolean;
}
export type RemoteRefBoolean = Schemas.Level1.RemoteBoolean;
}
例3 - 大きめの実装例
PlayGroundに書きました。
制限
ディレクトリ構造を実装に落とし込むことによって制限が生じています。
TypeScriptのnamespaceの宣言名で利用できるものしかディレクトリ名に利用できなくなります。
この点の自由度はマイナスポイントですが、名前空間を受動的に整理される利点があります。
もしかしたら、TypeScript内で利用できない命名はvalidationで弾いたり、camelCaseに変換するなど、内部実装を柔軟に変更する方法もあるかもしれません。
ほかにも制限があるので、気になる人はREAMDEを見てみてください。
テンプレート機能について
TypeScript ASTを利用してコードを拡張できるようにしています。一見すると難しいのですが、ts-ast-viewerというとても優秀なツールを利用することで、ASTの利点を瞬時に享受することができます。
出力する言語の実装を、同じ言語で変更できるので学習コストが低く、ts-ast-viewerのようなPlaygroundがあることで最初の第一歩が踏み出しやすく、拡張の自由度が担保できていると考えています。
依存関係の注入
デフォルトのAPI Templateは依存関係の注入を行う様になっています。
また、QueryParmaeterのフォーマットも外側で行うようにしており、実質的な処理が記述されていません。
Parameter系はさまざまなフォーマットがあり、これに関しては別のライブラリとして切り出しています。
サンプルプロジェクトにこれらの使い方が書いてあるので、ぜひ見てください。
今後について
自分が気づいた範囲で修正入れていく予定ですが、全てを網羅している自信は全くありません。自分で作っておいてアレですが、$ref
の解決アルゴリズムはうまく働いていますが、再帰的に動いているためすべてを網羅しているかどうか、を担保しかねます。
もし見つけた場合はバグレポートをissueの方に投げてほしいです。
また、実装に対してコントリビューションしたい場合はここをまずは読んでください。
リポジトリにスターを付けるとモチベーションが大変上がります。