swagger_generator_flutter/lib/commands/generate_command.dart

1091 lines
37 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:swagger_generator_flutter/commands/base_command.dart';
import 'package:swagger_generator_flutter/core/config.dart';
import 'package:swagger_generator_flutter/core/config_loader.dart';
import 'package:swagger_generator_flutter/core/models.dart';
import 'package:swagger_generator_flutter/generators/model_code_generator.dart';
import 'package:swagger_generator_flutter/generators/retrofit_api_generator.dart';
import 'package:swagger_generator_flutter/parsers/swagger_data_parser.dart';
import 'package:swagger_generator_flutter/utils/file_utils.dart';
/// Generate命令
/// 用于生成各种代码文件
class GenerateCommand extends BaseCommand {
@override
String get name => 'generate';
@override
String get description => '生成API代码文件模型、端点等';
@override
String get usage => 'dart swagger_cli.dart generate [options]';
@override
List<CommandOption> get options => [
const CommandOption(
name: 'models',
shortName: 'm',
description: '生成数据模型',
),
const CommandOption(
name: 'api',
shortName: 'r',
description: '生成Retrofit风格API接口',
),
const CommandOption(
name: 'split-by-tags',
shortName: 't',
description: '按tags分组生成多个API文件默认启用',
),
const CommandOption(
name: 'all',
shortName: 'a',
description: '生成所有文件(默认)',
),
const CommandOption(
name: 'output-dir',
shortName: 'o',
description: '输出目录',
type: OptionType.string,
defaultValue: 'generator',
),
const CommandOption(
name: 'included-tags',
shortName: 'i',
description: '只生成指定tags的API和模型逗号分隔User,Pet,Store',
type: OptionType.string,
),
const CommandOption(
name: 'excluded-tags',
shortName: 'e',
description: '从生成中排除指定的tags逗号分隔',
type: OptionType.string,
),
];
@override
Future<int> execute(List<String> args) async {
try {
final parsedArgs = parseArguments(args);
validateArguments(parsedArgs);
await prepare(parsedArgs);
// 解析所有 Swagger 文档
progress('正在解析 ${SwaggerConfig.swaggerJsonUrls.length} 个 Swagger 文档...');
final parser = SwaggerDataParser();
// 合并所有文档的 paths 和 models
// 注意:合并策略是后面的文档会覆盖前面的同名模型和路径
// 因此建议将高版本(如 V2配置在低版本如 V1之后以确保高版本的模型覆盖低版本
SwaggerDocument? mergedDocument;
final urls = SwaggerConfig.swaggerJsonUrls;
progress('URL 处理顺序: ${urls.join(" -> ")}');
for (var i = 0; i < urls.length; i++) {
final url = urls[i];
progress(' [${i + 1}/${urls.length}] 正在解析: $url');
final doc = await parser.fetchAndParseSwaggerDocument(url);
progress(' 解析完成: ${doc.models.length} 个模型, ${doc.paths.length} 个路径');
if (mergedDocument == null) {
mergedDocument = doc;
progress(' 初始文档: ${doc.models.length} 个模型');
} else {
// 合并 paths 和 models
// 重要:使用 {...mergedDocument.models, ...doc.models}
// 后面的 doc.models 会覆盖前面的 mergedDocument.models 中的同名 key
// 这样可以确保高版本的模型定义覆盖低版本的模型定义
final beforeModelCount = mergedDocument.models.length;
final currentModelCount = doc.models.length;
// 找出会被覆盖的模型(同名模型)
final overlappingModels = <String>[];
for (final key in doc.models.keys) {
if (mergedDocument.models.containsKey(key)) {
overlappingModels.add(key);
}
}
if (overlappingModels.isNotEmpty) {
progress(
' 发现 ${overlappingModels.length} 个同名模型将被覆盖: ${overlappingModels.take(5).join(", ")}${overlappingModels.length > 5 ? "..." : ""}',
);
}
mergedDocument = SwaggerDocument(
title: mergedDocument.title,
description: mergedDocument.description,
version: '${mergedDocument.version} + ${doc.version}',
// 注意:后面的 doc 会覆盖前面的 mergedDocument 中的同名 key
// 确保 V2 的模型覆盖 V1 的同名模型
paths: {...mergedDocument.paths, ...doc.paths},
models: {...mergedDocument.models, ...doc.models},
controllers: {...mergedDocument.controllers, ...doc.controllers},
);
final afterModelCount = mergedDocument.models.length;
progress(
' 合并后: $beforeModelCount + $currentModelCount -> $afterModelCount 个模型',
);
// 验证同名模型是否被正确覆盖
if (overlappingModels.isNotEmpty) {
progress(
' 同名模型列表: ${overlappingModels.take(10).join(", ")}${overlappingModels.length > 10 ? "..." : ""}',
);
}
}
}
if (mergedDocument == null) {
print('❌ 没有成功解析任何 Swagger 文档');
return 1;
}
success('成功合并 ${SwaggerConfig.swaggerJsonUrls.length} 个 Swagger 文档');
// 解析生成选项
final options = _parseGenerateOptions(parsedArgs);
// 根据 includedTags 和 excludedTags 过滤文档
final document = _filterDocumentByTags(
mergedDocument,
options.includedTags,
options.excludedTags,
);
// 使用配置的输出目录
final baseDir = SwaggerConfig.generatorDir;
final apiDir = SwaggerConfig.apiDir;
final modelsDir = SwaggerConfig.modelsDir;
progress('输出目录: $baseDir');
progress('API 目录: $apiDir');
progress('模型目录: $modelsDir');
// 确保输出目录存在
await FileUtils.ensureDirectoryExists(baseDir);
await FileUtils.ensureDirectoryExists(apiDir);
await FileUtils.ensureDirectoryExists(modelsDir);
var generatedFiles = 0;
// 生成模型代码
if (options.generateModels) {
progress('正在生成数据模型...');
final generator = ModelCodeGenerator(document);
await FileUtils.ensureDirectoryExists(modelsDir);
final modelFiles = generator.generateSeparateModelFiles();
for (final entry in modelFiles.entries) {
final filePath = '$modelsDir/${entry.key}';
// 检查是否跳过此文件
if (ConfigLoader.shouldSkipFile(filePath)) {
progress('跳过文件: $filePath');
continue;
}
await FileUtils.writeFile(filePath, entry.value);
success('模型文件已保存到: $filePath');
generatedFiles++;
}
}
// 生成 Retrofit 风格的 API 接口(默认使用拆分模式)
if (options.generateApi) {
progress('正在按版本和tags分组生成Retrofit风格API接口...');
await FileUtils.ensureDirectoryExists(apiDir);
// 🎯 先按版本分组 paths
final pathsByVersion = <String, List<ApiPath>>{};
for (final path in document.paths.values) {
final version = _extractVersionFromPath(path.path);
pathsByVersion.putIfAbsent(version, () => []).add(path);
}
progress(
'检测到 ${pathsByVersion.keys.length} 个版本: ${pathsByVersion.keys.join(", ")}',
);
// ✨ 按版本分别生成 API 文件
final versionedFiles = <String, Map<String, String>>{};
for (final versionEntry in pathsByVersion.entries) {
final version = versionEntry.key;
final versionPaths = versionEntry.value;
progress(' 正在生成 $version 版本 API${versionPaths.length} 个接口)...');
// 筛选出当前版本实际使用的 controllers
final versionTags = versionPaths.expand((p) => p.tags).toSet();
final versionControllers = {
for (final tag in versionTags)
if (document.controllers.containsKey(tag))
tag: document.controllers[tag]!,
};
// 创建该版本的临时文档
final versionDocument = SwaggerDocument(
title: document.title,
description: document.description,
version: document.version,
paths: {for (final p in versionPaths) p.path: p},
models: document.models,
controllers: versionControllers, // 使用过滤后的 controllers
);
// 创建生成器(使用配置的类名)
final apiClientClassName = ConfigLoader.getApiClientClassName();
final generator = RetrofitApiGenerator(
className: apiClientClassName,
);
generator.document = versionDocument;
generator.ensureParameterEntitiesGenerated();
// 生成该版本的 API 文件
final tagApiFiles = generator.generateApiFilesByTags();
versionedFiles[version] = {};
for (final entry in tagApiFiles.entries) {
final fileName = entry.key;
var code = entry.value;
// 添加版本后缀到类名
code = _addVersionSuffixToCode(code, version);
versionedFiles[version]![fileName] = code;
}
}
// 按版本写入文件
for (final versionEntry in versionedFiles.entries) {
final version = versionEntry.key;
final files = versionEntry.value;
final versionDir = '$apiDir/$version';
// 检查是否跳过此版本目录
if (ConfigLoader.shouldSkipFile(versionDir)) {
progress('跳过版本目录: $versionDir');
continue;
}
await FileUtils.ensureDirectoryExists(versionDir);
for (final fileEntry in files.entries) {
final fileName = fileEntry.key;
final code = fileEntry.value;
final filePath = '$versionDir/$fileName';
// 检查是否跳过此文件
if (ConfigLoader.shouldSkipFile(filePath)) {
progress('跳过文件: $filePath');
continue;
}
await FileUtils.writeFile(filePath, code);
success('API接口文件已保存到: $filePath');
generatedFiles++;
}
// 生成该版本目录的 index.dart如果目录未被跳过
if (!ConfigLoader.shouldSkipFile(versionDir)) {
await _generateVersionIndexFile(versionDir, files.keys.toList());
success('$version/index.dart 已生成');
}
}
// 生成主 API 文件(使用配置的文件名和类名)
final apiClientClassName = ConfigLoader.getApiClientClassName();
final apiClientFileName = ConfigLoader.getApiClientFileName();
final mainCode = _generateVersionedApiClient(versionedFiles);
final mainFilePath = '$apiDir/$apiClientFileName.dart';
// 检查是否跳过主 API 文件
if (!ConfigLoader.shouldSkipFile(mainFilePath)) {
await FileUtils.writeFile(mainFilePath, mainCode);
success('主API接口文件已保存到: $mainFilePath');
generatedFiles++;
} else {
progress('跳过文件: $mainFilePath');
}
// 生成参数实体类文件(使用最后一个生成器)
final lastGenerator = RetrofitApiGenerator(
className: apiClientClassName,
);
lastGenerator.document = document;
lastGenerator.ensureParameterEntitiesGenerated();
final parameterEntityFiles =
lastGenerator.generateParameterEntityFiles();
if (parameterEntityFiles.isNotEmpty) {
// Parameters 文件放在独立的 parameters 子目录中
final parametersDir = '$modelsDir/parameters';
await FileUtils.ensureDirectoryExists(parametersDir);
for (final entry in parameterEntityFiles.entries) {
final filePath = '$parametersDir/${entry.key}';
// 检查是否跳过此文件
if (ConfigLoader.shouldSkipFile(filePath)) {
progress('跳过文件: $filePath');
continue;
}
await FileUtils.writeFile(filePath, entry.value);
success('参数实体类文件已保存到: $filePath');
generatedFiles++;
}
// 生成 parameters/index.dart
await _generateSubDirectoryIndexFile(parametersDir);
}
}
// 重新生成 index.dart 文件,包含所有生成的文件
if (options.generateModels || options.generateApi) {
progress('正在重新生成 index.dart 文件...');
final allFiles = await _getAllModelFiles(modelsDir);
final indexContent = _generateUpdatedIndexFile(allFiles);
final indexPath = '$modelsDir/index.dart';
await FileUtils.writeFile(indexPath, indexContent);
success('index.dart 文件已更新');
}
// 生成摘要
_generateSummary(document, baseDir);
success('代码生成完成!共生成 $generatedFiles 个文件');
return 0;
} catch (e) {
print('❌ 生成失败: $e');
return 1;
}
}
/// 解析生成选项
GenerateOptions _parseGenerateOptions(ParsedArguments args) {
final hasAnyFlag = args.hasOption('models') || args.hasOption('api');
// 解析 included-tags 参数
// 优先级:命令行参数 > 配置文件
List<String>? includedTags;
final includedTagsStr = args.getOption<String>('included-tags');
if (includedTagsStr != null && includedTagsStr.isNotEmpty) {
// 从命令行参数读取
includedTags = includedTagsStr
.split(',')
.map((tag) => tag.trim())
.where((tag) => tag.isNotEmpty)
.toList();
if (includedTags.isNotEmpty) {
progress('🏷️ [命令行] 只生成以下 tags: ${includedTags.join(", ")}');
}
} else {
// 从配置文件读取
includedTags = ConfigLoader.getIncludedTags();
if (includedTags != null && includedTags.isNotEmpty) {
progress('🏷️ [配置文件] 只生成以下 tags: ${includedTags.join(", ")}');
}
}
// 解析 split-by-tags 参数
// 优先级:命令行参数 > 配置文件 > 默认值(true)
bool splitByTags;
if (args.hasOption('split-by-tags')) {
// 从命令行参数读取
splitByTags = args.getOption<bool>('split-by-tags') ?? true;
progress('📂 [命令行] 按 tags 分组: ${splitByTags ? "" : ""}');
} else {
// 从配置文件读取
splitByTags = ConfigLoader.getSplitByTags();
progress('📂 [配置文件] 按 tags 分组: ${splitByTags ? "" : ""}');
}
// 解析 excluded-tags 参数
// 优先级:命令行参数 > 配置文件
List<String>? excludedTags;
final excludedTagsStr = args.getOption<String>('excluded-tags');
if (excludedTagsStr != null && excludedTagsStr.isNotEmpty) {
// 从命令行参数读取
excludedTags = excludedTagsStr
.split(',')
.map((tag) => tag.trim())
.where((tag) => tag.isNotEmpty)
.toList();
if (excludedTags.isNotEmpty) {
progress('🚫 [命令行] 排除以下 tags: ${excludedTags.join(", ")}');
}
} else {
// 从配置文件读取
excludedTags = ConfigLoader.getExcludedTags();
if (excludedTags != null && excludedTags.isNotEmpty) {
progress('🚫 [配置文件] 排除以下 tags: ${excludedTags.join(", ")}');
}
}
return GenerateOptions(
generateModels: hasAnyFlag
? (args.getOption<bool>('models') ?? false)
: (args.getOption<bool>('all') ?? true),
generateApi: hasAnyFlag
? (args.getOption<bool>('api') ?? false)
: (args.getOption<bool>('all') ?? true),
splitByTags: splitByTags,
includedTags: includedTags,
excludedTags: excludedTags,
);
}
/// 获取所有模型文件列表
Future<List<String>> _getAllModelFiles(String modelsDir) async {
try {
final directory = Directory(modelsDir);
if (!await directory.exists()) {
return [];
}
final files = await directory.list().toList();
final exportPaths = <String>[];
for (final entity in files) {
if (entity is Directory) {
// 如果是子目录,导出子目录的 index.dart
final dirName = path.basename(entity.path);
// 检查子目录是否有 index.dart
final subIndexPath = path.join(entity.path, 'index.dart');
if (await File(subIndexPath).exists()) {
exportPaths.add('$dirName/index.dart');
}
} else if (entity is File && entity.path.endsWith('.dart')) {
final fileName = path.basename(entity.path);
// 排除 index.dart 本身和 .g.dart 文件
if (fileName != 'index.dart' && !fileName.endsWith('.g.dart')) {
// 直接在 api_models 目录下的文件(如 Parameters 文件)
exportPaths.add(fileName);
}
}
}
// 按路径排序:先子目录,后直接文件
exportPaths.sort((a, b) {
final aIsDir = a.contains('/');
final bIsDir = b.contains('/');
if (aIsDir && !bIsDir) return -1;
if (!aIsDir && bIsDir) return 1;
return a.compareTo(b);
});
return exportPaths;
} catch (e) {
print('获取模型文件列表失败: $e');
return [];
}
}
/// 为子目录生成 index.dart 文件
Future<void> _generateSubDirectoryIndexFile(String subDir) async {
try {
final directory = Directory(subDir);
if (!await directory.exists()) return;
final dirName = path.basename(subDir);
// 获取子目录下的所有 .dart 文件
final files = await directory.list().toList();
final dartFiles = <String>[];
for (final entity in files) {
if (entity is File && entity.path.endsWith('.dart')) {
final fileName = path.basename(entity.path);
// 排除 index.dart 本身和 .g.dart 文件
if (fileName != 'index.dart' && !fileName.endsWith('.g.dart')) {
// 检查文件是否在 ignored_files 配置中
final filePath = path.join(subDir, fileName);
if (!ConfigLoader.shouldSkipFile(filePath)) {
dartFiles.add(fileName);
} else {
progress(' 跳过导出文件: $fileName (在 ignored_files 配置中)');
}
}
}
}
// 按文件名排序
dartFiles.sort();
// 生成 index.dart 内容
final buffer = StringBuffer();
buffer.writeln('// 模型导出文件');
buffer.writeln('// 基于 Swagger API 文档: ');
buffer.writeln('// 由 xy_swagger_generator by max 生成');
buffer.writeln('// Copyright (C) 2025 YuanXuan. All rights reserved.');
buffer.writeln();
buffer.writeln();
buffer.writeln('library;');
buffer.writeln();
for (final fileName in dartFiles) {
buffer.writeln("export '$fileName';");
}
// 写入文件
final indexPath = path.join(subDir, 'index.dart');
await FileUtils.writeFile(indexPath, buffer.toString());
success('$dirName/index.dart 已生成,包含 ${dartFiles.length} 个文件');
} catch (e) {
print('生成 index.dart 失败: $e');
}
}
/// 生成更新的 index.dart 文件内容
String _generateUpdatedIndexFile(List<String> fileNames) {
final buffer = StringBuffer();
// 生成文件头
buffer.writeln('// API 模型导出文件');
buffer.writeln('// 基于 Swagger API 文档: ');
buffer.writeln('// 由 xy_swagger_generator by max 生成');
buffer.writeln('// Copyright (C) 2025 YuanXuan. All rights reserved.');
buffer.writeln();
buffer.writeln('library;');
buffer.writeln();
// 导出 base_result 和 base_page_result如果配置了
final baseResultImport = SwaggerConfig.baseResultImport;
final basePageResultImport = SwaggerConfig.basePageResultImport;
if (baseResultImport.isNotEmpty) {
buffer.writeln("export '$baseResultImport';");
}
if (basePageResultImport.isNotEmpty) {
buffer.writeln("export '$basePageResultImport';");
}
if ((baseResultImport.isNotEmpty || basePageResultImport.isNotEmpty) &&
fileNames.isNotEmpty) {
buffer.writeln();
}
// 导出所有文件
for (final fileName in fileNames) {
buffer.writeln("export '$fileName';");
}
return buffer.toString();
}
/// 生成摘要信息
void _generateSummary(SwaggerDocument document, String outputDir) {
final summary = StringBuffer();
summary.writeln('# 代码生成摘要');
summary.writeln();
summary.writeln('**API标题**: ${document.title}');
summary.writeln('**API版本**: ${document.version}');
summary.writeln('**生成时间**: ${DateTime.now().toIso8601String()}');
summary.writeln();
summary.writeln('## 统计信息');
summary.writeln('- 控制器数量: ${document.controllers.length}');
summary.writeln('- API路径数量: ${document.paths.length}');
summary.writeln('- 数据模型数量: ${document.models.length}');
summary.writeln();
summary.writeln('## 控制器列表');
document.controllers.forEach((name, controller) {
summary.writeln(
'- **$name**: ${controller.description} (${controller.paths.length} 个路径)',
);
});
FileUtils.writeFile('$outputDir/SUMMARY.md', summary.toString());
}
/// 从 API 路径中提取版本号
/// 例如: /api/v1/User/GetData → v1
/// /api/v2/User/GetData → v2
/// /api/User/GetData → v1 (默认)
/// 使用配置文件中的版本提取模式
String _extractVersionFromPath(String path) {
// 从配置文件读取版本提取模式
final pattern = ConfigLoader.getVersionExtractionPattern();
final defaultVersion = ConfigLoader.getDefaultVersion();
try {
final versionMatch = RegExp(pattern).firstMatch(path);
if (versionMatch != null && versionMatch.groupCount > 0) {
return 'v${versionMatch.group(1)}';
}
} catch (e) {
// 如果正则表达式无效,使用默认模式
const defaultPattern = r'/api/v(\d+)/';
final versionMatch = RegExp(defaultPattern).firstMatch(path);
if (versionMatch != null) {
return 'v${versionMatch.group(1)}';
}
}
return defaultVersion;
}
/// 为代码中的类名添加版本后缀
/// V1: MobileManagerApi不加后缀
/// V2: MobileManagerApiV2加V2后缀
String _addVersionSuffixToCode(String code, String version) {
// V1 不添加后缀,直接返回
if (version == 'v1') {
return code;
}
final versionUpper = version.toUpperCase(); // v2 → V2, v3 → V3
// 替换 abstract class 声明
code = code.replaceAllMapped(
RegExp(r'abstract class (\w+Api)\b'),
(match) => 'abstract class ${match.group(1)}$versionUpper',
);
// 替换 factory 构造函数
code = code.replaceAllMapped(
RegExp(r'factory (\w+Api)\('),
(match) => 'factory ${match.group(1)}$versionUpper(',
);
// 替换实现类引用 = _XXXApi
code = code.replaceAllMapped(
RegExp(r'= _(\w+Api);'),
(match) => '= _${match.group(1)}$versionUpper;',
);
// 替换 part 文件名
code = code.replaceAllMapped(
RegExp(r"part '(\w+)\.g\.dart';"),
(match) => "part '${match.group(1)}.g.dart';",
);
// 更新 import 路径(如果有引用其他 API
code = code.replaceAllMapped(
RegExp(r"import '../(\w+_api)\.dart';"),
(match) => "import '../$version/${match.group(1)}.dart';",
);
return code;
}
/// 生成版本化的 ApiClient
String _generateVersionedApiClient(
Map<String, Map<String, String>> versionedFiles,
) {
final buffer = StringBuffer();
// 文件头
buffer.writeln('// 统一 API 客户端');
buffer.writeln('// 支持多版本 API 管理');
buffer.writeln('// 由 xy_swagger_generator by max 生成');
buffer.writeln('// Copyright (C) 2025 YuanXuan. All rights reserved.');
buffer.writeln();
buffer.writeln("import 'package:dio/dio.dart';");
buffer.writeln();
// 收集所有 API 类
final apiClasses = <String, Set<String>>{}; // version -> class names
for (final versionEntry in versionedFiles.entries) {
final version = versionEntry.key;
final files = versionEntry.value;
apiClasses[version] = {};
for (final entry in files.entries) {
final fileName = entry.key;
final code = entry.value;
// 优先从代码里解析真正的类名(避免从文件名推断时丢失缩写,如 QRCode -> QCode
final extracted = _extractApiClassNamesFromCode(code);
if (extracted.isNotEmpty) {
apiClasses[version]!.addAll(extracted.toSet());
continue;
}
// 兜底:从文件名提取类名: mobile_manager_api.dart → MobileManagerApi
final className = fileName
.replaceAll('.dart', '')
.split('_')
.map(
(word) => word.isEmpty
? ''
: (word[0].toUpperCase() + word.substring(1)),
)
.join();
apiClasses[version]!.add(className);
}
}
// 导入所有版本的 index.dart
final versions = apiClasses.keys.toList()..sort();
for (final version in versions) {
buffer.writeln("import '$version/index.dart';");
}
buffer.writeln();
// 生成 API Client 类(使用配置的类名)
final apiClientClassName = ConfigLoader.getApiClientClassName();
buffer.writeln('/// 统一 API 客户端');
buffer.writeln('/// 支持多版本 API 访问');
buffer.writeln('class $apiClientClassName {');
buffer.writeln(' final Dio _dio;');
buffer.writeln();
// 生成各版本 API 实例字段
for (final versionEntry in apiClasses.entries) {
final version = versionEntry.key;
final versionUpper =
version == 'v1' ? '' : version.toUpperCase(); // v1不加后缀
for (final className in versionEntry.value) {
final suffix = version == 'v1' ? '' : versionUpper;
buffer.writeln(
' late final $className$suffix _${_toLowerCamelCase(className)}$suffix;',
);
}
}
buffer.writeln();
// 构造函数(使用配置的类名)
buffer.writeln(' $apiClientClassName(this._dio) {');
buffer.writeln(' _initApis();');
buffer.writeln(' }');
buffer.writeln();
// 初始化方法
buffer.writeln(' void _initApis() {');
for (final versionEntry in apiClasses.entries) {
final version = versionEntry.key;
final versionUpper =
version == 'v1' ? '' : version.toUpperCase(); // v1不加后缀
for (final className in versionEntry.value) {
final fieldName = _toLowerCamelCase(className);
final suffix = version == 'v1' ? '' : versionUpper;
buffer.writeln(' _$fieldName$suffix = $className$suffix(_dio);');
}
}
buffer.writeln(' }');
buffer.writeln();
// 生成显式版本访问属性
buffer.writeln(' // ========== 版本化 API 访问 ==========');
buffer.writeln();
for (final versionEntry in apiClasses.entries) {
final version = versionEntry.key;
final versionUpper =
version == 'v1' ? '' : version.toUpperCase(); // v1不加后缀
final versionLabel =
version == 'v1' ? 'V1默认版本' : '${version.toUpperCase()} 版本';
buffer.writeln(' /// $versionLabel API');
for (final className in versionEntry.value) {
final fieldName = _toLowerCamelCase(className);
final suffix = version == 'v1' ? '' : versionUpper;
buffer.writeln(
' $className$suffix get $fieldName$suffix => _$fieldName$suffix;',
);
}
buffer.writeln();
}
buffer.writeln('}');
return buffer.toString();
}
/// 从生成的 API 源码中提取 API 类名(如 QRCodeApi、MobileManagerApi 等)
/// 仅匹配形如 "abstract class XxxApi {" 的声明
List<String> _extractApiClassNamesFromCode(String code) {
try {
final regex = RegExp(r'abstract\s+class\s+(\w+Api)\b');
final matches = regex.allMatches(code);
if (matches.isEmpty) return const [];
return matches.map((m) => m.group(1)!).toList();
} catch (_) {
return const [];
}
}
/// 转换为小驼峰命名
/// MobileManagerApi → mobileManager
String _toLowerCamelCase(String className) {
// 移除 'Api' 后缀
final name = className.replaceAll('Api', '');
// 首字母小写
return name[0].toLowerCase() + name.substring(1);
}
/// 生成版本目录的 index.dart
Future<void> _generateVersionIndexFile(
String versionDir,
List<String> fileNames,
) async {
final buffer = StringBuffer();
// 文件头
buffer.writeln('// API 接口导出文件');
buffer.writeln('// 由 xy_swagger_generator by max 生成');
buffer.writeln('// Copyright (C) 2025 YuanXuan. All rights reserved.');
buffer.writeln();
// 导出所有 API 文件
final sortedFiles = fileNames.toList()..sort();
for (final fileName in sortedFiles) {
buffer.writeln("export '$fileName';");
}
final indexPath = '$versionDir/index.dart';
await FileUtils.writeFile(indexPath, buffer.toString());
}
/// 根据 includedTags 和 excludedTags 过滤文档
SwaggerDocument _filterDocumentByTags(
SwaggerDocument document,
List<String>? includedTags,
List<String>? excludedTags,
) {
final hasIncludes = includedTags != null && includedTags.isNotEmpty;
final hasExcludes = excludedTags != null && excludedTags.isNotEmpty;
// 如果没有指定任何过滤条件,返回原文档
if (!hasIncludes && !hasExcludes) {
return document;
}
progress('🔍 正在根据 tags 过滤文档...');
if (hasIncludes) progress(' 只保留 tags: ${includedTags.join(", ")}');
if (hasExcludes) progress(' 排除 tags: ${excludedTags.join(", ")}');
// 过滤 paths
final filteredPaths = <String, ApiPath>{};
final usedModelNames = <String>{};
for (final entry in document.paths.entries) {
final path = entry.value;
final pathTags = path.tags;
// 1. Inclusion check: 如果提供了 included_tags则路径必须至少有一个 tag 在列表中
final included =
!hasIncludes || pathTags.any((tag) => includedTags.contains(tag));
if (!included) {
continue; // 不满足包含条件,跳过
}
// 2. Exclusion check: 如果提供了 excluded_tags则路径的所有 tags 不能都在排除列表中
// 换句话说,如果路径的所有 tags 都在排除列表中,则排除该路径。
final excluded = hasExcludes &&
pathTags.isNotEmpty &&
pathTags.every((tag) => excludedTags.contains(tag));
if (excluded) {
continue; // 满足排除条件,跳过
}
// 如果路径通过了所有检查,则保留它
filteredPaths[entry.key] = path;
_collectUsedModels(path, usedModelNames);
}
progress(' 保留了 ${filteredPaths.length}/${document.paths.length} 个接口');
// 过滤 models只保留被使用的 models (此逻辑与之前相同)
final filteredModels = <String, ApiModel>{};
final modelsToCheck = Set<String>.from(usedModelNames);
final checkedModels = <String>{};
while (modelsToCheck.isNotEmpty) {
final modelName = modelsToCheck.first;
modelsToCheck.remove(modelName);
if (checkedModels.contains(modelName)) {
continue;
}
checkedModels.add(modelName);
final model = document.models[modelName];
if (model != null) {
filteredModels[modelName] = model;
_collectModelDependencies(model, modelsToCheck, checkedModels);
}
}
progress(' 保留了 ${filteredModels.length}/${document.models.length} 个模型');
// 过滤 controllers
final filteredControllers = <String, ApiController>{};
for (final entry in document.controllers.entries) {
final tagName = entry.key;
var shouldKeep = true;
if (hasIncludes && !includedTags.contains(tagName)) {
shouldKeep = false;
}
if (hasExcludes && excludedTags.contains(tagName)) {
shouldKeep = false;
}
if (shouldKeep) {
filteredControllers[tagName] = entry.value;
}
}
progress(
' 保留了 ${filteredControllers.length}/${document.controllers.length} 个控制器',
);
// 返回过滤后的文档
return SwaggerDocument(
title: document.title,
version: document.version,
description: document.description,
servers: document.servers,
components: document.components,
paths: filteredPaths,
models: filteredModels,
controllers: filteredControllers,
security: document.security,
);
}
/// 收集 ApiPath 使用的所有 model 名称
void _collectUsedModels(ApiPath path, Set<String> usedModelNames) {
// 递归地从 schema 中提取模型名称
void extractModelsFromSchema(Map<String, dynamic> schema) {
if (schema.containsKey(r'$ref')) {
final modelName = _extractModelNameFromRef(schema[r'$ref'] as String);
if (modelName != null) {
usedModelNames.add(modelName);
}
return;
}
if (schema.containsKey('type')) {
final type = schema['type'];
if (type == 'array' && schema.containsKey('items')) {
extractModelsFromSchema(schema['items'] as Map<String, dynamic>);
} else if (type == 'object' && schema.containsKey('properties')) {
final properties = schema['properties'] as Map<String, dynamic>;
for (final propSchema in properties.values) {
extractModelsFromSchema(propSchema as Map<String, dynamic>);
}
}
}
for (final key in ['allOf', 'anyOf', 'oneOf']) {
if (schema.containsKey(key)) {
final subSchemas = schema[key] as List<dynamic>;
for (final subSchema in subSchemas) {
extractModelsFromSchema(subSchema as Map<String, dynamic>);
}
}
}
}
// 从 requestBody 收集
if (path.requestBody != null) {
for (final mediaType in path.requestBody!.content.values) {
if (mediaType.schema != null) {
extractModelsFromSchema(mediaType.schema!);
}
}
}
// 从 responses 收集
for (final response in path.responses.values) {
for (final mediaType in response.content.values) {
if (mediaType.schema != null) {
extractModelsFromSchema(mediaType.schema!);
}
}
}
}
/// 收集 ApiModel 依赖的其他 models
void _collectModelDependencies(
ApiModel model,
Set<String> modelsToCheck,
Set<String> checkedModels,
) {
// 从 properties 收集
for (final property in model.properties.values) {
// 使用 reference 字段
if (property.reference != null &&
!checkedModels.contains(property.reference)) {
modelsToCheck.add(property.reference!);
}
// 处理数组类型 - items 是 ApiModel有 name 字段
if (property.type == PropertyType.array && property.items != null) {
final itemsName = property.items!.name;
if (itemsName.isNotEmpty && !checkedModels.contains(itemsName)) {
modelsToCheck.add(itemsName);
}
}
// 处理嵌套属性中的引用
for (final nestedProp in property.nestedProperties.values) {
if (nestedProp.reference != null &&
!checkedModels.contains(nestedProp.reference)) {
modelsToCheck.add(nestedProp.reference!);
}
}
}
// 从 allOf, oneOf, anyOf 收集 - 使用 reference 字段
for (final schema in [...model.allOf, ...model.oneOf, ...model.anyOf]) {
if (schema.reference != null) {
final modelName = _extractModelNameFromRef(schema.reference!);
if (modelName != null && !checkedModels.contains(modelName)) {
modelsToCheck.add(modelName);
}
}
}
}
/// 从 $ref 中提取 model 名称
/// 例如:#/components/schemas/User -> User
String? _extractModelNameFromRef(String ref) {
if (ref.startsWith('#/components/schemas/')) {
return ref.substring('#/components/schemas/'.length);
}
if (ref.startsWith('#/definitions/')) {
return ref.substring('#/definitions/'.length);
}
return null;
}
}
/// 生成选项
class GenerateOptions {
const GenerateOptions({
required this.generateModels,
required this.generateApi,
required this.splitByTags,
this.includedTags,
this.excludedTags,
});
final bool generateModels;
final bool generateApi;
final bool splitByTags;
final List<String>? includedTags;
final List<String>? excludedTags;
}