swagger-cli validateがvalidとなるapi.jsonを作れるようにする (#12403)

* api.jsonがswagger-cli validateでエラーにならないように生成ロジックを修正

* フィールドの消し方に不備があったので変更

* バックエンドを起動しなくてもapi.jsonを作れるようにした

* deepCopyしてからレスポンス部分を作るようにした

* fix CHANGELOG.md

* securitySchemesの定義を復活&ApiCallServiceの実装的にベアラトークンなのでその形で

* bodyが無い(空オブジェクト)のときはrequestBodyを描画しないようにする

* allowGetがtrueな項目はget用の記載も作成

---------

Co-authored-by: osamu <46447427+sam-osamu@users.noreply.github.com>
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
おさむのひと 2023-11-22 17:08:56 +09:00 committed by GitHub
parent a4f8863786
commit c284d41b5b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 51 additions and 22 deletions

View file

@ -25,6 +25,7 @@
### Server ### Server
- Fix: 時間経過により無効化されたアンテナを再有効化したとき、サーバ再起動までその状況が反映されないのを修正 #12303 - Fix: 時間経過により無効化されたアンテナを再有効化したとき、サーバ再起動までその状況が反映されないのを修正 #12303
- Fix: ロールタイムラインが保存されない問題を修正 - Fix: ロールタイムラインが保存されない問題を修正
- Fix: api.jsonの生成ロジックを改善 #12402
## 2023.11.1 ## 2023.11.1

View file

@ -0,0 +1,8 @@
import { loadConfig } from './built/config.js'
import { genOpenapiSpec } from './built/server/api/openapi/gen-spec.js'
import { writeFileSync } from "node:fs";
const config = loadConfig();
const spec = genOpenapiSpec(config);
writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');

View file

@ -23,7 +23,8 @@
"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit", "jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit",
"jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache", "jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache",
"test": "pnpm jest", "test": "pnpm jest",
"test-and-coverage": "pnpm jest-and-coverage" "test-and-coverage": "pnpm jest-and-coverage",
"generate-api-json": "node ./generate_api_json.js"
}, },
"optionalDependencies": { "optionalDependencies": {
"@swc/core-android-arm64": "1.3.11", "@swc/core-android-arm64": "1.3.11",

View file

@ -16,12 +16,9 @@ export const meta = {
requireCredential: false, requireCredential: false,
res: { res: {
oneOf: [{
type: 'object', type: 'object',
optional: false, nullable: true,
ref: 'FederationInstance', ref: 'FederationInstance',
}, {
type: 'null',
}],
}, },
} as const; } as const;

View file

@ -4,7 +4,7 @@
*/ */
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import endpoints from '../endpoints.js'; import endpoints, { IEndpoint } from '../endpoints.js';
import { errors as basicErrors } from './errors.js'; import { errors as basicErrors } from './errors.js';
import { schemas, convertSchemaToOpenApiSchema } from './schemas.js'; import { schemas, convertSchemaToOpenApiSchema } from './schemas.js';
@ -33,16 +33,17 @@ export function genOpenapiSpec(config: Config) {
schemas: schemas, schemas: schemas,
securitySchemes: { securitySchemes: {
ApiKeyAuth: { bearerAuth: {
type: 'apiKey', type: 'http',
in: 'body', scheme: 'bearer',
name: 'i',
}, },
}, },
}, },
}; };
for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) { // 書き換えたりするのでディープコピーしておく。そのまま編集するとメモリ上の値が汚れて次回以降の出力に影響する
const copiedEndpoints = JSON.parse(JSON.stringify(endpoints)) as IEndpoint[];
for (const endpoint of copiedEndpoints.filter(ep => !ep.meta.secure)) {
const errors = {} as any; const errors = {} as any;
if (endpoint.meta.errors) { if (endpoint.meta.errors) {
@ -79,6 +80,13 @@ export function genOpenapiSpec(config: Config) {
schema.required = [...schema.required ?? [], 'file']; schema.required = [...schema.required ?? [], 'file'];
} }
if (schema.required && schema.required.length <= 0) {
// 空配列は許可されない
schema.required = undefined;
}
const hasBody = (schema.type === 'object' && schema.properties && Object.keys(schema.properties).length >= 1);
const info = { const info = {
operationId: endpoint.name, operationId: endpoint.name,
summary: endpoint.name, summary: endpoint.name,
@ -92,9 +100,10 @@ export function genOpenapiSpec(config: Config) {
} : {}), } : {}),
...(endpoint.meta.requireCredential ? { ...(endpoint.meta.requireCredential ? {
security: [{ security: [{
ApiKeyAuth: [], bearerAuth: [],
}], }],
} : {}), } : {}),
...(hasBody ? {
requestBody: { requestBody: {
required: true, required: true,
content: { content: {
@ -103,6 +112,7 @@ export function genOpenapiSpec(config: Config) {
}, },
}, },
}, },
} : {}),
responses: { responses: {
...(endpoint.meta.res ? { ...(endpoint.meta.res ? {
'200': { '200': {
@ -118,6 +128,11 @@ export function genOpenapiSpec(config: Config) {
description: 'OK (without any results)', description: 'OK (without any results)',
}, },
}), }),
...(endpoint.meta.res?.optional === true || endpoint.meta.res?.nullable === true ? {
'204': {
description: 'OK (without any results)',
},
} : {}),
'400': { '400': {
description: 'Client error', description: 'Client error',
content: { content: {
@ -190,6 +205,7 @@ export function genOpenapiSpec(config: Config) {
}; };
spec.paths['/' + endpoint.name] = { spec.paths['/' + endpoint.name] = {
...(endpoint.meta.allowGet ? { get: info } : {}),
post: info, post: info,
}; };
} }

View file

@ -7,10 +7,16 @@ import type { Schema } from '@/misc/json-schema.js';
import { refs } from '@/misc/json-schema.js'; import { refs } from '@/misc/json-schema.js';
export function convertSchemaToOpenApiSchema(schema: Schema) { export function convertSchemaToOpenApiSchema(schema: Schema) {
const res: any = schema; // optional, refはスキーマ定義に含まれないので分離しておく
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { optional, ref, ...res }: any = schema;
if (schema.type === 'object' && schema.properties) { if (schema.type === 'object' && schema.properties) {
res.required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k); const required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k);
if (required.length > 0) {
// 空配列は許可されない
res.required = required;
}
for (const k of Object.keys(schema.properties)) { for (const k of Object.keys(schema.properties)) {
res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k]); res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k]);