swagger2api-v3 1.1.10 → 1.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/.swagger2api.schema.json +2 -2
- package/dist/cli/index.js +2 -2
- package/dist/config/validator.js +92 -9
- package/dist/core/generator.d.ts +71 -17
- package/dist/core/generator.js +261 -106
- package/dist/core/parser.d.ts +14 -16
- package/dist/core/parser.js +32 -90
- package/dist/core/query-parameter-generator.d.ts +26 -0
- package/dist/core/query-parameter-generator.js +114 -0
- package/dist/core/type-generator.d.ts +73 -0
- package/dist/core/type-generator.js +215 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -3
- package/dist/types/index.d.ts +27 -6
- package/dist/utils/comment.d.ts +4 -1
- package/dist/utils/comment.js +6 -2
- package/dist/utils/file.d.ts +10 -3
- package/dist/utils/file.js +48 -19
- package/dist/utils/naming.js +3 -2
- package/dist/utils/schema.d.ts +79 -0
- package/dist/utils/schema.js +536 -0
- package/dist/utils/type.d.ts +7 -37
- package/dist/utils/type.js +29 -260
- package/package.json +4 -4
package/dist/core/generator.js
CHANGED
|
@@ -39,13 +39,19 @@ const fs = __importStar(require("fs"));
|
|
|
39
39
|
const child_process_1 = require("child_process");
|
|
40
40
|
const util_1 = require("util");
|
|
41
41
|
const utils_1 = require("../utils");
|
|
42
|
+
const query_parameter_generator_1 = require("./query-parameter-generator");
|
|
42
43
|
const execPromise = (0, util_1.promisify)(child_process_1.exec);
|
|
43
44
|
/**
|
|
44
45
|
* 代码生成器
|
|
45
46
|
*/
|
|
46
47
|
class CodeGenerator {
|
|
48
|
+
/**
|
|
49
|
+
* 创建代码生成器
|
|
50
|
+
* @param config 生成配置
|
|
51
|
+
*/
|
|
47
52
|
constructor(config) {
|
|
48
53
|
this.config = config;
|
|
54
|
+
this.outputRoot = path.resolve(config.output);
|
|
49
55
|
}
|
|
50
56
|
/**
|
|
51
57
|
* 生成所有文件
|
|
@@ -54,17 +60,27 @@ class CodeGenerator {
|
|
|
54
60
|
* @param groupedApis 按标签分组的API
|
|
55
61
|
*/
|
|
56
62
|
async generateAll(apis, types, groupedApis) {
|
|
63
|
+
this.assertSafeOutputDirectory();
|
|
64
|
+
if (this.config.options?.generateApis !== false &&
|
|
65
|
+
this.config.groupByTags) {
|
|
66
|
+
this.validateTagFolders(groupedApis);
|
|
67
|
+
}
|
|
57
68
|
// 根据overwrite配置决定是否清空目录
|
|
58
69
|
if (this.config.overwrite !== false) {
|
|
59
70
|
// 默认为true,清空输出目录
|
|
60
|
-
(0, utils_1.removeDirectory)(this.
|
|
71
|
+
(0, utils_1.removeDirectory)(this.outputRoot);
|
|
61
72
|
}
|
|
62
73
|
// 确保输出目录存在
|
|
63
|
-
(0, utils_1.ensureDirectoryExists)(this.
|
|
74
|
+
(0, utils_1.ensureDirectoryExists)(this.outputRoot);
|
|
64
75
|
// 生成类型文件(仅在 TypeScript 模式下生成)
|
|
65
76
|
if (this.config.generator === 'typescript' &&
|
|
66
77
|
this.config.options?.generateModels !== false) {
|
|
67
|
-
|
|
78
|
+
const rootTypes = this.config.groupByTags
|
|
79
|
+
? types
|
|
80
|
+
: this.config.options?.generateApis === false
|
|
81
|
+
? types
|
|
82
|
+
: this.mergeTypeDefinitions(types, (0, query_parameter_generator_1.generateQueryParameterTypes)(apis, types.map((type) => type.name)));
|
|
83
|
+
await this.generateTypesFile(rootTypes);
|
|
68
84
|
}
|
|
69
85
|
// 生成API文件
|
|
70
86
|
if (this.config.options?.generateApis !== false) {
|
|
@@ -90,7 +106,7 @@ class CodeGenerator {
|
|
|
90
106
|
*/
|
|
91
107
|
async generateTypesFile(types) {
|
|
92
108
|
const content = this.generateTypesContent(types);
|
|
93
|
-
const filePath =
|
|
109
|
+
const filePath = this.resolveOutputPath('types.ts');
|
|
94
110
|
this.writeGeneratedFile(filePath, content);
|
|
95
111
|
}
|
|
96
112
|
/**
|
|
@@ -105,23 +121,66 @@ class CodeGenerator {
|
|
|
105
121
|
' * 此文件由 swagger2api-v3 自动生成,请勿手动修改',
|
|
106
122
|
' */'
|
|
107
123
|
]);
|
|
108
|
-
const typeDefinitions = types
|
|
124
|
+
const typeDefinitions = this.renderTypeDefinitions(types);
|
|
125
|
+
return `${header}\n\n${typeDefinitions}\n`;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 渲染类型定义列表
|
|
129
|
+
* @param types 类型定义数组
|
|
130
|
+
* @returns 类型定义内容
|
|
131
|
+
*/
|
|
132
|
+
renderTypeDefinitions(types) {
|
|
133
|
+
return types
|
|
109
134
|
.map((type) => {
|
|
110
135
|
const comment = type.description
|
|
111
136
|
? `/**\n * ${type.description}\n */\n`
|
|
112
137
|
: '';
|
|
113
|
-
|
|
114
|
-
let definition = type.definition;
|
|
115
|
-
if (this.isGenericResponseContainer(type, definition)) {
|
|
116
|
-
const typeName = type.name;
|
|
117
|
-
definition = definition
|
|
118
|
-
.replace(`export interface ${typeName} {`, `export interface ${typeName}<T = Record<string, any>> {`)
|
|
119
|
-
.replace('data: Record<string, any>;', 'data: T;');
|
|
120
|
-
}
|
|
121
|
-
return `${comment}${definition}`;
|
|
138
|
+
return `${comment}${type.definition}`;
|
|
122
139
|
})
|
|
123
140
|
.join('\n\n');
|
|
124
|
-
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* 合并类型定义
|
|
144
|
+
* @param modelTypes 组件模型类型
|
|
145
|
+
* @param parameterTypes 查询参数类型
|
|
146
|
+
* @returns 合并后的类型定义
|
|
147
|
+
*/
|
|
148
|
+
mergeTypeDefinitions(modelTypes, parameterTypes) {
|
|
149
|
+
return [...modelTypes, ...parameterTypes];
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* 判断是否应提取查询参数类型
|
|
153
|
+
* @returns 是否提取查询参数类型
|
|
154
|
+
*/
|
|
155
|
+
shouldExtractQueryParameterTypes() {
|
|
156
|
+
return (this.config.generator === 'typescript' &&
|
|
157
|
+
this.config.options?.generateModels !== false);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* 生成 tag 内的查询参数类型文件
|
|
161
|
+
* @param folderName tag 目录名
|
|
162
|
+
* @param apis 当前 tag 的 API 接口
|
|
163
|
+
* @param modelTypes 组件模型类型
|
|
164
|
+
*/
|
|
165
|
+
generateTagParameterTypesFile(folderName, apis, modelTypes) {
|
|
166
|
+
const parameterTypes = (0, query_parameter_generator_1.generateQueryParameterTypes)(apis, modelTypes.map((type) => type.name));
|
|
167
|
+
if (parameterTypes.length === 0)
|
|
168
|
+
return;
|
|
169
|
+
const usedModelTypes = (0, query_parameter_generator_1.collectQueryParameterModelTypes)(apis, modelTypes);
|
|
170
|
+
const content = [
|
|
171
|
+
this.generateHeader([
|
|
172
|
+
'/**',
|
|
173
|
+
' * API 查询参数类型',
|
|
174
|
+
' * 此文件由 swagger2api-v3 自动生成,请勿手动修改',
|
|
175
|
+
' */'
|
|
176
|
+
])
|
|
177
|
+
];
|
|
178
|
+
if (usedModelTypes.length > 0) {
|
|
179
|
+
content.push(`import type { ${usedModelTypes.join(', ')} } from '../../types';`);
|
|
180
|
+
}
|
|
181
|
+
content.push('', this.renderTypeDefinitions(parameterTypes), '');
|
|
182
|
+
const filePath = this.resolveOutputPath(folderName, 'types', 'index.ts');
|
|
183
|
+
this.writeGeneratedFile(filePath, content.join('\n'));
|
|
125
184
|
}
|
|
126
185
|
/**
|
|
127
186
|
* 按标签生成API文件
|
|
@@ -131,9 +190,12 @@ class CodeGenerator {
|
|
|
131
190
|
async generateApiFilesByTags(groupedApis, types) {
|
|
132
191
|
for (const [tag, apis] of groupedApis) {
|
|
133
192
|
const folderName = this.getTagFileName(tag);
|
|
134
|
-
const tagFolderPath =
|
|
193
|
+
const tagFolderPath = this.resolveOutputPath(folderName);
|
|
135
194
|
// 确保tag文件夹存在
|
|
136
195
|
(0, utils_1.ensureDirectoryExists)(tagFolderPath);
|
|
196
|
+
if (this.shouldExtractQueryParameterTypes()) {
|
|
197
|
+
this.generateTagParameterTypesFile(folderName, apis, types);
|
|
198
|
+
}
|
|
137
199
|
const ext = this.config.generator === 'javascript' ? 'js' : 'ts';
|
|
138
200
|
const filePath = path.join(tagFolderPath, `index.${ext}`);
|
|
139
201
|
const content = this.generateApiFileContent(apis, types, tag);
|
|
@@ -147,7 +209,7 @@ class CodeGenerator {
|
|
|
147
209
|
*/
|
|
148
210
|
async generateSingleApiFile(apis, types) {
|
|
149
211
|
const ext = this.config.generator === 'javascript' ? 'js' : 'ts';
|
|
150
|
-
const filePath =
|
|
212
|
+
const filePath = this.resolveOutputPath(`api.${ext}`);
|
|
151
213
|
const content = this.generateApiFileContent(apis, types);
|
|
152
214
|
this.writeGeneratedFile(filePath, content);
|
|
153
215
|
}
|
|
@@ -169,33 +231,50 @@ class CodeGenerator {
|
|
|
169
231
|
]),
|
|
170
232
|
importTemplate + ';'
|
|
171
233
|
];
|
|
234
|
+
const extractQueryParameterTypes = this.shouldExtractQueryParameterTypes();
|
|
235
|
+
const queryParameterTypes = extractQueryParameterTypes
|
|
236
|
+
? (0, query_parameter_generator_1.generateQueryParameterTypes)(apis, types.map((type) => type.name))
|
|
237
|
+
: [];
|
|
238
|
+
const queryParameterTypeNames = queryParameterTypes.map((type) => type.name);
|
|
172
239
|
// 收集当前文件实际使用的类型
|
|
173
|
-
const usedTypes = this.collectUsedTypes(apis, types);
|
|
240
|
+
const usedTypes = this.collectUsedTypes(apis, types, extractQueryParameterTypes);
|
|
241
|
+
const queryParameterTypeByApi = new Map(queryParameterTypes.map((type) => [type.api, type.name]));
|
|
174
242
|
// 添加类型导入(仅在 TypeScript 且生成类型文件时)
|
|
175
243
|
if (this.config.generator === 'typescript' &&
|
|
176
244
|
this.config.options?.generateModels !== false &&
|
|
177
|
-
usedTypes.length > 0) {
|
|
178
|
-
const typeNames =
|
|
245
|
+
(usedTypes.length > 0 || (!tag && queryParameterTypeNames.length > 0))) {
|
|
246
|
+
const typeNames = Array.from(new Set([...usedTypes, ...(tag ? [] : queryParameterTypeNames)]))
|
|
247
|
+
.sort()
|
|
248
|
+
.join(', ');
|
|
179
249
|
const typesPath = tag ? '../types' : './types';
|
|
180
250
|
header.push(`import type { ${typeNames} } from '${typesPath}';`);
|
|
181
251
|
}
|
|
252
|
+
if (tag && queryParameterTypeNames.length > 0) {
|
|
253
|
+
const typeNames = queryParameterTypeNames.sort().join(', ');
|
|
254
|
+
header.push(`import type { ${typeNames} } from './types';`);
|
|
255
|
+
}
|
|
182
256
|
header.push('');
|
|
183
257
|
const apiImplementations = apis
|
|
184
|
-
.map((api) => this.generateApiFunction(api
|
|
258
|
+
.map((api) => this.generateApiFunction(api, extractQueryParameterTypes
|
|
259
|
+
? queryParameterTypeByApi.get(api)
|
|
260
|
+
: undefined))
|
|
185
261
|
.join('\n\n');
|
|
186
262
|
return `${header.join('\n')}${apiImplementations}\n`;
|
|
187
263
|
}
|
|
188
264
|
/**
|
|
189
265
|
* 生成单个API函数
|
|
190
266
|
* @param api API接口信息
|
|
267
|
+
* @param queryParameterTypeName 查询参数类型名
|
|
191
268
|
* @returns API函数代码
|
|
192
269
|
*/
|
|
193
|
-
generateApiFunction(api) {
|
|
270
|
+
generateApiFunction(api, queryParameterTypeName) {
|
|
194
271
|
const parts = [];
|
|
195
272
|
// 生成注释
|
|
196
273
|
if (this.config.options?.addComments !== false) {
|
|
274
|
+
const pathParameterNames = this.createPathParameterNames(api.parameters.filter((parameter) => parameter.in === 'path'));
|
|
275
|
+
let pathParameterIndex = 0;
|
|
197
276
|
const swaggerParams = api.parameters.map((p) => ({
|
|
198
|
-
name: p.name,
|
|
277
|
+
name: p.in === 'path' ? pathParameterNames[pathParameterIndex++] : p.name,
|
|
199
278
|
in: p.in,
|
|
200
279
|
required: p.required,
|
|
201
280
|
type: p.type,
|
|
@@ -213,7 +292,7 @@ class CodeGenerator {
|
|
|
213
292
|
schema: p.schema || { type: p.type }
|
|
214
293
|
}));
|
|
215
294
|
// 生成直接参数形式
|
|
216
|
-
const functionParams = this.generateDirectParameters(swaggerParameters, this.config.generator === 'javascript');
|
|
295
|
+
const functionParams = this.generateDirectParameters(swaggerParameters, this.config.generator === 'javascript', queryParameterTypeName);
|
|
217
296
|
const responseType = this.config.options?.generateModels === false
|
|
218
297
|
? 'any'
|
|
219
298
|
: api.responseType || 'any';
|
|
@@ -240,31 +319,41 @@ class CodeGenerator {
|
|
|
240
319
|
/**
|
|
241
320
|
* 生成直接参数形式
|
|
242
321
|
* @param parameters OpenAPI 参数数组
|
|
322
|
+
* @param isJavaScript 是否生成 JavaScript 参数
|
|
323
|
+
* @param queryParameterTypeName 查询参数类型名
|
|
243
324
|
* @returns 函数参数字符串
|
|
244
325
|
*/
|
|
245
|
-
generateDirectParameters(parameters, isJavaScript = false) {
|
|
326
|
+
generateDirectParameters(parameters, isJavaScript = false, queryParameterTypeName) {
|
|
246
327
|
const params = [];
|
|
247
328
|
const queryParams = parameters.filter((p) => p.in === 'query');
|
|
248
329
|
const pathParams = parameters.filter((p) => p.in === 'path');
|
|
249
330
|
const bodyParams = parameters.filter((p) => p.in === 'body');
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
331
|
+
const bodyParam = bodyParams[0];
|
|
332
|
+
const hasRequiredBody = bodyParam?.required === true;
|
|
333
|
+
const pathParameterNames = this.createPathParameterNames(pathParams);
|
|
334
|
+
pathParams.forEach((parameter, index) => {
|
|
335
|
+
const parameterName = pathParameterNames[index];
|
|
336
|
+
if (isJavaScript) {
|
|
337
|
+
params.push(parameterName);
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
params.push(`${parameterName}: ${this.getSafeGeneratedType(parameter.type)}`);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
if (queryParams.length > 0) {
|
|
253
344
|
if (isJavaScript) {
|
|
254
345
|
params.push('params');
|
|
255
346
|
}
|
|
256
347
|
else {
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const optionalModifier = allOptional ? '?' : '';
|
|
267
|
-
params.push(`params${optionalModifier}: { ${paramType} }`);
|
|
348
|
+
const allOptional = queryParams.every((p) => !p.required);
|
|
349
|
+
const optionalModifier = allOptional && !hasRequiredBody ? '?' : '';
|
|
350
|
+
if (queryParameterTypeName) {
|
|
351
|
+
params.push(`params${optionalModifier}: ${queryParameterTypeName}`);
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
const paramType = this.renderInlineQueryParameterMembers(queryParams);
|
|
355
|
+
params.push(`params${optionalModifier}: { ${paramType} }`);
|
|
356
|
+
}
|
|
268
357
|
}
|
|
269
358
|
}
|
|
270
359
|
// 请求体参数
|
|
@@ -273,28 +362,15 @@ class CodeGenerator {
|
|
|
273
362
|
params.push('data');
|
|
274
363
|
}
|
|
275
364
|
else {
|
|
276
|
-
const
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
: bodyParam.type;
|
|
280
|
-
params.push(`data: ${this.getSafeGeneratedType(bodyType)}`);
|
|
365
|
+
const bodyType = bodyParam.type;
|
|
366
|
+
const optionalModifier = bodyParam.required ? '' : '?';
|
|
367
|
+
params.push(`data${optionalModifier}: ${this.getSafeGeneratedType(bodyType)}`);
|
|
281
368
|
}
|
|
282
369
|
}
|
|
283
370
|
// 添加可选的config参数
|
|
284
371
|
params.push(isJavaScript ? 'config' : 'config?: any');
|
|
285
372
|
return params.join(', ');
|
|
286
373
|
}
|
|
287
|
-
/**
|
|
288
|
-
* 从schema获取类型
|
|
289
|
-
* @param schema Swagger schema
|
|
290
|
-
* @returns 类型字符串
|
|
291
|
-
*/
|
|
292
|
-
getTypeFromSchema(schema) {
|
|
293
|
-
if (this.config.options?.generateModels === false) {
|
|
294
|
-
return 'any';
|
|
295
|
-
}
|
|
296
|
-
return (0, utils_1.swaggerTypeToTsType)(schema);
|
|
297
|
-
}
|
|
298
374
|
/**
|
|
299
375
|
* 获取当前配置下可安全输出的类型
|
|
300
376
|
* @param type 类型字符串
|
|
@@ -307,13 +383,30 @@ class CodeGenerator {
|
|
|
307
383
|
return type;
|
|
308
384
|
return this.isPrimitiveType(type) ? type : 'any';
|
|
309
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* 渲染内联查询参数类型成员
|
|
388
|
+
* @param parameters 查询参数数组
|
|
389
|
+
* @returns 内联类型成员
|
|
390
|
+
*/
|
|
391
|
+
renderInlineQueryParameterMembers(parameters) {
|
|
392
|
+
return parameters
|
|
393
|
+
.map((parameter) => {
|
|
394
|
+
const optional = parameter.required ? '' : '?';
|
|
395
|
+
let type = this.getSafeGeneratedType(parameter.type);
|
|
396
|
+
if (optional)
|
|
397
|
+
type = (0, utils_1.stripNullFromUnion)(type);
|
|
398
|
+
return `${(0, utils_1.formatTsPropertyName)(parameter.name)}${optional}: ${type}`;
|
|
399
|
+
})
|
|
400
|
+
.join(', ');
|
|
401
|
+
}
|
|
310
402
|
/**
|
|
311
403
|
* 收集API数组中实际使用的类型
|
|
312
404
|
* @param apis API接口数组
|
|
313
405
|
* @param definedTypes 已定义的类型数组
|
|
406
|
+
* @param excludeQueryParameters 是否排除查询参数
|
|
314
407
|
* @returns 使用的类型名称数组
|
|
315
408
|
*/
|
|
316
|
-
collectUsedTypes(apis, definedTypes) {
|
|
409
|
+
collectUsedTypes(apis, definedTypes, excludeQueryParameters = false) {
|
|
317
410
|
const usedTypes = new Set();
|
|
318
411
|
apis.forEach((api) => {
|
|
319
412
|
// 收集响应类型
|
|
@@ -325,17 +418,15 @@ class CodeGenerator {
|
|
|
325
418
|
}
|
|
326
419
|
// 收集参数类型
|
|
327
420
|
api.parameters.forEach((param) => {
|
|
328
|
-
if (param.
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
421
|
+
if (excludeQueryParameters && param.in === 'query')
|
|
422
|
+
return;
|
|
423
|
+
if (param.type && param.type !== 'any') {
|
|
424
|
+
if (!this.isPrimitiveType(param.type)) {
|
|
425
|
+
this.extractTypeNames(param.type).forEach((typeName) => {
|
|
332
426
|
usedTypes.add(typeName);
|
|
333
427
|
});
|
|
334
428
|
}
|
|
335
429
|
}
|
|
336
|
-
else if (param.type && !this.isPrimitiveType(param.type)) {
|
|
337
|
-
usedTypes.add(param.type);
|
|
338
|
-
}
|
|
339
430
|
});
|
|
340
431
|
});
|
|
341
432
|
// 如果提供了 definedTypes,则只保留已定义的类型
|
|
@@ -378,29 +469,13 @@ class CodeGenerator {
|
|
|
378
469
|
'object',
|
|
379
470
|
'array',
|
|
380
471
|
'any',
|
|
472
|
+
'blob',
|
|
381
473
|
'void',
|
|
382
474
|
'null',
|
|
383
475
|
'undefined'
|
|
384
476
|
];
|
|
385
477
|
return primitiveTypes.includes(type.toLowerCase());
|
|
386
478
|
}
|
|
387
|
-
/**
|
|
388
|
-
* 检测是否为通用响应容器类型
|
|
389
|
-
* @param type 类型信息
|
|
390
|
-
* @param definition 类型定义
|
|
391
|
-
* @returns 是否为通用响应容器类型
|
|
392
|
-
*/
|
|
393
|
-
isGenericResponseContainer(type, definition) {
|
|
394
|
-
// 检查是否为接口定义
|
|
395
|
-
if (!definition.includes(`export interface ${type.name} {`)) {
|
|
396
|
-
return false;
|
|
397
|
-
}
|
|
398
|
-
// 检查是否包含 data 字段且类型为 Record<string, any>
|
|
399
|
-
const hasDataField = definition.includes('data: Record<string, any>;');
|
|
400
|
-
// 检查是否包含其他常见的响应容器字段
|
|
401
|
-
const hasCommonFields = ['code', 'message', 'success', 'status'].some((field) => new RegExp(`\\b${field}\\??:`).test(definition));
|
|
402
|
-
return hasDataField && hasCommonFields;
|
|
403
|
-
}
|
|
404
479
|
/**
|
|
405
480
|
* 生成请求配置
|
|
406
481
|
* @param api API接口信息
|
|
@@ -416,26 +491,21 @@ class CodeGenerator {
|
|
|
416
491
|
}
|
|
417
492
|
const pathParams = api.parameters.filter((p) => p.in === 'path');
|
|
418
493
|
const queryParams = api.parameters.filter((p) => p.in === 'query');
|
|
494
|
+
const pathParameterNames = this.createPathParameterNames(pathParams);
|
|
419
495
|
if (pathParams.length > 0) {
|
|
420
|
-
|
|
421
|
-
pathParams.forEach((param) => {
|
|
422
|
-
|
|
496
|
+
url = this.escapeTemplateLiteral(url);
|
|
497
|
+
pathParams.forEach((param, index) => {
|
|
498
|
+
const placeholder = this.escapeTemplateLiteral(`{${param.name}}`);
|
|
499
|
+
const replacement = `\${${pathParameterNames[index]}}`;
|
|
500
|
+
url = url.split(placeholder).join(replacement);
|
|
423
501
|
});
|
|
424
502
|
config.push(`url: \`${url}\``);
|
|
425
503
|
}
|
|
426
504
|
else {
|
|
427
|
-
config.push(`url:
|
|
505
|
+
config.push(`url: ${this.toSingleQuotedString(url)}`);
|
|
428
506
|
}
|
|
429
|
-
// 查询参数和路径参数都从params对象中获取
|
|
430
507
|
if (queryParams.length > 0) {
|
|
431
|
-
|
|
432
|
-
const queryEntries = queryParams.map((param) => `${JSON.stringify(param.name)}: ${this.getParamAccessExpression(param.name)}`);
|
|
433
|
-
config.push(`params: { ${queryEntries.join(', ')} }`);
|
|
434
|
-
}
|
|
435
|
-
else {
|
|
436
|
-
// 直接传递params对象,让axios自动过滤undefined值
|
|
437
|
-
config.push('params');
|
|
438
|
-
}
|
|
508
|
+
config.push('params');
|
|
439
509
|
}
|
|
440
510
|
// 请求体数据
|
|
441
511
|
const bodyParams = api.parameters.filter((p) => p.in === 'body');
|
|
@@ -451,14 +521,26 @@ class CodeGenerator {
|
|
|
451
521
|
return `{\n ${config.join(',\n ')}\n }`;
|
|
452
522
|
}
|
|
453
523
|
/**
|
|
454
|
-
*
|
|
455
|
-
* @param
|
|
456
|
-
* @returns
|
|
524
|
+
* 创建不重复的路径参数变量名
|
|
525
|
+
* @param parameters OpenAPI 路径参数
|
|
526
|
+
* @returns 路径参数变量名
|
|
457
527
|
*/
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
528
|
+
createPathParameterNames(parameters) {
|
|
529
|
+
const usedNames = new Set(['params', 'data', 'config']);
|
|
530
|
+
return parameters.map((parameter, index) => {
|
|
531
|
+
const normalizedName = parameter.name.replace(/[^A-Za-z0-9_$]/g, '_');
|
|
532
|
+
const baseName = (0, utils_1.isValidIdentifier)(normalizedName)
|
|
533
|
+
? normalizedName
|
|
534
|
+
: `pathParam${index + 1}`;
|
|
535
|
+
let parameterName = baseName;
|
|
536
|
+
let suffix = 2;
|
|
537
|
+
while (usedNames.has(parameterName)) {
|
|
538
|
+
parameterName = `${baseName}_${suffix}`;
|
|
539
|
+
suffix++;
|
|
540
|
+
}
|
|
541
|
+
usedNames.add(parameterName);
|
|
542
|
+
return parameterName;
|
|
543
|
+
});
|
|
462
544
|
}
|
|
463
545
|
/**
|
|
464
546
|
* 生成入口文件
|
|
@@ -476,7 +558,7 @@ class CodeGenerator {
|
|
|
476
558
|
// 按标签导出
|
|
477
559
|
for (const tag of groupedApis.keys()) {
|
|
478
560
|
const folderName = this.getTagFileName(tag);
|
|
479
|
-
exports.push(`export * from
|
|
561
|
+
exports.push(`export * from ${this.toSingleQuotedString(`./${folderName}`)};`);
|
|
480
562
|
}
|
|
481
563
|
}
|
|
482
564
|
else {
|
|
@@ -486,9 +568,31 @@ class CodeGenerator {
|
|
|
486
568
|
}
|
|
487
569
|
const content = [this.generateHeader(), '', ...exports, ''].join('\n');
|
|
488
570
|
const ext = this.config.generator === 'javascript' ? 'js' : 'ts';
|
|
489
|
-
const filePath =
|
|
571
|
+
const filePath = this.resolveOutputPath(`index.${ext}`);
|
|
490
572
|
this.writeGeneratedFile(filePath, content);
|
|
491
573
|
}
|
|
574
|
+
/**
|
|
575
|
+
* 校验输出目录不会覆盖当前工作目录或其父目录
|
|
576
|
+
*/
|
|
577
|
+
assertSafeOutputDirectory() {
|
|
578
|
+
const currentDirectory = path.resolve(process.cwd());
|
|
579
|
+
if (this.outputRoot === currentDirectory ||
|
|
580
|
+
(0, utils_1.isPathInside)(this.outputRoot, currentDirectory)) {
|
|
581
|
+
throw new Error('输出目录不能是当前工作目录或其父目录');
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* 解析并校验输出目录内的文件路径
|
|
586
|
+
* @param segments 输出目录下的路径片段
|
|
587
|
+
* @returns 安全的绝对路径
|
|
588
|
+
*/
|
|
589
|
+
resolveOutputPath(...segments) {
|
|
590
|
+
const targetPath = path.resolve(this.outputRoot, ...segments);
|
|
591
|
+
if (!(0, utils_1.isPathInside)(this.outputRoot, targetPath)) {
|
|
592
|
+
throw new Error(`生成路径超出输出目录: ${targetPath}`);
|
|
593
|
+
}
|
|
594
|
+
return targetPath;
|
|
595
|
+
}
|
|
492
596
|
/**
|
|
493
597
|
* 写入生成文件,overwrite=false 时保留已存在文件
|
|
494
598
|
* @param filePath 文件路径
|
|
@@ -507,17 +611,68 @@ class CodeGenerator {
|
|
|
507
611
|
*/
|
|
508
612
|
getTagFileName(tag) {
|
|
509
613
|
const cleanTag = (0, utils_1.sanitizeFilename)(tag);
|
|
614
|
+
let folderName;
|
|
510
615
|
switch (this.config.tagGrouping?.fileNaming) {
|
|
511
616
|
case 'camelCase':
|
|
512
|
-
|
|
617
|
+
folderName = (0, utils_1.toCamelCase)(cleanTag);
|
|
618
|
+
break;
|
|
513
619
|
case 'kebab-case':
|
|
514
|
-
|
|
620
|
+
folderName = (0, utils_1.toKebabCase)(cleanTag);
|
|
621
|
+
break;
|
|
515
622
|
case 'tag':
|
|
516
|
-
|
|
623
|
+
folderName = cleanTag.toLowerCase();
|
|
624
|
+
break;
|
|
517
625
|
default:
|
|
518
|
-
|
|
626
|
+
folderName = (0, utils_1.toCamelCase)(cleanTag);
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
if (!folderName || folderName === '.' || folderName === '..') {
|
|
630
|
+
throw new Error(`无效的标签名称: ${JSON.stringify(tag)}`);
|
|
631
|
+
}
|
|
632
|
+
return folderName;
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* 校验所有标签生成的目录名称安全且不重复
|
|
636
|
+
* @param groupedApis 按标签分组的 API
|
|
637
|
+
*/
|
|
638
|
+
validateTagFolders(groupedApis) {
|
|
639
|
+
const folderOwners = new Map();
|
|
640
|
+
for (const tag of groupedApis.keys()) {
|
|
641
|
+
const folderName = this.getTagFileName(tag);
|
|
642
|
+
const normalizedFolderName = folderName.toLowerCase();
|
|
643
|
+
const existingTag = folderOwners.get(normalizedFolderName);
|
|
644
|
+
if (existingTag !== undefined && existingTag !== tag) {
|
|
645
|
+
throw new Error(`标签目录名称冲突: ${existingTag} 和 ${tag} 都会生成 ${folderName}`);
|
|
646
|
+
}
|
|
647
|
+
folderOwners.set(normalizedFolderName, tag);
|
|
519
648
|
}
|
|
520
649
|
}
|
|
650
|
+
/**
|
|
651
|
+
* 转义模板字符串中的特殊字符
|
|
652
|
+
* @param value 原始字符串
|
|
653
|
+
* @returns 可安全写入模板字符串的内容
|
|
654
|
+
*/
|
|
655
|
+
escapeTemplateLiteral(value) {
|
|
656
|
+
return value
|
|
657
|
+
.replace(/\\/g, '\\\\')
|
|
658
|
+
.replace(/`/g, '\\`')
|
|
659
|
+
.replace(/\$\{/g, '\\${');
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* 将字符串转换为安全的单引号字面量
|
|
663
|
+
* @param value 原始字符串
|
|
664
|
+
* @returns 单引号字符串字面量
|
|
665
|
+
*/
|
|
666
|
+
toSingleQuotedString(value) {
|
|
667
|
+
const escapedValue = value
|
|
668
|
+
.replace(/\\/g, '\\\\')
|
|
669
|
+
.replace(/'/g, "\\'")
|
|
670
|
+
.replace(/\r/g, '\\r')
|
|
671
|
+
.replace(/\n/g, '\\n')
|
|
672
|
+
.replace(/\u2028/g, '\\u2028')
|
|
673
|
+
.replace(/\u2029/g, '\\u2029');
|
|
674
|
+
return `'${escapedValue}'`;
|
|
675
|
+
}
|
|
521
676
|
/**
|
|
522
677
|
* 生成文件头部注释
|
|
523
678
|
* @param defaultHeader 默认头部注释
|
package/dist/core/parser.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { SwaggerDocument, ApiInfo, TypeInfo, SwaggerConfig } from '../types';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* OpenAPI 3.0 文档解析器
|
|
4
4
|
*/
|
|
5
5
|
export declare class SwaggerParser {
|
|
6
6
|
private document;
|
|
7
7
|
private config;
|
|
8
|
+
/**
|
|
9
|
+
* 创建 OpenAPI 文档解析器
|
|
10
|
+
* @param document OpenAPI 文档
|
|
11
|
+
* @param config 生成配置
|
|
12
|
+
*/
|
|
8
13
|
constructor(document: SwaggerDocument, config: SwaggerConfig);
|
|
9
14
|
/**
|
|
10
15
|
* 获取 OpenAPI components.schemas
|
|
@@ -30,27 +35,20 @@ export declare class SwaggerParser {
|
|
|
30
35
|
* @returns 类型信息数组
|
|
31
36
|
*/
|
|
32
37
|
parseTypes(): TypeInfo[];
|
|
33
|
-
/**
|
|
34
|
-
* 解析单个类型定义
|
|
35
|
-
* @param name 类型名称
|
|
36
|
-
* @param schema 模式对象
|
|
37
|
-
* @returns 类型信息
|
|
38
|
-
*/
|
|
39
|
-
private parseTypeDefinition;
|
|
40
|
-
/**
|
|
41
|
-
* 获取枚举成员名称
|
|
42
|
-
* @param schema 枚举 schema
|
|
43
|
-
* @param value 枚举值
|
|
44
|
-
* @param index 枚举值索引
|
|
45
|
-
* @returns 枚举成员名称
|
|
46
|
-
*/
|
|
47
|
-
private getEnumMemberName;
|
|
48
38
|
/**
|
|
49
39
|
* 解析本地 $ref 引用对象
|
|
50
40
|
* @param value 可能带有 $ref 的对象
|
|
41
|
+
* @param seenRefs 已访问的引用
|
|
42
|
+
* @param depth 当前解析深度
|
|
51
43
|
* @returns 引用解析后的对象
|
|
52
44
|
*/
|
|
53
45
|
private resolveReference;
|
|
46
|
+
/**
|
|
47
|
+
* 解析响应对象中的本地引用
|
|
48
|
+
* @param responses OpenAPI 响应集合
|
|
49
|
+
* @returns 已解析的响应集合
|
|
50
|
+
*/
|
|
51
|
+
private resolveResponses;
|
|
54
52
|
/**
|
|
55
53
|
* 合并同名同位置参数,后出现的 operation 级参数覆盖 path 级参数
|
|
56
54
|
* @param parameters 参数数组
|