-
Notifications
You must be signed in to change notification settings - Fork 350
feat: 実行時にレスポンスの型チェックを行うように #2619
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -55,6 +55,7 @@ | |
| "@quasar/extras": "1.16.17", | ||
| "@sevenc-nanashi/utaformatix-ts": "npm:@jsr/[email protected]", | ||
| "@std/path": "npm:@jsr/[email protected]", | ||
| "ajv": "8.17.1", | ||
| "async-lock": "1.4.1", | ||
| "dayjs": "1.11.13", | ||
| "electron-log": "5.3.1", | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| import { cloneWithUnwrapProxy } from "@/helpers/cloneWithUnwrapProxy"; | ||
| import openapi from "../../openapi.json"; | ||
| import { ProxyStoreState, ProxyStoreTypes, EditorAudioQuery } from "./type"; | ||
| import { createPartialStore } from "./vuex"; | ||
| import { createEngineUrl } from "@/domain/url"; | ||
|
|
@@ -7,11 +9,107 @@ | |
| OpenAPIEngineAndMockConnectorFactory, | ||
| OpenAPIEngineConnectorFactory, | ||
| } from "@/infrastructures/EngineConnector"; | ||
| import { AudioQuery } from "@/openapi"; | ||
| import { AudioQuery, DefaultApiInterface } from "@/openapi"; | ||
| import { EngineInfo } from "@/type/preload"; | ||
| import Ajv, { ValidateFunction } from "ajv"; | ||
|
|
||
| export const proxyStoreState: ProxyStoreState = {}; | ||
|
|
||
| const validateOpenApiResponse = createValidateOpenApiResponse(); | ||
|
|
||
| function toCamelCase(str: string) { | ||
| return str.replace(/_./g, (s) => s.charAt(1).toUpperCase()); | ||
| } | ||
|
|
||
| /** OpenAPIのレスポンスを検証する */ | ||
| function createValidateOpenApiResponse() { | ||
|
||
| const ajv = new Ajv().addSchema({ | ||
| $id: "openapi.json", | ||
| definitions: patchOpenApiJson(openapi).components.schemas, | ||
| }); | ||
| const validatorCache = new Map<string, ValidateFunction>(); | ||
|
|
||
| for (const path of Object.values(openapi.paths)) { | ||
| for (const method of Object.values(path)) { | ||
| const schema = | ||
| method.responses["200"]?.content?.["application/json"]?.schema; | ||
| if (schema == null) { | ||
| continue; | ||
| } | ||
| validatorCache.set( | ||
| toCamelCase(method.operationId), | ||
|
Check failure on line 40 in src/store/proxy.ts
|
||
| ajv.compile(patchOpenApiJson(schema)), | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| return <K extends keyof DefaultApiInterface>( | ||
| key: K, | ||
| response: ReturnType<DefaultApiInterface[K]>, | ||
| ): ReturnType<DefaultApiInterface[K]> => { | ||
| return response.then((res) => { | ||
| const maybeValidator = validatorCache.get(key); | ||
| if (maybeValidator == null) { | ||
| return res; | ||
| } | ||
|
|
||
| if (!maybeValidator(res)) { | ||
| throw new Error( | ||
| `Response validation error in ${key}: ${ajv.errorsText(maybeValidator.errors)}`, | ||
| ); | ||
| } | ||
|
|
||
| return res; | ||
| }) as ReturnType<DefaultApiInterface[K]>; | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * OpenAPIのスキーマを修正する。 | ||
| * | ||
| * 具体的には以下の変更を行う: | ||
| * - `$ref`の参照先を`#/components/schemas/`から`openapi.json#/definitions/`に変更する | ||
| * - オブジェクトのプロパティ名をキャメルケースに変換する | ||
| */ | ||
| function patchOpenApiJson<T extends Record<string, unknown>>(schema: T): T { | ||
| return inner(cloneWithUnwrapProxy(schema)) as T; | ||
|
|
||
| function inner(schema: Record<string, unknown>): Record<string, unknown> { | ||
| if (schema["$ref"] != null) { | ||
| const ref = schema["$ref"]; | ||
| if (typeof ref === "string") { | ||
| schema["$ref"] = ref.replace( | ||
| "#/components/schemas/", | ||
| "openapi.json#/definitions/", | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| if ( | ||
|
||
| schema["type"] === "object" && | ||
| typeof schema["properties"] === "object" && | ||
| schema["properties"] != null && | ||
| Array.isArray(schema["required"]) | ||
| ) { | ||
| schema["properties"] = Object.fromEntries( | ||
| Object.entries(schema["properties"]).map(([key, value]) => [ | ||
| toCamelCase(key), | ||
| inner(value as Record<string, unknown>), | ||
| ]), | ||
| ); | ||
|
|
||
| schema["required"] = schema["required"].map((key) => toCamelCase(key)); | ||
| } | ||
|
|
||
| for (const key in schema) { | ||
| if (typeof schema[key] === "object" && schema[key] != null) { | ||
| inner(schema[key] as Record<string, unknown>); | ||
| } | ||
| } | ||
| return schema; | ||
| } | ||
| } | ||
|
|
||
| const proxyStoreCreator = (_engineFactory: IEngineConnectorFactory) => { | ||
| const proxyStore = createPartialStore<ProxyStoreTypes>({ | ||
| INSTANTIATE_ENGINE_CONNECTOR: { | ||
|
|
@@ -35,11 +133,11 @@ | |
| ); | ||
| return Promise.resolve({ | ||
| invoke: (v) => (arg) => | ||
| // FIXME: anyを使わないようにする | ||
| // eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
| // @ts-ignore | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return | ||
| instance[v](arg) as any, | ||
| validateOpenApiResponse( | ||
| v, | ||
| // @ts-expect-error 動いているので無視 | ||
| instance[v](arg), | ||
| ), | ||
| }); | ||
| }, | ||
| }, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ちなみにzodでできたりしないですかね?
(依存を減らしたい)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
zodはjson schemaを扱えませんね。
方法としてはjson-schema-to-zodを走らせるツールをtools下に生やすのが現実的かも?(それを作るなら一緒にopenapi generatorも走らせたい気持ちがある)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
あーそうか。。
これってpackage.jsonでインストールしてるgeneratorのことですかね?それとも自作とか?
(自作だと流石に避けたい)
うーーーーん メンテが結構大変かもな気がしてきました。
ちょっと別のとこでコメントします!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
package.jsonのやつですね。イメージとしては
pnpm run tools:update-openapi http://localhost:50021をするとopenapi-generator-cliとかが走るイメージ。There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
あーなるほどです!!
それは結構嬉しそう感。