TypeScript Compiler APIを使って型定義からOpenAPIのスキーマを生成する
TypeScript Compiler APIでは、TypeScriptコードを入力として木構造のデータ(AST)を生成し様々な形に変換をすることができます。 わかりやすいかつ一番有名なものはTypeScript Compiler APIの名前の通り、TypeScriptをJavaScriptにコンパイルする変換です。
以前、適当なサーバーを立ててOpenAPIを生成したときにPythonのFastAPIを使ったのですが、TypeScriptで近いことはできないだろうかと考えていました。
今回はTypeScript Compiler APIを使って型定義からOpenAPIのスキーマを生成を試しました。
脱線 TypeScript Compiler APIを使うときの便利ツール
少しずれますがTypeScriptのASTを扱う際に便利なツールにTypeScript AST Viewerがあります。
左上の入力枠にTypeScriptコードを入力するといくつか情報が表示されます。
- 中央 AST上でどういうデータ(シンタックス)として認識したか表示している
- 右 ASTでノードが持つデータを表示している
- 左下 入力したコードのASTをTypeScriptコードで生成するために必要な処理を表示している
TypeScriptコードから(TypeScriptコードも含む)別の何かに変換する際は、中央と右側の情報を見ながらやるとどういうデータを扱っているか分かりやすくていいです。
TypeScriptコードを生成する場合は左下を見ながらやるとノードの生成がとても楽そう。
プロジェクトの準備
npm init -y npm install typescript npm install -D @types/node
書く
まずコードを入力としてOpenAPIのスキーマを返すガワを書きます。
import * as typescript from 'typescript'; // スキーマ情報を保持するオブジェクト const openapiComponents = { components: { schemes: [], }, }; export function type2openapiSchemes(src: string): string { // コードからASTを生成する const source = typescript.createSourceFile('', src, typescript.ScriptTarget.ES2020); // ASTを使って処理をする // スキーマのオブジェクトを文字列化して返す return JSON.stringify(openapiComponents); }
source.statements
でコードのトップレベルで定義されているノードの配列が得られるのでそこを起点として処理をしていきます。TypeScriptコード中から型定義だけの部分だけを処理するように、型定義をしているNodeで絞り込みます。
const f = (node: typescript.Node) => { if (typeof node !== 'object') return; if (node.kind === typescript.SyntaxKind.TypeAliasDeclaration) { const typeAliasDeclarationNode = <typescript.TypeAliasDeclaration>node; // Nodeの情報からスキーマオブジェクトにする const scheme = {}; // Nodeの情報をJSONに詰めていく openapiComponents.components.schemes = { ...openapiComponents.components.schemes, [`${scheme.title}`]: scheme }; } } export function type2openapiSchemes(src: string): string { ... // ASTを使って処理をする source.statements.forEach(f); ... }
型定義をしているNodeの情報からスキーマオブジェクトを作っていく処理を作っていきます。OpenAPIのデータにする部分は型ごとに実装が必要ですがここは気合です。
const createSchemeFromTypeDeclaration = (typeName: string, members: typescript.NodeArray<typescript.TypeElement>) => { const properties = members.map((member) => { return { name: (<typescript.Identifier>member.name).escapedText, required: member.questionToken == null, type: (() => { // 型部分のNodeの種類毎にOpenAPIの仕様に合わせて変換する const type = (<typescript.PropertySignature>member).type; if (type.kind === typescript.SyntaxKind.StringKeyword) { return 'string'; } if (type.kind === typescript.SyntaxKind.UnionType) { /* @ts-ignore next-line */ const types = (type as typescript.UnionType).types; return types.map((type: typescript.LiteralType) => { const literal = (type as any).literal; if (literal.numericLiteralFlags != null && !Number.isNaN(literal.text)) { return Number(literal.text); } return literal.text; }); } return null; })() }; }); return { title: typeName, required: properties.map((property) => property.required && property.name).filter((name) => name), type: 'object', properties: Object.assign({}, ...properties.map((property) => { return { [`${property.name}`]: { title: typeName, type: property.type, }, }; })) }; }; const createScheme = (node: typescript.TypeAliasDeclaration) => { return createSchemeFromTypeDeclaration( node.name.text, (<typescript.TypeLiteralNode>node.type).members ); }
うごかす
入力ファイル
export type PingResponse = { body: string; status: 'ok' | 'ng' | 1; nullableParameter?: string; } export type CommentResponse = { text: string; }
出力をフォーマットしたもの
{ "components": { "schemes": { "PingResponse": { "title": "PingResponse", "required": [ "body", "status" ], "type": "object", "properties": { "body": { "title": "PingResponse", "type": "string" }, "status": { "title": "PingResponse", "type": [ "ok", "ng", 1 ] }, "nullableParameter": { "title": "PingResponse", "type": "string" } } }, "CommentResponse": { "title": "CommentResponse", "required": [ "text" ], "type": "object", "properties": { "text": { "title": "CommentResponse", "type": "string" } } } } } }
おわり
対応していない型がほとんどですし定義した型同士の参照はまだできませんが、コマンド化とGitHub packagesで公開をしてみました。
これで生成したものを使ってOpenAPIの定義全体を生成するWebフレームワークを作れたら面白いなと思いました 。
参考
Using the Compiler API · microsoft/TypeScript Wiki · GitHub
TypeScript Compiler API で型を自動生成する仕組みを TDD で実装する
TypeScriptのcompiler APIをいじる - asterisc