swagger_generator_flutter/lib/commands/services/generation_output_service.dart

658 lines
19 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/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());
}
}