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.
@@ -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.config.output);
71
+ (0, utils_1.removeDirectory)(this.outputRoot);
61
72
  }
62
73
  // 确保输出目录存在
63
- (0, utils_1.ensureDirectoryExists)(this.config.output);
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
- await this.generateTypesFile(types);
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 = path.join(this.config.output, 'types.ts');
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
- return `${header}\n\n${typeDefinitions}\n`;
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 = path.join(this.config.output, folderName);
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 = path.join(this.config.output, `api.${ext}`);
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); // 传入 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 = usedTypes.join(', ');
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
- // 合并路径参数和查询参数为一个params对象
251
- const allParams = [...pathParams, ...queryParams];
252
- if (allParams.length > 0) {
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 paramType = allParams
258
- .map((p) => {
259
- const optional = p.required ? '' : '?';
260
- const type = this.getSafeGeneratedType(p.type);
261
- return `${(0, utils_1.formatTsPropertyName)(p.name)}${optional}: ${type}`;
262
- })
263
- .join(', ');
264
- // 检查是否所有参数都是可选的
265
- const allOptional = allParams.every((p) => !p.required);
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 bodyParam = bodyParams[0];
277
- const bodyType = bodyParam.schema
278
- ? this.getTypeFromSchema(bodyParam.schema)
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.schema) {
329
- const type = this.getTypeFromSchema(param.schema);
330
- if (type && type !== 'any' && !this.isPrimitiveType(type)) {
331
- this.extractTypeNames(type).forEach((typeName) => {
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
- // 替换路径参数,从params对象中获取
421
- pathParams.forEach((param) => {
422
- url = url.replace(`{${param.name}}`, `\${params.${param.name}}`);
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: '${url}'`);
505
+ config.push(`url: ${this.toSingleQuotedString(url)}`);
428
506
  }
429
- // 查询参数和路径参数都从params对象中获取
430
507
  if (queryParams.length > 0) {
431
- if (pathParams.length > 0) {
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
- * 生成 params 对象字段访问表达式
455
- * @param name 参数名
456
- * @returns 字段访问表达式
524
+ * 创建不重复的路径参数变量名
525
+ * @param parameters OpenAPI 路径参数
526
+ * @returns 路径参数变量名
457
527
  */
458
- getParamAccessExpression(name) {
459
- return (0, utils_1.isValidIdentifier)(name)
460
- ? `params.${name}`
461
- : `params[${JSON.stringify(name)}]`;
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 './${folderName}';`);
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 = path.join(this.config.output, `index.${ext}`);
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
- return (0, utils_1.toCamelCase)(cleanTag);
617
+ folderName = (0, utils_1.toCamelCase)(cleanTag);
618
+ break;
513
619
  case 'kebab-case':
514
- return (0, utils_1.toKebabCase)(cleanTag);
620
+ folderName = (0, utils_1.toKebabCase)(cleanTag);
621
+ break;
515
622
  case 'tag':
516
- return cleanTag.toLowerCase();
623
+ folderName = cleanTag.toLowerCase();
624
+ break;
517
625
  default:
518
- return (0, utils_1.toCamelCase)(cleanTag);
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 默认头部注释
@@ -1,10 +1,15 @@
1
1
  import { SwaggerDocument, ApiInfo, TypeInfo, SwaggerConfig } from '../types';
2
2
  /**
3
- * Swagger文档解析器
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 参数数组