1091 lines
37 KiB
Dart
1091 lines
37 KiB
Dart
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;
|
||
}
|