658 lines
19 KiB
Dart
658 lines
19 KiB
Dart
import 'dart:io';
|
||
|
||
import 'package:path/path.dart' as path;
|
||
import 'package:swagger_generator_flutter/commands/generate_command.dart'
|
||
show GenerateOptions;
|
||
import 'package:swagger_generator_flutter/commands/services/service_typedefs.dart';
|
||
import 'package:swagger_generator_flutter/core/config.dart';
|
||
import 'package:swagger_generator_flutter/core/config_repository.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/utils/file_utils.dart';
|
||
import 'package:swagger_generator_flutter/utils/logger.dart';
|
||
|
||
class GenerationOutputService {
|
||
const GenerationOutputService();
|
||
static ConfigRepository? _cachedConfig;
|
||
ConfigRepository get _config => _cachedConfig ??= ConfigRepository.loadSync();
|
||
|
||
Future<int> generateOutputs({
|
||
required SwaggerDocument document,
|
||
required GenerateOptions options,
|
||
required String baseDir,
|
||
required String apiDir,
|
||
required String modelsDir,
|
||
required LogCallback progress,
|
||
required LogCallback success,
|
||
}) async {
|
||
await FileUtils.ensureDirectoryExists(baseDir);
|
||
await FileUtils.ensureDirectoryExists(apiDir);
|
||
await FileUtils.ensureDirectoryExists(modelsDir);
|
||
|
||
var generatedFiles = 0;
|
||
|
||
if (options.generateModels) {
|
||
generatedFiles += await _generateModels(
|
||
document,
|
||
modelsDir,
|
||
progress,
|
||
success,
|
||
);
|
||
}
|
||
|
||
if (options.generateApi) {
|
||
generatedFiles += await _generateApis(
|
||
document,
|
||
apiDir,
|
||
modelsDir,
|
||
progress,
|
||
success,
|
||
);
|
||
}
|
||
|
||
if (options.generateModels || options.generateApi) {
|
||
await _regenerateModelsIndex(modelsDir, success);
|
||
}
|
||
|
||
await _generateSummary(document, baseDir);
|
||
return generatedFiles;
|
||
}
|
||
|
||
Future<int> _generateModels(
|
||
SwaggerDocument document,
|
||
String modelsDir,
|
||
LogCallback progress,
|
||
LogCallback success,
|
||
) async {
|
||
progress('正在生成数据模型...');
|
||
final generator = ModelCodeGenerator(document);
|
||
final modelFiles = generator.generateSeparateModelFiles();
|
||
var generatedFiles = 0;
|
||
|
||
for (final entry in modelFiles.entries) {
|
||
final filePath = '$modelsDir/${entry.key}';
|
||
if (_config.shouldSkipFile(filePath)) {
|
||
progress('跳过文件: $filePath');
|
||
continue;
|
||
}
|
||
await FileUtils.writeFile(filePath, entry.value);
|
||
success('模型文件已保存到: $filePath');
|
||
generatedFiles++;
|
||
}
|
||
|
||
return generatedFiles;
|
||
}
|
||
|
||
Future<int> _generateApis(
|
||
SwaggerDocument document,
|
||
String apiDir,
|
||
String modelsDir,
|
||
LogCallback progress,
|
||
LogCallback success,
|
||
) async {
|
||
progress('正在按版本和tags分组生成Retrofit风格API接口...');
|
||
|
||
await FileUtils.ensureDirectoryExists(apiDir);
|
||
final pathsByVersion = _groupPathsByVersion(document);
|
||
|
||
progress(
|
||
'检测到 ${pathsByVersion.keys.length} 个版本: '
|
||
'${pathsByVersion.keys.join(", ")}',
|
||
);
|
||
|
||
final versionedFiles = await _buildVersionedApis(
|
||
document,
|
||
pathsByVersion,
|
||
progress,
|
||
);
|
||
|
||
var generatedFiles = 0;
|
||
generatedFiles += await _writeVersionedApis(
|
||
apiDir,
|
||
versionedFiles,
|
||
progress,
|
||
success,
|
||
);
|
||
|
||
generatedFiles += await _writeMainApiFile(
|
||
apiDir,
|
||
versionedFiles,
|
||
success,
|
||
);
|
||
|
||
generatedFiles += await _writeParameterEntities(
|
||
document,
|
||
modelsDir,
|
||
success,
|
||
progress,
|
||
);
|
||
|
||
return generatedFiles;
|
||
}
|
||
|
||
Map<String, List<ApiPath>> _groupPathsByVersion(SwaggerDocument document) {
|
||
final pathsByVersion = <String, List<ApiPath>>{};
|
||
for (final path in document.paths.values) {
|
||
final version = _extractVersionFromPath(path.path);
|
||
pathsByVersion.putIfAbsent(version, () => []).add(path);
|
||
}
|
||
return pathsByVersion;
|
||
}
|
||
|
||
Future<Map<String, Map<String, String>>> _buildVersionedApis(
|
||
SwaggerDocument document,
|
||
Map<String, List<ApiPath>> pathsByVersion,
|
||
LogCallback progress,
|
||
) async {
|
||
final versionedFiles = <String, Map<String, String>>{};
|
||
final apiClientClassName = _config.apiClientClassName;
|
||
|
||
for (final versionEntry in pathsByVersion.entries) {
|
||
final version = versionEntry.key;
|
||
final versionPaths = versionEntry.value;
|
||
|
||
progress(' 正在生成 $version 版本 API(${versionPaths.length} 个接口)...');
|
||
|
||
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)
|
||
SwaggerDocument.buildPathKey(p.path, p.method): p,
|
||
},
|
||
models: document.models,
|
||
controllers: versionControllers,
|
||
);
|
||
|
||
final generator = RetrofitApiGenerator(
|
||
className: apiClientClassName,
|
||
)
|
||
..document = versionDocument
|
||
..ensureParameterEntitiesGenerated();
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
return versionedFiles;
|
||
}
|
||
|
||
Future<int> _writeVersionedApis(
|
||
String apiDir,
|
||
Map<String, Map<String, String>> versionedFiles,
|
||
LogCallback progress,
|
||
LogCallback success,
|
||
) async {
|
||
var generatedFiles = 0;
|
||
|
||
for (final versionEntry in versionedFiles.entries) {
|
||
final version = versionEntry.key;
|
||
final files = versionEntry.value;
|
||
final versionDir = '$apiDir/$version';
|
||
|
||
if (_config.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 (_config.shouldSkipFile(filePath)) {
|
||
progress('跳过文件: $filePath');
|
||
continue;
|
||
}
|
||
|
||
await FileUtils.writeFile(filePath, code);
|
||
success('API接口文件已保存到: $filePath');
|
||
generatedFiles++;
|
||
}
|
||
|
||
if (!_config.shouldSkipFile(versionDir)) {
|
||
await _generateVersionIndexFile(versionDir, files.keys.toList());
|
||
success('$version/index.dart 已生成');
|
||
}
|
||
}
|
||
|
||
return generatedFiles;
|
||
}
|
||
|
||
Future<int> _writeMainApiFile(
|
||
String apiDir,
|
||
Map<String, Map<String, String>> versionedFiles,
|
||
LogCallback success,
|
||
) async {
|
||
final apiClientFileName = _config.apiClientFileName;
|
||
final mainCode = _generateVersionedApiClient(versionedFiles);
|
||
final mainFilePath = '$apiDir/$apiClientFileName.dart';
|
||
|
||
if (!_config.shouldSkipFile(mainFilePath)) {
|
||
await FileUtils.writeFile(mainFilePath, mainCode);
|
||
success('主API接口文件已保存到: $mainFilePath');
|
||
return 1;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
Future<int> _writeParameterEntities(
|
||
SwaggerDocument document,
|
||
String modelsDir,
|
||
LogCallback success,
|
||
LogCallback progress,
|
||
) async {
|
||
final apiClientClassName = _config.apiClientClassName;
|
||
final lastGenerator = RetrofitApiGenerator(
|
||
className: apiClientClassName,
|
||
)
|
||
..document = document
|
||
..ensureParameterEntitiesGenerated();
|
||
|
||
final parameterEntityFiles = lastGenerator.generateParameterEntityFiles();
|
||
if (parameterEntityFiles.isEmpty) {
|
||
return 0;
|
||
}
|
||
|
||
final parametersDir = '$modelsDir/parameters';
|
||
await FileUtils.ensureDirectoryExists(parametersDir);
|
||
var generatedFiles = 0;
|
||
|
||
for (final entry in parameterEntityFiles.entries) {
|
||
final filePath = '$parametersDir/${entry.key}';
|
||
|
||
if (_config.shouldSkipFile(filePath)) {
|
||
progress('跳过文件: $filePath');
|
||
continue;
|
||
}
|
||
|
||
await FileUtils.writeFile(filePath, entry.value);
|
||
success('参数实体类文件已保存到: $filePath');
|
||
generatedFiles++;
|
||
}
|
||
|
||
await _generateSubDirectoryIndexFile(parametersDir, success);
|
||
return generatedFiles;
|
||
}
|
||
|
||
Future<void> _regenerateModelsIndex(
|
||
String modelsDir,
|
||
LogCallback success,
|
||
) async {
|
||
final allFiles = await _getAllModelFiles(modelsDir);
|
||
final indexContent = _generateUpdatedIndexFile(allFiles);
|
||
final indexPath = '$modelsDir/index.dart';
|
||
await FileUtils.writeFile(indexPath, indexContent);
|
||
success('index.dart 文件已更新');
|
||
}
|
||
|
||
Future<List<String>> _getAllModelFiles(String modelsDir) async {
|
||
try {
|
||
final directory = Directory(modelsDir);
|
||
if (!directory.existsSync()) {
|
||
return [];
|
||
}
|
||
|
||
final files = directory.listSync();
|
||
final exportPaths = <String>[];
|
||
|
||
for (final entity in files) {
|
||
if (entity is Directory) {
|
||
final dirName = path.basename(entity.path);
|
||
final subIndexPath = path.join(entity.path, 'index.dart');
|
||
if (File(subIndexPath).existsSync()) {
|
||
exportPaths.add('$dirName/index.dart');
|
||
}
|
||
} else if (entity is File && entity.path.endsWith('.dart')) {
|
||
final fileName = path.basename(entity.path);
|
||
if (fileName != 'index.dart' && !fileName.endsWith('.g.dart')) {
|
||
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;
|
||
} on Exception catch (e, stackTrace) {
|
||
appLogger.severe('获取模型文件列表失败', e, stackTrace);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
Future<void> _generateSubDirectoryIndexFile(
|
||
String subDir,
|
||
LogCallback success,
|
||
) async {
|
||
final directory = Directory(subDir);
|
||
if (!directory.existsSync()) return;
|
||
|
||
final dirName = path.basename(subDir);
|
||
final files = directory.listSync();
|
||
final dartFiles = <String>[];
|
||
|
||
for (final entity in files) {
|
||
if (entity is File && entity.path.endsWith('.dart')) {
|
||
final fileName = path.basename(entity.path);
|
||
if (fileName != 'index.dart' && !fileName.endsWith('.g.dart')) {
|
||
final filePath = path.join(subDir, fileName);
|
||
if (!_config.shouldSkipFile(filePath)) {
|
||
dartFiles.add(fileName);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
dartFiles.sort();
|
||
|
||
final buffer = StringBuffer()
|
||
..writeln('// 模型导出文件')
|
||
..writeln('// 基于 Swagger API 文档: ')
|
||
..writeln('// 由 xy_swagger_generator by max 生成')
|
||
..writeln('// Copyright (C) 2025 YuanXuan. All rights reserved.')
|
||
..writeln()
|
||
..writeln()
|
||
..writeln('library;')
|
||
..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} 个文件');
|
||
}
|
||
|
||
String _generateUpdatedIndexFile(List<String> fileNames) {
|
||
final buffer = StringBuffer()
|
||
..writeln('// API 模型导出文件')
|
||
..writeln('// 基于 Swagger API 文档: ')
|
||
..writeln('// 由 xy_swagger_generator by max 生成')
|
||
..writeln('// Copyright (C) 2025 YuanXuan. All rights reserved.')
|
||
..writeln()
|
||
..writeln('library;')
|
||
..writeln();
|
||
|
||
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();
|
||
}
|
||
|
||
Future<void> _generateSummary(
|
||
SwaggerDocument document,
|
||
String outputDir,
|
||
) async {
|
||
final summary = StringBuffer()
|
||
..writeln('# 代码生成摘要')
|
||
..writeln()
|
||
..writeln('**API标题**: ${document.title}')
|
||
..writeln('**API版本**: ${document.version}')
|
||
..writeln('**生成时间**: ${DateTime.now().toIso8601String()}')
|
||
..writeln()
|
||
..writeln('## 统计信息')
|
||
..writeln('- 控制器数量: ${document.controllers.length}')
|
||
..writeln('- API路径数量: ${document.paths.length}')
|
||
..writeln('- 数据模型数量: ${document.models.length}')
|
||
..writeln()
|
||
..writeln('## 控制器列表');
|
||
document.controllers.forEach((name, controller) {
|
||
summary.writeln(
|
||
'- **$name**: ${controller.description} '
|
||
'(${controller.paths.length} 个路径)',
|
||
);
|
||
});
|
||
|
||
await FileUtils.writeFile('$outputDir/SUMMARY.md', summary.toString());
|
||
}
|
||
|
||
String _extractVersionFromPath(String path) {
|
||
final pattern = _config.versionExtractionPattern;
|
||
final defaultVersion = _config.defaultVersion;
|
||
|
||
try {
|
||
final versionMatch = RegExp(pattern).firstMatch(path);
|
||
if (versionMatch != null && versionMatch.groupCount > 0) {
|
||
return 'v${versionMatch.group(1)}';
|
||
}
|
||
} on FormatException {
|
||
const defaultPattern = r'/api/v(\d+)/';
|
||
final versionMatch = RegExp(defaultPattern).firstMatch(path);
|
||
if (versionMatch != null) {
|
||
return 'v${versionMatch.group(1)}';
|
||
}
|
||
}
|
||
|
||
return defaultVersion;
|
||
}
|
||
|
||
String _addVersionSuffixToCode(String code, String version) {
|
||
if (version == 'v1') {
|
||
return code;
|
||
}
|
||
|
||
final versionUpper = version.toUpperCase();
|
||
var updatedCode = code;
|
||
|
||
updatedCode = updatedCode.replaceAllMapped(
|
||
RegExp(r'abstract class (\w+Api)\b'),
|
||
(match) => 'abstract class ${match.group(1)}$versionUpper',
|
||
);
|
||
|
||
updatedCode = updatedCode.replaceAllMapped(
|
||
RegExp(r'factory (\w+Api)\('),
|
||
(match) => 'factory ${match.group(1)}$versionUpper(',
|
||
);
|
||
|
||
updatedCode = updatedCode.replaceAllMapped(
|
||
RegExp(r'= _(\w+Api);'),
|
||
(match) => '= _${match.group(1)}$versionUpper;',
|
||
);
|
||
|
||
updatedCode = updatedCode.replaceAllMapped(
|
||
RegExp(r"part '(\w+)\.g\.dart';"),
|
||
(match) => "part '${match.group(1)}.g.dart';",
|
||
);
|
||
|
||
updatedCode = updatedCode.replaceAllMapped(
|
||
RegExp(r"import '../(\w+_api)\.dart';"),
|
||
(match) => "import '../$version/${match.group(1)}.dart';",
|
||
);
|
||
|
||
return updatedCode;
|
||
}
|
||
|
||
String _generateVersionedApiClient(
|
||
Map<String, Map<String, String>> versionedFiles,
|
||
) {
|
||
final buffer = StringBuffer()
|
||
..writeln('// 统一 API 客户端')
|
||
..writeln('// 支持多版本 API 管理')
|
||
..writeln('// 由 xy_swagger_generator by max 生成')
|
||
..writeln('// Copyright (C) 2025 YuanXuan. All rights reserved.')
|
||
..writeln()
|
||
..writeln("import 'package:dio/dio.dart';")
|
||
..writeln();
|
||
|
||
final apiClasses = <String, Set<String>>{};
|
||
|
||
for (final versionEntry in versionedFiles.entries) {
|
||
final version = versionEntry.key;
|
||
final files = versionEntry.value;
|
||
apiClasses[version] = {};
|
||
|
||
for (final entry in files.entries) {
|
||
final code = entry.value;
|
||
final extracted = _extractApiClassNamesFromCode(code);
|
||
if (extracted.isNotEmpty) {
|
||
apiClasses[version]!.addAll(extracted.toSet());
|
||
continue;
|
||
}
|
||
|
||
final fileName = entry.key;
|
||
final className = fileName
|
||
.replaceAll('.dart', '')
|
||
.split('_')
|
||
.map(
|
||
(word) => word.isEmpty
|
||
? ''
|
||
: (word[0].toUpperCase() + word.substring(1)),
|
||
)
|
||
.join();
|
||
apiClasses[version]!.add(className);
|
||
}
|
||
}
|
||
|
||
final versions = apiClasses.keys.toList()..sort();
|
||
for (final version in versions) {
|
||
buffer.writeln("import '$version/index.dart';");
|
||
}
|
||
|
||
buffer
|
||
..writeln()
|
||
..writeln('/// 统一 API 客户端');
|
||
|
||
final apiClientClassName = _config.apiClientClassName;
|
||
buffer
|
||
..writeln('/// 支持多版本 API 访问')
|
||
..writeln('class $apiClientClassName {')
|
||
..writeln(' final Dio _dio;')
|
||
..writeln();
|
||
|
||
for (final versionEntry in apiClasses.entries) {
|
||
final version = versionEntry.key;
|
||
final versionUpper = version == 'v1' ? '' : version.toUpperCase();
|
||
|
||
for (final className in versionEntry.value) {
|
||
final suffix = version == 'v1' ? '' : versionUpper;
|
||
buffer.writeln(
|
||
' late final $className$suffix '
|
||
'_${_toLowerCamelCase(className)}$suffix;',
|
||
);
|
||
}
|
||
}
|
||
|
||
buffer
|
||
..writeln()
|
||
..writeln(' $apiClientClassName(this._dio) {')
|
||
..writeln(' _initApis();')
|
||
..writeln(' }')
|
||
..writeln()
|
||
..writeln(' void _initApis() {');
|
||
|
||
for (final versionEntry in apiClasses.entries) {
|
||
final version = versionEntry.key;
|
||
final versionUpper = version == 'v1' ? '' : version.toUpperCase();
|
||
|
||
for (final className in versionEntry.value) {
|
||
final fieldName = _toLowerCamelCase(className);
|
||
final suffix = version == 'v1' ? '' : versionUpper;
|
||
buffer.writeln(' _$fieldName$suffix = $className$suffix(_dio);');
|
||
}
|
||
}
|
||
|
||
buffer
|
||
..writeln(' }')
|
||
..writeln()
|
||
..writeln(' // ========== 版本化 API 访问 ==========')
|
||
..writeln();
|
||
|
||
for (final versionEntry in apiClasses.entries) {
|
||
final version = versionEntry.key;
|
||
final versionUpper = version == 'v1' ? '' : version.toUpperCase();
|
||
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();
|
||
}
|
||
|
||
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();
|
||
} on FormatException {
|
||
return const [];
|
||
}
|
||
}
|
||
|
||
String _toLowerCamelCase(String className) {
|
||
final name = className.replaceAll('Api', '');
|
||
return name[0].toLowerCase() + name.substring(1);
|
||
}
|
||
|
||
Future<void> _generateVersionIndexFile(
|
||
String versionDir,
|
||
List<String> fileNames,
|
||
) async {
|
||
final buffer = StringBuffer()
|
||
..writeln('// API 接口导出文件')
|
||
..writeln('// 由 xy_swagger_generator by max 生成')
|
||
..writeln('// Copyright (C) 2025 YuanXuan. All rights reserved.')
|
||
..writeln();
|
||
|
||
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());
|
||
}
|
||
}
|