Skip to main content

OpenAPI TypeScript Code GeneratorをASTを利用して作成しました

CHANGELOG

  • 2021/01/13 12:00頃
    • Playgroundを作りました。Remoteの$refは使えません。
    • 例3を追加しました。
  • 2021/02/06 19:00頃
    • github/rest-api-descriptionのOpenAPI Schemaを改変せずにTypeScriptのコードを生成できるようになりました。例4に追加しました。
  • 2021/03/24 13:30頃
    • 既存のOpenAPI Schemaのうち利用するoperationIdのみを選択できるようにオプションを追加しました。
    • 同期的なAPI Clientを生成するためのフラグを追加しました。Google App Scriptなどの利用シーンで有用です。
  • 2022/03/21 16:00頃
    • 例4を修正。有名なシステムやライブラリのOpenAPIから生成したTypeScriptのサンプルコードのリポジトリを追加しました。
    • 「今後について」を更新しました。

作ったCode Generator

開発のモチベーション

OpenAPI Specification(以下 OAS)からTypeScriptのコードを生成するためにはいくつかのライブラリが存在したので最初はこれらを使ってみました。

しかしながら、複雑度が増すとうまく機能しない点が出てきました。

OASがシンプルなうちは小回りが効くので良かったのですが、分割したファイル数が50を超え、 それらが一つの型定義に集約されるとその型定義がとても使いづらくなる問題が発生しました。

また、特定のライブラリに依存したAPI Clientを生成するものがほとんどで、実際のプロジェクトに導入するときの明らかな障害になります。 fetchで統一してるのに、axiosいれる?とかその逆も然り。

さらに、自作のテンプレートが書けるとのことで実際に書いてみると、API Clientの部分的な変更や、Mustash記法などさまざまな形式で提供されているのでこれに対する学習コストが高く、保守コストの観点から流布させるのが困難でした。

このため、拡張性がしやすくTypeScriptユーザーが保守しやすいジェネレーターを作ろう、というのが今回のモチベーションです。

設計コンセプト

GitHubのREADMEにも書いてますが、以下を指針としてライブラリを設計して開発しました、

  • 型定義ファーストであること
  • 型定義に実体が含まれないこと(型定義部分を.jsに変換したとき、ファイルサイズが 0 になること)
  • ディレクトリ構造が型定義の構造に写像されること
  • どの API クライアントライブラリにも依存しないこと
  • TypeScript AST による拡張ができること
  • OpenAPI の仕様に準拠すること
  • 1 ファイル化することにより、ポータビリティを保つこと
    • 2021/04/16追記:複数ファイル化サポートするようになりました。基本方針は1ファイルで動くように設計し、それを分割できる状態にしています。

これを実現させるために、TypeScript ASTをフルに利用してCode Generatorを作成しました。 TypeScript ASTを利用すると、oneOfallOfを共用型と交差型に変換することも(かなり)容易でした。

また、ディレクトリ構造をnamespaceへと変換し、その構造を型定義の構造へ写像することによって参照構造がかなり明確になりました。 この参照解決の実装に関してはかなり労力を割いて作ったので、ぜひ試してほしい機能の一つです。

具体例

2つほど例を示しておきます。実際に使ってみると威力がわかるかと思います。 (すくなくとも記事中で説明すると半端ない量になるのでドキュメント化すると心が折れます)

例1 - oneOfの例

Playground

# 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に書きました。

例4 - さまざまなOpenAPI SchemaからTypeScriptの型定義やAPI Clientを出力する

※ DockerのOpenAPIに関しては、unixソケット経由での通信になるため、node-fetchなどではCallできないため、自前でRequestを書く必要があります(Source)。

ディレクトリの制限

ディレクトリ構造を実装に落とし込むことによって制限が生じています。 TypeScriptのnamespaceの宣言名で利用できるものしかディレクトリ名に利用できなくなります。 この点の自由度はマイナスポイントですが、名前空間を受動的に整理される利点があります。

もしかしたら、TypeScript内で利用できない命名はvalidationで弾いたり、camelCaseに変換するなど、内部実装を柔軟に変更する方法もあるかもしれません。

追記: -|/|_が含まれている場合自動的に$に変換するようになりました。

ほかにも制限があるので、気になる人はREAMDEを見てみてください。

テンプレート機能について

TypeScript ASTを利用してコードを拡張できるようにしています。一見すると難しいのですが、ts-ast-viewerというとても優秀なツールを利用することで、ASTの利点を瞬時に享受することができます。

出力する言語の実装を、同じ言語で変更できるので学習コストが低く、ts-ast-viewerのようなPlaygroundがあることで最初の第一歩が踏み出しやすく、拡張の自由度が担保できていると考えています。

依存関係の注入

デフォルトのAPI Templateは依存関係の注入を行う様になっています。 また、QueryParmaeterのフォーマットも外側で行うようにしており、実質的な処理が記述されていません。 Parameter系はさまざまなフォーマットがあり、これに関しては別のライブラリとして切り出しています。

サンプルプロジェクトにこれらの使い方が書いてあるので、ぜひ見てください。

今後について

(2022/03/21更新)

いくつかのメジャーなシステムのOpenAPI Schemaを生成することができるようになってきました。 今後は使い勝手と精度を上げていこうと考えています。バグなどあればぜひPull Requestを投げてください。

リポジトリにスターを付けるとモチベーションが大変上がります。