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
mswのモックにOpenAPIスキーマのexampleを使う
フロントエンド開発をしているとAPIがない状況でリクエスト部分を含めた動作確認をしたい場合があります。
今回はmswを使います。
mswはモックAPIを作成するライブラリのひとつです。
アプリケーションと一緒に動かしてリクエストに割り込んでモックを返すところが特徴的で、モック用のサーバーを立てて向き先を切り替えるというような設定をせずに使えます。
ブラウザ上ではService Worker、node上ではサーバーとして動きStorybookやCI上のJestでも使いやすいです。
事前に用意した openapi.json
と create-react-app
で生成したReactプロジェクトに追加します。
npm install -D msw mkdir src/mocks touch src/mocks/handlers.js
// handlers.js import { rest } from 'msw'; import openapi from '../openapi/openapi.json'; export const handlers = [ // "http://localhost:8080" の "/" にアクセスするGETリクエストの定義 rest.get('http://localhost:8000/', (_, res, ctx) => { // OpenAPIで生成したJSONファイルのexampleをレスポンスとして渡す const response = openapi.paths['/'].get.responses[200].content['application/json'].example; return res( ctx.json(response), ctx.status(200), ) }), ]
https://github.com/ihch/msw-openapi/blob/main/src/mocks/handlers.ts
FastAPIでOpenAPIスキーマを生成する
Web開発をしているとREST APIを当たり前のようにすると思いますし、大抵はバックエンドとフロントエンドの仕様共有にOpenAPIを使うと思います。
フロントエンドではよくOpenAPIから型定義の生成をしますが、そのOpenAPIも自動生成したかったりします。(OpenAPI書くのがちょっと面倒だったり、実装と乖離していたりすることがたまによくあります👼)
FastAPIはOpenAPIスキーマを生成できるPythonのWebアプリケーションフレームワークで、Pythonコードの型アノテーションを基にして、コンポーネントやAPIのスキーマを生成できるすごい特徴を持っています。
普段TypeScriptを使っている身としてはTypeScriptでもFastAPIのようなものがあったらなとは思うものの、実行時には型情報が落ちるTypeScriptの悲しき定めを感じています。
Node.jsでOpenAPIが生成できるWebアプリケーションフレームワークで fastify があります。こちらはプラグインでJavaScriptのオブジェクトとしてスキーマ定義をすることができますが、自分でスキーマ定義はやりたくないので今回は使いませんでした。
インストールして動かす
Python(3.10.6)そのもののインストールだったりエディタのLanguage Serverを入れたり色々しましたが、ここでは省きます。
pip install fastapi "uvicorn[standard]"
ソースコードは本当に何でもよかったですが、TypeScriptの型生成をするときに問題になりがちなEnumや日付文字列を返すAPIにしています。(この記事とは関係ないところで使いたい)
import datetime from enum import Enum import json from fastapi import FastAPI from fastapi.openapi.utils import get_openapi from pydantic import BaseModel app = FastAPI() class Type(Enum): JSON = 1 YAML = 2 PLAIN = 3 class Message(BaseModel): message: str date: datetime.datetime type: Type @app.get("/", response_model=Message) async def root(): return { "message": "Hello, World!", "date": datetime.datetime.now(), "type": Type.JSON } def export_openapi_scheme(): with open('openapi/openapi.json', 'w') as file: json.dump(get_openapi( title=app.title, version=app.version, openapi_version=app.openapi_version, description=app.description, routes=app.routes, ), file) export_openapi_scheme()
python -m uvicorn main:app
実行すると localhost で起動するサーバーでAPIを叩いたり、 /docs
や /openapi.json
で定義したスキーマの確認ができます。
Webサーバーを起動するのとは別にスキーマを出力するために export_openapi_scheme
関数を定義・実行できるようにしています。FastAPIのissueコメントでこの方法を答えている方がいてハッピーでした。
終わり
「書いたコードから生成できるの最高や」とか言いながらやっていました。リポジトリの方には久しぶりに書いたDockerfileやMakefileも込みであげています。
React Offscreen APIが気になる
React 18のリリースブログなどで触れられているReact Offscreen APIが便利そうだなと気になっています。
const App = ({ show }) => { return ( <Offscreen mode={show ? 'visible' : 'hidden'}> <NanikaDekiruElement /> </Offscreen> ); };
(2022/07/30にマージされていたOffscreen API関連のPR中のテストコードでは上のようなインターフェースになっている)
Offscreen API
React 18で追加された並行処理機能で実現可能になった機能のひとつで、stateを保持したまま DOMの削除/表示 を切り替えることができるものです。
React Labs: 私達のこれまでの取り組み - 2022年6月版 にReactユーザーがこれまでどのようにDOMの削除/表示 をしてきたか触れられています。
ひとつはDOMをツリー上から削除する方法。
const App = ({ show }) => { return ( <div> {show && ( <NanikaDekiruElement /> )} </div> ); };
もうひとつはCSSで display: none
などを指定して隠す方法です。
const App = ({ show }) => { return ( <div> <NanikaDekiruElement style={{ display: show ? 'block' : 'none' }} /> </div> ); };
前者は追加し直したとしてもstateやDOMの持つ状態を失っているため、復元が手間だったり完全にはできない問題があります。(同一ページ内で表示を切り替えていくフォームやモーダルはいつも大変)
後者はstateを保持できますが、画面上で表示しないのにもかかわらずDOMの再描画は都度行われるためパフォーマンス的には嬉しくないです。
このどうにも困った問題がOffscreen APIで解決されます。
終わり
Offscreen APIが出たら実装の仕方が今までと変わるのかと思うと楽しみですし、Offscreen API自体がどうやって実装されるのかも気になります。(単純に考えると hidden
の間は仮想DOMでのみ更新をして実DOMに反映しないなんですがどうなんでしょう。そのうちコードやDiscussionsを見ていきます。)
Goを始めて3日で何かを作る。あとひとり言
この記事はICT Advent Calendar 13日目の記事です。
おはようございます。
昨日から風邪をひいて寝込んでいるねむじんです。
今年も残すところあとわずかですが、みなさん体調には気をつけていきましょう。
この記事では風邪をひく前にGo言語で作っていたCUIツールの紹介とGoの浅い話をしていきたいと思います。
余談ですが、こういうタイトルよくありますよね。
個人的には始めて何日だろうが何かを作った人はえらいと思います。
なので僕もえらいです。
これを作った
notifissueといいます。名前は適当につけました。
Githubのissueやpull requestなどのアクティビティを取得して、コンソール上に表示してくれます。
もちろん日常的にGithubをブンブン使い倒している方なら、IssuesやPull Requestsなどをご覧になっているかと思います。しかし、違うのです。開発をするときに一番最初に何を開くかと言われたらブラウザではなくコンソールなのです。ブラウザを開いてGithubに飛びIssuesに遷移する。たったそれだけのことが面倒なのです。それならコンソールで見れたらいいと思いました。
動作
Shellの起動時に動くようにしているので上の画像のような動作をします。
僕がFishユーザーなのでREADMEにはFish用の動かし方しか書いていません。いつかBashやZshも書くと思います。
今後ほしい機能
現時点では更新日時, Issueやpull requestのタイトルしか見れていません。
トークンも別にいいかと思って使用してないです。
🍣 アクティビティを指定して詳細やコメントを表示する
🍣 アクティビティを指定してGithubのページに飛ぶ
🍣 トークンをつけれるようにしたらコメントとかもっと細かいことができそう
Goの浅い話
Go入門の流れ
新しく言語を触り始めるとき皆さんは何をするのでしょうか。以前の僕は適当に競プロの問題をいくつかやって雰囲気を知るみたいなことをしていました。
今回僕が実践したGo入門簡単3ステップ(は?)を紹介します。
step1 記憶の中のGoを書く
step2 A Tour of Goをやる
step3 なんか作る
ものづくり駆動学習などは目的がはっきりしているので、必要な言語機能の使い方をさっさと覚えていけていいと思います。置き去りにされる知識もあると思いますが、それは使っていたらいつか知りたくなるはずなのできっと大丈夫です。
Goを触った感想
C likeな文法だけど違う部分が個人的に好きです。
関数定義と返り値の型の情報がわかれていたり、構造体の定義時にJSONなどの外部データからの情報の読み方を書くこともできます。
変数名・関数名の先頭が大文字小文字かでそのパッケージをimportしたときに見えるかどうかが変わる。C/C++だと修飾子が多くなってつらかったりするのでありだと思います。
go get hogehoge
なども最近はMac OSとmanjaroを行き来しているのでビルドをよしなにやってくれて助かります。
// C int add(int a, int b) { return a + b; } // Go func Add(a int, b int) int { return a + b }
どの言語も分かりにくいわけではないですが、修飾子で情報を明示させる度合いやコードの書き方で暗黙的に制約がつく度合いが、何を分かりやすさだとして作られたかの違いなのかなと思いました。
まとめ
新しい言語を触るのも何かを作るのも難しくないですし楽しいのでおすすめです。
しばらくはnotifissueを育てていこうと思ってるので、ぜひあれがほしいこうした方が良いなどあれば声をかけてほしいです。
以下、ひとり言
今年の振り返り
驚くほどの内容の薄さと文字の多さです。この時点で2000文字を超えています。風邪をひいて頭が回っていないのによくやったと自分を賞賛しています。
今年は成長できただろうか...
Githubの草から分かる通り2019年前半は何も手につかないほど気が病んでいました。一体何故なんでしょうか... 病み案件が解消されてからはそれ以前よりも、日常的にGithub上で何かをやるようになっていて、少し良い感じです。1人で遊ぶだけでなく、チームでの開発やハッカソンの参加もあったりして楽しくやれました。
来年は
🍣 人として生きる -> まずは朝起きて二度寝しないことからね
🍣 やったことを記事に残していく -> 脳のメモリが2Byteくらいしかないことに気づいたよ
ICT委員会さんへ
🍣 すでに来年のプロコンチームが結成されていると思いますが、アイディア出しは病まない程度に頑張ってください
🍣 大会に出ない選択をした方々は好きなことをやりまくってLTでドヤってやりましょう
最近の推し
終わり
明日はmitohato14さんの記事です。一体何を話してくれるのでしょうか楽しみですね。
WSLでOpenGL(GUI)を使いたい
WSLからOpenGLを使いたいあなたへ
※2017-2018くらいにやったものです。
OpenGLのインストール
$ sudo apt-get install freeglut3-dev libglew1.5-dev libxmu-dev libxi-dev # 必要なものをインストールします # proxy環境の場合はsudoのあとに`-E`を追加してください $ gcc ファイル名 -lGL -lglut # コンパイルにはオプションが必要です $ ./a.out # 実行します
おそらく実行するとこんな感じで怒られます
freeglut (./a.out): failed to open display ''
Xmingのインストール
XmingというものをインストールしてWSLにGUI環境を生やします
ダウンロード・インストール
Xmingダウンロードページ
最新ファイルのXming-*-*-*-**-setup.exe
のリンクをクリックするとダウンロードが始まります
インストールマネージャが開くので特にいじらずにインストールします
Bashの設定
$ sudo apt-get install x11-apps # Xmingにつなげるためのものをインストール $ echo "export DISPLAY=localhost:0.0" >> ~/.bashrc # .bashrcに設定を保存 $ exec bash # bashの再起動 $ xeyes & # 起動 # &はバックグラウンドで動かすためのもの
もう一度./a.out
をするとちゃんと動くはず
普段使うときにやること
windowsキーを押して検索するなりしてXming
を起動する
$ xeyes & $ gcc ファイル名 -lGL -lglut $ ./a.out
追記 macユーザーになったのでmacでのコマンドも書いておきます 開発環境の方はXcodeさえあればいい感じだったと思います
$ gcc -framework GLUT -framework OpenGL $filename -Wno-deprecated
PCK(パソコン甲子園)引退
PCK参加記
PCKから2ヶ月も経っていてウケますね。
中間試験、研修旅行、セキュリティミニキャンプとイベント続きだったので許してください。
チーム結成時の思い
2月か3月にチームの発表がありました。
チームメンバーはぼく、mito、makabiでした。
たるとで前の年のリベンジをしたかったですが、しゅり先輩が大会に出ないことはわかっていたので「がんばるぞい!」といった気持ちでした。
ICT春合宿
初めて開発をする当時の一年生向けに、しゅーがくりょーこんを題材にして開発勉強会のようなものが開かれます。
ぼくのコードも教え方もアで反省点が多かったです。
いまだに振り返っていませんが
このころにはプロコン組はアイディア出しを始めていて忙しそうでした。
アイディア出し
PCKは春合宿の終わりも終わり、4月に入る直前にテーマが発表されました。
テーマ「トマト学ぶ 友と学ぶ」 の文字列を見た瞬間にテーマの出し方がいままでの「家族」のような主語だけとは違い「友と」、「学ぶ」とやることまで限定されていて、困惑したのを覚えています。
さて、学校も始まり時は過ぎていくわけですがアイディアは出ません。
運営側の思惑通りかそうでないかは知りませんが、テーマに引っ張られて面白いアイディアが一向に出ませんでした。
そんな日々が2ヶ月ほど続き6月になりました。
そろそろ企画書を書かなければならない時期ですがアイディアはありません。
この時期になるとアイディア出し中の沈黙が増えていたような気がします。
精神的にもつらくなり、寝る前になると「あいでぃあ...あいでぃあ...」と呪文のようにつぶやくか、アイディア出しが終わったら何をしようとか考えていました。
つらすぎて悪夢を見るようになってからは、本気でやめたいと思っていました。
そんなこんなで教授からアイディアの種を投げられ、企画書を書いて一時の平穏がやってきました。
ところでチーム名の「ちょこちっぷマフィン」はぼくが考えました。
PCKチームの慣習どおりおいしそうなお菓子になりました。
暫定チーム名だった「mitoのご老公」も好きでした。
チョコに関してはそこまで好きではないが食べられるようになっています。
夏休み
アプリで作るものの詳細が決まらないまま夏休みに入ります。
夏休み後の変更を考えると虚無を過ごしたことになります。
夏休み後
激動です。
アプリのいろいろに変更が入ります。
悪夢を見て深夜に目を覚ます日々が再開します。
makabiをギリギリまで開発に関わらせていたこともあり、プレゼン・ポスター・パンフが死ぬほどやばいタイミングで完成しました。
去年のしゅりはこれを一人でやったのかと思うと感謝しきれません。
本番
本番前日
- 今年はスーツを忘れる人もおらず何事もなく飛行機に乗りました。
- 美味しいラーメンを食べて夜は開発をしました。
- この日の夜には某 JAPANの某川さんがいたような気がしたりしなかったりします。
本番1日目
- 開会式では大勢の競プロerに前後を挟まれながら、これどこかのチームに紛れててもバレないんじゃないかとか考えていました。
- 展示ブースの飾り付けが始まりますが、今年はデバイスなどもないため机の上の隙間が目立ちました。
- 2回目の参加ということもあり、時間的・精神的な余裕はあったのでぼくは他チームの展示を見に行ったり、鈴鹿高専の去年も参加していた人と少し話をしたりしました。
- 夕食の立食パーティーでは、久留米高専の方や✝高専プロコン競技部門優勝✝を果たした産技高専の方、地元高校の教員の方(?)ともお話しました。
- キノの旅などを見ながら開発をします。
本番2日目
- プレゼン・デモの日です。
- やはり机の隙間が気になるのでパンフレットをいくつか広げて置いたりします。
- プレゼンが始まる前に競プロerたちがぞろぞろと展示を見に来るのでデモをします。
- ついでにアカウントの特定もします。
- プレゼン直前にmakabiがステージの上でジャンプしていました。
- 特に何も考えず無心で立っていたのでプレゼン後に前に出されて質問されたときはびっくりしました。
- 表彰式があります。ベストデザイン賞でした。
- 今年は副賞を落として競プロ勢の席の方から「マァァァァァ!!オトシタァ-!」とか聞こえてくることもありませんでした。
- ホテルに帰るバスに乗ろうと歩いていったら満員なうえに雨が降ってきました。
- バスに乗っていたらSLが走っていました。吹き出る煙がかっこよくてテンション上がりました。
- 温泉に入ります。プチ反省会がありました。
本番1日後
- りんちゃん先輩と秋葉原へ行き、おたく活動をします。
- 飛行機に乗りました。
振り返ります
アイディア出し
つらかったです。
アイディア出しを長くやりすぎると意識していても沈黙が多くなります。
期限が近づくと「微妙だからもうちょっと考えない?」とも言いづらくなります。(言いますが)
これに関しては解決の方法が思いつきません。
自分たちが楽しめるアイディアを出せるようになりたいです。
やめたかった
今思うと逃げ道がわかっていたことで余裕ができて良かったと思います。(いつでも逃げられるのでね)
おそらくやめるという選択肢を真面目に考えていたのはプロコン組の円満離婚などをみていたからですね。
夏休み前にすべきこと
仕様はちゃんと決めましょう。
期末試験もありますががんばってください。夏休みに虚無を得ることになります。
ギーク女子
去年しゅりに全く開発をさせることができなかった分makabiにはさせたいねってmitoと話していましたが、プレゼン準備に入ってもらうタイミングが遅くなり完全に危ないラインでした。
ICT委員会ではよくギーク女子育成計画のような言葉を聞きますが、それを支えるためにはデザイン男子やプレゼン男子も必要だと強く感じました。
まあ、そもそも育てるようなものではないのかもしれませんが。
締め
つらいこともありましたが、2年間PCKに参加できてよかったです。
1年目は緊張して参加だけで精一杯でしたが、2年目は他校の人と開発について話したり競プロ勢の数人と話すことができました。
わりと青春らしいものを過ごせたような気がします。
ありがとうございました。
大会のときのツイート集です。(いまは鍵をかけてるので見れるかわかりませんが)