refactor(pipeline): 迁移generators模块到pipeline结构
- 将 base_generator.dart, model_code_generator.dart, retrofit_api_generator.dart 及其 part 文件物理迁移到 lib/pipeline/generate/impl/ 目录。 - 在原 lib/generators/ 目录下保留向后兼容的导出 shim 文件。 - 修复了因文件移动导致的 part of 路径问题和 mixin 引用失效问题。 质量门禁: - dart analyze: 0 error / 0 warning (info only) - dart test: all pass
This commit is contained in:
parent
ceab0b6f19
commit
7627236650
|
|
@ -1,657 +1,4 @@
|
||||||
import 'dart:io';
|
/// Backward-compat shim for GenerationOutputService
|
||||||
|
library;
|
||||||
|
|
||||||
import 'package:path/path.dart' as path;
|
export 'package:swagger_generator_flutter/pipeline/output/impl/generation_output_service.dart';
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,92 +1,4 @@
|
||||||
import 'dart:io';
|
/// Backward-compat shim for TemplateRenderer
|
||||||
|
library;
|
||||||
|
|
||||||
import 'package:mustache_template/mustache_template.dart';
|
export 'package:swagger_generator_flutter/pipeline/render/impl/template_renderer.dart';
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
import 'package:swagger_generator_flutter/core/config_repository.dart';
|
|
||||||
import 'package:swagger_generator_flutter/utils/path_resolver.dart';
|
|
||||||
|
|
||||||
part 'template/template_loader.dart';
|
|
||||||
|
|
||||||
/// 模板渲染器
|
|
||||||
/// 负责加载和渲染 Mustache 模板,支持文件覆盖与内置模板
|
|
||||||
class TemplateRenderer {
|
|
||||||
TemplateRenderer({
|
|
||||||
String? templateRoot,
|
|
||||||
List<String>? extraTemplateRoots,
|
|
||||||
}) : _loader = TemplateLoader(
|
|
||||||
customRoot: templateRoot,
|
|
||||||
extraRoots: extraTemplateRoots,
|
|
||||||
),
|
|
||||||
_baseContext = _buildBaseContext();
|
|
||||||
|
|
||||||
final TemplateLoader _loader;
|
|
||||||
final Map<String, Template> _templateCache = {};
|
|
||||||
final Map<String, dynamic> _baseContext;
|
|
||||||
|
|
||||||
/// 渲染模板
|
|
||||||
///
|
|
||||||
/// [templateName] 模板名称(不含 .mustache 扩展名)
|
|
||||||
/// [data] 模板数据
|
|
||||||
String render(
|
|
||||||
String templateName,
|
|
||||||
Map<String, dynamic> data, {
|
|
||||||
Map<String, String>? partials,
|
|
||||||
}) {
|
|
||||||
final template = _getTemplate(templateName);
|
|
||||||
final context = {..._baseContext, ...data};
|
|
||||||
return template.renderString(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 部分模板解析器
|
|
||||||
Template? _partialResolver(String name) {
|
|
||||||
try {
|
|
||||||
return _getTemplate(name);
|
|
||||||
} on Exception {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取模板(带缓存)
|
|
||||||
Template _getTemplate(String templateName) {
|
|
||||||
if (_templateCache.containsKey(templateName)) {
|
|
||||||
return _templateCache[templateName]!;
|
|
||||||
}
|
|
||||||
|
|
||||||
final source = _getTemplateSource(templateName);
|
|
||||||
final template = Template(
|
|
||||||
source,
|
|
||||||
name: templateName,
|
|
||||||
lenient: true,
|
|
||||||
htmlEscapeValues: false,
|
|
||||||
partialResolver: _partialResolver,
|
|
||||||
);
|
|
||||||
_templateCache[templateName] = template;
|
|
||||||
return template;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取模板源码:优先文件,其次内嵌
|
|
||||||
String _getTemplateSource(String templateName) {
|
|
||||||
final fileTemplate = _loader.load(templateName);
|
|
||||||
if (fileTemplate != null) {
|
|
||||||
return fileTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw Exception('Template not found in file system: $templateName');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 清除模板缓存
|
|
||||||
void clearCache() {
|
|
||||||
_templateCache.clear();
|
|
||||||
_loader.clearCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
static Map<String, dynamic> _buildBaseContext() {
|
|
||||||
// Load once synchronously to avoid repeated disk IO
|
|
||||||
final config = ConfigRepository.loadSync();
|
|
||||||
return {
|
|
||||||
'generatorName': config.generatorName,
|
|
||||||
'author': config.author,
|
|
||||||
'copyright': config.copyright,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,365 +1,4 @@
|
||||||
import 'package:swagger_generator_flutter/core/config.dart';
|
/// Backward-compat shim for BaseGenerator
|
||||||
import 'package:swagger_generator_flutter/core/exceptions.dart';
|
library;
|
||||||
import 'package:swagger_generator_flutter/core/models.dart';
|
|
||||||
import 'package:swagger_generator_flutter/utils/string_utils.dart';
|
|
||||||
|
|
||||||
/// 代码生成器基类
|
export 'package:swagger_generator_flutter/pipeline/generate/impl/base_generator.dart';
|
||||||
/// 定义通用的接口和功能
|
|
||||||
abstract class BaseGenerator {
|
|
||||||
/// 生成器类型
|
|
||||||
String get generatorType;
|
|
||||||
|
|
||||||
/// 生成代码
|
|
||||||
String generate();
|
|
||||||
|
|
||||||
/// 生成文件头注释
|
|
||||||
/// [description] 文件描述
|
|
||||||
/// [fileName] 文件名(可选)
|
|
||||||
String generateFileHeader(String description, {String? fileName}) {
|
|
||||||
final header = StringUtils.generateFileHeader(
|
|
||||||
description,
|
|
||||||
SwaggerConfig.swaggerJsonUrls.isNotEmpty
|
|
||||||
? SwaggerConfig.swaggerJsonUrls.first
|
|
||||||
: '',
|
|
||||||
fileName: fileName,
|
|
||||||
fileType: description,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 添加 lint 忽略注释
|
|
||||||
return '$header\n// ignore_for_file: type=lint, invalid_annotation_target\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 生成类型安全的代码
|
|
||||||
String generateTypeCheckedCode(String code) {
|
|
||||||
// 基础类型检查和验证
|
|
||||||
try {
|
|
||||||
if (code.trim().isEmpty) {
|
|
||||||
throw CodeGenerationException(
|
|
||||||
'生成的代码不能为空',
|
|
||||||
generatorType: generatorType,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保文件以换行符结尾
|
|
||||||
if (!code.endsWith('\n')) {
|
|
||||||
return '$code\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
return code;
|
|
||||||
} catch (e) {
|
|
||||||
throw CodeGenerationException(
|
|
||||||
'代码生成失败',
|
|
||||||
details: e.toString(),
|
|
||||||
generatorType: generatorType,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 验证生成的代码是否符合Dart语法规范
|
|
||||||
bool validateDartCode(String code) {
|
|
||||||
// 基础验证规则
|
|
||||||
final validationRules = [
|
|
||||||
// 检查是否有未闭合的大括号
|
|
||||||
(String code) => _countMatches(code, '{') == _countMatches(code, '}'),
|
|
||||||
// 检查是否有未闭合的小括号
|
|
||||||
(String code) => _countMatches(code, '(') == _countMatches(code, ')'),
|
|
||||||
// 检查是否有未闭合的方括号
|
|
||||||
(String code) => _countMatches(code, '[') == _countMatches(code, ']'),
|
|
||||||
// 检查是否有基本的类声明
|
|
||||||
(String code) =>
|
|
||||||
code.contains('class ') ||
|
|
||||||
code.contains('enum ') ||
|
|
||||||
code.contains('const '),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (final rule in validationRules) {
|
|
||||||
if (!rule(code)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 计算字符出现次数
|
|
||||||
int _countMatches(String text, String pattern) {
|
|
||||||
return pattern.allMatches(text).length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 模型代码生成器基类
|
|
||||||
abstract class ModelGenerator extends BaseGenerator {
|
|
||||||
ModelGenerator(this.document, {this.useSimpleModels = false});
|
|
||||||
final SwaggerDocument document;
|
|
||||||
final bool useSimpleModels;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get generatorType => 'ModelGenerator';
|
|
||||||
|
|
||||||
/// 生成单个模型的代码
|
|
||||||
String generateModelCode(ApiModel model);
|
|
||||||
|
|
||||||
/// 生成枚举代码
|
|
||||||
String generateEnumCode(ApiModel model) {
|
|
||||||
if (!model.isEnum) {
|
|
||||||
throw CodeGenerationException('模型不是枚举类型', generatorType: generatorType);
|
|
||||||
}
|
|
||||||
|
|
||||||
final className = StringUtils.generateClassName(model.name);
|
|
||||||
final enumType = model.enumType?.value ?? 'string';
|
|
||||||
final valueType =
|
|
||||||
enumType == 'integer' || enumType == 'number' ? 'int' : 'String';
|
|
||||||
final buffer = StringBuffer()
|
|
||||||
// 生成文件头
|
|
||||||
..writeln(generateFileHeader('${model.name} 枚举定义'))
|
|
||||||
..writeln();
|
|
||||||
|
|
||||||
// 生成枚举类
|
|
||||||
if (model.description.isNotEmpty) {
|
|
||||||
buffer.writeln(StringUtils.generateComment(model.description));
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.writeln('enum $className {');
|
|
||||||
|
|
||||||
// 生成枚举值
|
|
||||||
for (var i = 0; i < model.enumValues.length; i++) {
|
|
||||||
final value = model.enumValues[i];
|
|
||||||
final enumName = StringUtils.generateEnumValueName(value, i);
|
|
||||||
final enumLine = enumType == 'integer' || enumType == 'number'
|
|
||||||
? ' $enumName($value),'
|
|
||||||
: " $enumName('$value'),";
|
|
||||||
|
|
||||||
buffer.writeln(enumLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移除最后一个逗号
|
|
||||||
final content = buffer.toString().trimRight();
|
|
||||||
buffer
|
|
||||||
..clear()
|
|
||||||
..writeAll(
|
|
||||||
[
|
|
||||||
content.substring(0, content.lastIndexOf(',')),
|
|
||||||
';',
|
|
||||||
'',
|
|
||||||
' const $className(this.value);',
|
|
||||||
' final $valueType value;',
|
|
||||||
'',
|
|
||||||
' static $className fromValue(dynamic value) {',
|
|
||||||
' for (final enumValue in $className.values) {',
|
|
||||||
' if (enumValue.value == value) {',
|
|
||||||
' return enumValue;',
|
|
||||||
' }',
|
|
||||||
' }',
|
|
||||||
r" throw ArgumentError('Unknown enum value: $value');",
|
|
||||||
' }',
|
|
||||||
'',
|
|
||||||
' factory $className.fromJson(dynamic json) {',
|
|
||||||
' return fromValue(json);',
|
|
||||||
' }',
|
|
||||||
'',
|
|
||||||
' dynamic toJson() => value;',
|
|
||||||
'',
|
|
||||||
'}',
|
|
||||||
],
|
|
||||||
'\n',
|
|
||||||
);
|
|
||||||
|
|
||||||
return generateTypeCheckedCode(buffer.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 已移动到 StringUtils.generateEnumValueName
|
|
||||||
|
|
||||||
/// 获取导入的类型列表
|
|
||||||
Set<String> getImportedTypes(ApiModel model) {
|
|
||||||
final importedTypes = <String>{};
|
|
||||||
|
|
||||||
model.properties.forEach((_, property) {
|
|
||||||
if (property.type == PropertyType.reference &&
|
|
||||||
property.reference != null) {
|
|
||||||
importedTypes.add(property.reference!);
|
|
||||||
} else if (property.type == PropertyType.array) {
|
|
||||||
// 处理数组类型的引用
|
|
||||||
if (property.items != null) {
|
|
||||||
final itemType = _getItemType(property.items!);
|
|
||||||
// 如果是引用类型(不是基本类型),则添加到导入列表
|
|
||||||
if (itemType != 'String' &&
|
|
||||||
itemType != 'int' &&
|
|
||||||
itemType != 'double' &&
|
|
||||||
itemType != 'bool' &&
|
|
||||||
itemType != 'dynamic') {
|
|
||||||
importedTypes.add(property.items!.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return importedTypes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 生成属性的Dart类型
|
|
||||||
String getDartPropertyType(ApiProperty property) {
|
|
||||||
switch (property.type) {
|
|
||||||
case PropertyType.string:
|
|
||||||
switch (property.format) {
|
|
||||||
case 'date':
|
|
||||||
case 'date-time':
|
|
||||||
return 'DateTime';
|
|
||||||
default:
|
|
||||||
return 'String';
|
|
||||||
}
|
|
||||||
case PropertyType.integer:
|
|
||||||
return 'int';
|
|
||||||
case PropertyType.number:
|
|
||||||
return 'double';
|
|
||||||
case PropertyType.boolean:
|
|
||||||
return 'bool';
|
|
||||||
case PropertyType.enumType:
|
|
||||||
return 'String';
|
|
||||||
case PropertyType.array:
|
|
||||||
// 根据数组元素类型推导具体类型
|
|
||||||
if (property.items != null) {
|
|
||||||
final itemType = _getItemType(property.items!);
|
|
||||||
return 'List<$itemType>';
|
|
||||||
}
|
|
||||||
return 'List<dynamic>';
|
|
||||||
case PropertyType.object:
|
|
||||||
return 'Map<String, dynamic>';
|
|
||||||
case PropertyType.reference:
|
|
||||||
return property.reference != null
|
|
||||||
? StringUtils.generateClassName(property.reference!)
|
|
||||||
: 'dynamic';
|
|
||||||
case PropertyType.file:
|
|
||||||
return 'dynamic';
|
|
||||||
case PropertyType.bytes:
|
|
||||||
return 'List<int>';
|
|
||||||
case PropertyType.date:
|
|
||||||
return 'DateTime';
|
|
||||||
case PropertyType.dateTime:
|
|
||||||
return 'DateTime';
|
|
||||||
case PropertyType.unknown:
|
|
||||||
return 'dynamic';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取数组元素的类型
|
|
||||||
String _getItemType(ApiModel items) {
|
|
||||||
// 如果是引用类型,直接返回类名
|
|
||||||
if (items.name != 'string' &&
|
|
||||||
items.name != 'integer' &&
|
|
||||||
items.name != 'number' &&
|
|
||||||
items.name != 'boolean') {
|
|
||||||
return StringUtils.generateClassName(items.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果是基本类型,转换为对应的Dart类型
|
|
||||||
switch (items.name) {
|
|
||||||
case 'string':
|
|
||||||
return 'String';
|
|
||||||
case 'integer':
|
|
||||||
return 'int';
|
|
||||||
case 'number':
|
|
||||||
return 'double';
|
|
||||||
case 'boolean':
|
|
||||||
return 'bool';
|
|
||||||
default:
|
|
||||||
return 'dynamic';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 选项配置类
|
|
||||||
class GeneratorOptions {
|
|
||||||
const GeneratorOptions({
|
|
||||||
this.generateEndpoints = true,
|
|
||||||
this.generateModels = true,
|
|
||||||
this.generateDocs = true,
|
|
||||||
this.useSimpleModels = false,
|
|
||||||
this.modelsDirectory = 'models',
|
|
||||||
this.outputDirectory = 'generator',
|
|
||||||
this.endpointsFileName = 'api_paths.dart',
|
|
||||||
this.docsFileName = 'api_documentation.md',
|
|
||||||
});
|
|
||||||
|
|
||||||
/// 从命令行参数创建选项
|
|
||||||
factory GeneratorOptions.fromArgs(List<String> args) {
|
|
||||||
var generateEndpoints = false;
|
|
||||||
var generateModels = false;
|
|
||||||
var generateDocs = false;
|
|
||||||
var useSimpleModels = false;
|
|
||||||
var modelsDirectory = 'models';
|
|
||||||
var outputDirectory = 'generator';
|
|
||||||
var endpointsFileName = 'api_paths.dart';
|
|
||||||
var docsFileName = 'api_documentation.md';
|
|
||||||
|
|
||||||
var hasSpecificOption = false;
|
|
||||||
|
|
||||||
for (var i = 0; i < args.length; i++) {
|
|
||||||
final arg = args[i];
|
|
||||||
|
|
||||||
switch (arg) {
|
|
||||||
case '--endpoints':
|
|
||||||
generateEndpoints = true;
|
|
||||||
hasSpecificOption = true;
|
|
||||||
case '--models':
|
|
||||||
generateModels = true;
|
|
||||||
hasSpecificOption = true;
|
|
||||||
case '--docs':
|
|
||||||
generateDocs = true;
|
|
||||||
hasSpecificOption = true;
|
|
||||||
case '--all':
|
|
||||||
generateEndpoints = true;
|
|
||||||
generateModels = true;
|
|
||||||
generateDocs = true;
|
|
||||||
hasSpecificOption = true;
|
|
||||||
case '--simple':
|
|
||||||
useSimpleModels = true;
|
|
||||||
case '--models-dir':
|
|
||||||
if (i + 1 < args.length) {
|
|
||||||
modelsDirectory = args[i + 1];
|
|
||||||
i++; // 跳过下一个参数
|
|
||||||
}
|
|
||||||
case '--output-dir':
|
|
||||||
if (i + 1 < args.length) {
|
|
||||||
outputDirectory = args[i + 1];
|
|
||||||
i++; // 跳过下一个参数
|
|
||||||
}
|
|
||||||
case '--endpoints-file':
|
|
||||||
if (i + 1 < args.length) {
|
|
||||||
endpointsFileName = args[i + 1];
|
|
||||||
i++; // 跳过下一个参数
|
|
||||||
}
|
|
||||||
case '--docs-file':
|
|
||||||
if (i + 1 < args.length) {
|
|
||||||
docsFileName = args[i + 1];
|
|
||||||
i++; // 跳过下一个参数
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有指定特定选项,默认生成所有文件
|
|
||||||
if (!hasSpecificOption) {
|
|
||||||
generateEndpoints = true;
|
|
||||||
generateModels = true;
|
|
||||||
generateDocs = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return GeneratorOptions(
|
|
||||||
generateEndpoints: generateEndpoints,
|
|
||||||
generateModels: generateModels,
|
|
||||||
generateDocs: generateDocs,
|
|
||||||
useSimpleModels: useSimpleModels,
|
|
||||||
modelsDirectory: modelsDirectory,
|
|
||||||
outputDirectory: outputDirectory,
|
|
||||||
endpointsFileName: endpointsFileName,
|
|
||||||
docsFileName: docsFileName,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final bool generateEndpoints;
|
|
||||||
final bool generateModels;
|
|
||||||
final bool generateDocs;
|
|
||||||
final bool useSimpleModels;
|
|
||||||
final String modelsDirectory;
|
|
||||||
final String outputDirectory;
|
|
||||||
final String endpointsFileName;
|
|
||||||
final String docsFileName;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,59 +1,4 @@
|
||||||
import 'package:swagger_generator_flutter/core/config.dart';
|
/// Backward-compat shim for ModelCodeGenerator
|
||||||
import 'package:swagger_generator_flutter/core/models.dart';
|
library;
|
||||||
import 'package:swagger_generator_flutter/generators/base_generator.dart';
|
|
||||||
import 'package:swagger_generator_flutter/utils/string_utils.dart';
|
|
||||||
|
|
||||||
part 'model/model_pagination_helpers.dart';
|
export 'package:swagger_generator_flutter/pipeline/generate/impl/model_code_generator.dart';
|
||||||
part 'model/model_file_writers.dart';
|
|
||||||
part 'model/model_content_builders.dart';
|
|
||||||
|
|
||||||
/// 模型代码生成器
|
|
||||||
/// 负责生成Dart模型类代码
|
|
||||||
class ModelCodeGenerator extends ModelGenerator {
|
|
||||||
ModelCodeGenerator(super.document);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get generatorType => 'ModelCodeGenerator';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String generate() {
|
|
||||||
throw UnimplementedError(
|
|
||||||
'Single file model generation is no longer supported.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getDartPropertyType(ApiProperty property) {
|
|
||||||
return getDartPropertyTypeWithPagination(this, property);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 提供对父类实现的访问,便于分页检测逻辑复用
|
|
||||||
String superGetDartPropertyType(ApiProperty property) {
|
|
||||||
return super.getDartPropertyType(property);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
@Deprecated(
|
|
||||||
'Use generateSingleModelFile or generateSeparateModelFiles instead',
|
|
||||||
)
|
|
||||||
String generateModelCode(ApiModel model) {
|
|
||||||
throw UnimplementedError(
|
|
||||||
'generateModelCode is no longer supported. Use generateSingleModelFile.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 生成所有模型文件,按子目录分组
|
|
||||||
Map<String, String> generateSeparateModelFiles() {
|
|
||||||
return buildSeparateModelFiles(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 生成单个模型文件
|
|
||||||
String generateSingleModelFile(ApiModel model, {String? fileName}) {
|
|
||||||
return buildSingleModelFile(this, model, fileName: fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 生成导出索引文件
|
|
||||||
String generateIndexFile(List<String> modelFileNames) {
|
|
||||||
return _buildIndexFile(this, modelFileNames);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,272 +0,0 @@
|
||||||
part of 'package:swagger_generator_flutter/generators/retrofit_api_generator.dart';
|
|
||||||
|
|
||||||
mixin RetrofitApiSchema {
|
|
||||||
RetrofitApiGenerator get _g => this as RetrofitApiGenerator;
|
|
||||||
|
|
||||||
/// 从路径的响应中提取返回类型
|
|
||||||
String? _extractResponseTypeFromPath(ApiPath path) {
|
|
||||||
final successResponses = ['200', '201', '202'];
|
|
||||||
|
|
||||||
for (final statusCode in successResponses) {
|
|
||||||
final response = path.responses[statusCode];
|
|
||||||
if (response != null) {
|
|
||||||
final type = _g._extractResponseType(response);
|
|
||||||
if (type != null) {
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final response in path.responses.values) {
|
|
||||||
final type = _g._extractResponseType(response);
|
|
||||||
if (type != null) {
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 从响应中提取返回类型
|
|
||||||
String? _extractResponseType(ApiResponse response) {
|
|
||||||
final applicationJsonMediaType = response.content['application/json'];
|
|
||||||
if (applicationJsonMediaType != null) {
|
|
||||||
final schema = applicationJsonMediaType.schema;
|
|
||||||
final type = _g._extractTypeFromSchema(schema);
|
|
||||||
if (type != null) {
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final type = _g._extractTypeFromSchema(response.schema);
|
|
||||||
if (type != null) {
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 从 schema 中提取类型
|
|
||||||
String? _extractTypeFromSchema(Map<String, dynamic>? schema) {
|
|
||||||
if (schema == null) return null;
|
|
||||||
|
|
||||||
final advancedType = _g._handleAdvancedSchemaFeatures(schema);
|
|
||||||
if (advancedType != null) {
|
|
||||||
return advancedType;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schema['allOf'] != null ||
|
|
||||||
schema['oneOf'] != null ||
|
|
||||||
schema['anyOf'] != null) {
|
|
||||||
return _g._extractTypeFromCompositionSchema(schema);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schema[r'$ref'] != null) {
|
|
||||||
final ref = schema[r'$ref'] as String;
|
|
||||||
final parts = ref.split('/');
|
|
||||||
if (parts.isNotEmpty) {
|
|
||||||
final refName = parts.last;
|
|
||||||
if (_g.document.models.containsKey(refName)) {
|
|
||||||
final model = _g.document.models[refName]!;
|
|
||||||
if (_g._isPaginationResponseModel(model)) {
|
|
||||||
final itemsProp = model.properties['items'];
|
|
||||||
if (itemsProp != null && itemsProp.type == PropertyType.array) {
|
|
||||||
var itemType = 'dynamic';
|
|
||||||
if (itemsProp.reference != null) {
|
|
||||||
itemType = StringUtils.generateClassName(itemsProp.reference!);
|
|
||||||
} else if (itemsProp.items != null) {
|
|
||||||
itemType = StringUtils.generateClassName(itemsProp.items!.name);
|
|
||||||
} else if (itemsProp.name.isNotEmpty) {
|
|
||||||
itemType = StringUtils.generateClassName(itemsProp.name);
|
|
||||||
} else if (itemsProp.type != PropertyType.array &&
|
|
||||||
itemsProp.type != PropertyType.reference) {
|
|
||||||
itemType = itemsProp.type.value;
|
|
||||||
}
|
|
||||||
return 'List<$itemType>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return StringUtils.generateClassName(refName);
|
|
||||||
}
|
|
||||||
return StringUtils.generateClassName(refName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schema['type'] == 'array' && schema['items'] != null) {
|
|
||||||
final items = schema['items'] as Map<String, dynamic>;
|
|
||||||
final itemType = _g._extractTypeFromSchema(items);
|
|
||||||
if (itemType != null) {
|
|
||||||
return 'List<$itemType>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schema['type'] == 'object') {
|
|
||||||
if (schema['properties'] != null) {
|
|
||||||
final properties = schema['properties'] as Map<String, dynamic>;
|
|
||||||
|
|
||||||
if (properties.containsKey('total') &&
|
|
||||||
properties.containsKey('items')) {
|
|
||||||
final totalProp = properties['total'] as Map<String, dynamic>?;
|
|
||||||
final itemsProp = properties['items'] as Map<String, dynamic>?;
|
|
||||||
|
|
||||||
final isTotalNumeric = totalProp != null &&
|
|
||||||
(totalProp['type'] == 'integer' || totalProp['type'] == 'number');
|
|
||||||
|
|
||||||
final isItemsArray = itemsProp != null &&
|
|
||||||
itemsProp['type'] == 'array' &&
|
|
||||||
itemsProp['items'] != null;
|
|
||||||
|
|
||||||
if (isTotalNumeric && isItemsArray) {
|
|
||||||
final itemsSchema = itemsProp['items'] as Map<String, dynamic>;
|
|
||||||
final itemType = _extractTypeFromSchema(itemsSchema);
|
|
||||||
if (itemType != null) {
|
|
||||||
return 'List<$itemType>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'Map<String, dynamic>';
|
|
||||||
}
|
|
||||||
if (schema['additionalProperties'] != null) {
|
|
||||||
return 'Map<String, dynamic>';
|
|
||||||
}
|
|
||||||
if (schema['allOf'] != null ||
|
|
||||||
schema['anyOf'] != null ||
|
|
||||||
schema['oneOf'] != null) {
|
|
||||||
return 'Map<String, dynamic>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schema['type'] != null) {
|
|
||||||
final type = schema['type'] as String;
|
|
||||||
switch (type) {
|
|
||||||
case 'string':
|
|
||||||
final format = schema['format'] as String?;
|
|
||||||
if (format == 'date-time' || format == 'date') {
|
|
||||||
return 'String';
|
|
||||||
}
|
|
||||||
if (format == 'uuid') {
|
|
||||||
return 'String';
|
|
||||||
}
|
|
||||||
return 'String';
|
|
||||||
case 'integer':
|
|
||||||
return 'int';
|
|
||||||
case 'number':
|
|
||||||
return 'double';
|
|
||||||
case 'boolean':
|
|
||||||
return 'bool';
|
|
||||||
case 'array':
|
|
||||||
final items = schema['items'] as Map<String, dynamic>?;
|
|
||||||
if (items != null) {
|
|
||||||
final itemType = _extractTypeFromSchema(items);
|
|
||||||
return 'List<${itemType ?? 'dynamic'}>';
|
|
||||||
}
|
|
||||||
return 'List<dynamic>';
|
|
||||||
case 'null':
|
|
||||||
return 'dynamic';
|
|
||||||
default:
|
|
||||||
return 'dynamic';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schema['enum'] != null) {
|
|
||||||
return 'String';
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 处理高级 Schema 特性
|
|
||||||
String? _handleAdvancedSchemaFeatures(Map<String, dynamic> schema) {
|
|
||||||
if (schema['const'] != null) {
|
|
||||||
final constValue = schema['const'];
|
|
||||||
if (constValue is String) {
|
|
||||||
return 'String';
|
|
||||||
} else if (constValue is num) {
|
|
||||||
return constValue is int ? 'int' : 'double';
|
|
||||||
} else if (constValue is bool) {
|
|
||||||
return 'bool';
|
|
||||||
}
|
|
||||||
return 'dynamic';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schema['additionalProperties'] != null) {
|
|
||||||
final additionalProps = schema['additionalProperties'];
|
|
||||||
if (additionalProps is bool) {
|
|
||||||
return additionalProps ? 'Map<String, dynamic>' : 'Map<String, never>';
|
|
||||||
} else if (additionalProps is Map<String, dynamic>) {
|
|
||||||
final valueType = _extractTypeFromSchema(additionalProps);
|
|
||||||
return 'Map<String, ${valueType ?? 'dynamic'}>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schema['patternProperties'] != null) {
|
|
||||||
final patternProps = schema['patternProperties'] as Map<String, dynamic>?;
|
|
||||||
if (patternProps != null && patternProps.isNotEmpty) {
|
|
||||||
return 'Map<String, dynamic>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schema['if'] != null ||
|
|
||||||
schema['then'] != null ||
|
|
||||||
schema['else'] != null) {
|
|
||||||
if (schema['then'] != null) {
|
|
||||||
final thenType =
|
|
||||||
_extractTypeFromSchema(schema['then'] as Map<String, dynamic>?);
|
|
||||||
if (thenType != null) return thenType;
|
|
||||||
}
|
|
||||||
if (schema['else'] != null) {
|
|
||||||
final elseType =
|
|
||||||
_extractTypeFromSchema(schema['else'] as Map<String, dynamic>?);
|
|
||||||
if (elseType != null) return elseType;
|
|
||||||
}
|
|
||||||
return 'dynamic';
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 检查 schema 是否包含分页结构(total 和 items 字段)
|
|
||||||
bool _hasPaginationSchema(Map<String, dynamic> schema) {
|
|
||||||
if (schema['type'] != 'object') return false;
|
|
||||||
|
|
||||||
final properties = schema['properties'] as Map<String, dynamic>?;
|
|
||||||
if (properties == null) return false;
|
|
||||||
|
|
||||||
if (!properties.containsKey('total') || !properties.containsKey('items')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final totalProp = properties['total'] as Map<String, dynamic>?;
|
|
||||||
final itemsProp = properties['items'] as Map<String, dynamic>?;
|
|
||||||
|
|
||||||
final isTotalNumeric = totalProp != null &&
|
|
||||||
(totalProp['type'] == 'integer' || totalProp['type'] == 'number');
|
|
||||||
|
|
||||||
final isItemsArray = itemsProp != null &&
|
|
||||||
itemsProp['type'] == 'array' &&
|
|
||||||
itemsProp['items'] != null;
|
|
||||||
|
|
||||||
return isTotalNumeric && isItemsArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 检查是否是分页响应模型(包含 total 和 items 字段)
|
|
||||||
bool _isPaginationResponseModel(ApiModel model) {
|
|
||||||
if (!model.properties.containsKey('total') ||
|
|
||||||
!model.properties.containsKey('items')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final totalProp = model.properties['total']!;
|
|
||||||
final itemsProp = model.properties['items']!;
|
|
||||||
|
|
||||||
final isTotalNumeric = totalProp.type == PropertyType.integer ||
|
|
||||||
totalProp.type == PropertyType.number;
|
|
||||||
final isItemsArray = itemsProp.type == PropertyType.array;
|
|
||||||
|
|
||||||
return isTotalNumeric && isItemsArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _isArraySchema(Map<String, dynamic> schema) {
|
|
||||||
return schema['type'] == 'array';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,325 +0,0 @@
|
||||||
part of 'package:swagger_generator_flutter/generators/retrofit_api_generator.dart';
|
|
||||||
|
|
||||||
mixin RetrofitApiTemplateData {
|
|
||||||
RetrofitApiGenerator get _g => this as RetrofitApiGenerator;
|
|
||||||
List<String> _getMainImports() {
|
|
||||||
final tagGroups = _g._groupPathsByTags();
|
|
||||||
final tagImports = tagGroups.keys
|
|
||||||
.map((tagName) => StringUtils.generateFileName('${tagName}Api'))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
final config = ConfigRepository.loadSync();
|
|
||||||
return [
|
|
||||||
...config.packageImports,
|
|
||||||
'package:dio/dio.dart',
|
|
||||||
...tagImports,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Map<String, dynamic>> _buildTagApisData() {
|
|
||||||
final tagGroups = _g._groupPathsByTags();
|
|
||||||
return tagGroups.keys
|
|
||||||
.map(
|
|
||||||
(tagName) => {
|
|
||||||
'tagName': tagName,
|
|
||||||
'apiClassName': '${StringUtils.toPascalCase(tagName)}Api',
|
|
||||||
'propertyName': StringUtils.toCamelCase(tagName),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> _buildApiClassData(List<ApiPath> paths) {
|
|
||||||
final baseUrl =
|
|
||||||
_g.document.servers.isNotEmpty ? _g.document.servers.first.url : '';
|
|
||||||
final fileName =
|
|
||||||
StringUtils.generateFileName(_g.className).replaceAll('.dart', '');
|
|
||||||
|
|
||||||
return {
|
|
||||||
'description': 'Retrofit 风格 API 接口定义',
|
|
||||||
'apiUrl': baseUrl,
|
|
||||||
'imports': _getImports(),
|
|
||||||
'parts': ['$fileName.g.dart'],
|
|
||||||
'docLines': [
|
|
||||||
'${_g.className} API 接口',
|
|
||||||
'使用 Retrofit 和 Dio 进行网络请求',
|
|
||||||
'支持多种媒体类型、文件上传、认证等功能',
|
|
||||||
],
|
|
||||||
'hasRestApi': _g.useRetrofit,
|
|
||||||
'baseUrl': baseUrl,
|
|
||||||
'className': _g.className,
|
|
||||||
'hasRetrofit': _g.useRetrofit,
|
|
||||||
'methods': _buildMethodsData(paths),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> _getImports() {
|
|
||||||
final config = ConfigRepository.loadSync();
|
|
||||||
return [
|
|
||||||
...config.packageImports,
|
|
||||||
'../../api_models/index.dart',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Map<String, dynamic>> _buildMethodsData(List<ApiPath> paths) {
|
|
||||||
return paths.map(_buildMethodData).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> _buildMethodData(ApiPath path) {
|
|
||||||
return {
|
|
||||||
'docLines': _buildDocLines(path),
|
|
||||||
'annotations': _buildAnnotations(path),
|
|
||||||
'returnType': _g._generateReturnType(path),
|
|
||||||
'methodName': _g._generateSimpleMethodName(path),
|
|
||||||
'params': _buildParamsData(path),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> _buildDocLines(ApiPath path) {
|
|
||||||
final lines = <String>[];
|
|
||||||
if (path.summary.isNotEmpty) {
|
|
||||||
lines.addAll(_wrapDocLine(StringUtils.cleanDescription(path.summary)));
|
|
||||||
}
|
|
||||||
if (path.description.isNotEmpty && path.description != path.summary) {
|
|
||||||
lines.addAll(
|
|
||||||
_wrapDocLine(StringUtils.cleanDescription(path.description)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final parameters = _g._generateParameters(path);
|
|
||||||
final paramsWithDescription = parameters
|
|
||||||
.where((p) => p.description.isNotEmpty || p.defaultValue != null)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (paramsWithDescription.isNotEmpty) {
|
|
||||||
lines
|
|
||||||
..add('')
|
|
||||||
..add('参数:');
|
|
||||||
for (final param in paramsWithDescription) {
|
|
||||||
final commentParts = <String>[];
|
|
||||||
if (param.description.isNotEmpty) {
|
|
||||||
commentParts.add(StringUtils.cleanDescription(param.description));
|
|
||||||
}
|
|
||||||
if (param.defaultValue != null) {
|
|
||||||
commentParts.add('默认值: ${param.defaultValue}');
|
|
||||||
}
|
|
||||||
final paramDoc = '- ${param.name}: ${commentParts.join(' - ')}';
|
|
||||||
// 使用改进的换行方法,处理参数文档
|
|
||||||
lines.addAll(_wrapParamDocLine(paramDoc));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 将参数文档行拆分为多行,确保每行不超过80字符
|
|
||||||
/// 专门处理 "- paramName: description" 格式的参数文档
|
|
||||||
List<String> _wrapParamDocLine(String paramDoc) {
|
|
||||||
const maxLength = 76; // 80 - '/// '.length
|
|
||||||
|
|
||||||
if (paramDoc.length <= maxLength) {
|
|
||||||
return [paramDoc];
|
|
||||||
}
|
|
||||||
|
|
||||||
final lines = <String>[];
|
|
||||||
|
|
||||||
// 提取参数名和描述部分
|
|
||||||
final match = RegExp(r'^- ([^:]+): (.+)$').firstMatch(paramDoc);
|
|
||||||
if (match == null) {
|
|
||||||
// 如果格式不匹配,使用通用换行方法
|
|
||||||
return _wrapDocLine(paramDoc);
|
|
||||||
}
|
|
||||||
|
|
||||||
final paramName = match.group(1)!;
|
|
||||||
final description = match.group(2)!;
|
|
||||||
final firstLinePrefix = '- $paramName: ';
|
|
||||||
const continuationPrefix = ' '; // 续行使用两个空格缩进
|
|
||||||
|
|
||||||
// 计算第一行可用长度
|
|
||||||
final firstLineMaxLength = maxLength - firstLinePrefix.length;
|
|
||||||
|
|
||||||
if (description.length <= firstLineMaxLength) {
|
|
||||||
// 描述足够短,可以放在一行
|
|
||||||
return [paramDoc];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 需要分多行
|
|
||||||
var remaining = description;
|
|
||||||
var isFirstLine = true;
|
|
||||||
|
|
||||||
while (remaining.isNotEmpty) {
|
|
||||||
final currentPrefix = isFirstLine ? firstLinePrefix : continuationPrefix;
|
|
||||||
final currentMaxLength = maxLength - currentPrefix.length;
|
|
||||||
|
|
||||||
if (remaining.length <= currentMaxLength) {
|
|
||||||
// 剩余内容可以放在当前行
|
|
||||||
lines.add(currentPrefix + remaining);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 寻找合适的断点
|
|
||||||
var breakPoint = currentMaxLength;
|
|
||||||
final breakChars = [' ', ',', ',', '、', ';', ';', '|', '/'];
|
|
||||||
var bestIdx = -1;
|
|
||||||
|
|
||||||
for (final ch in breakChars) {
|
|
||||||
final idx = remaining.lastIndexOf(ch, currentMaxLength);
|
|
||||||
if (idx > bestIdx && idx > currentMaxLength * 0.5) {
|
|
||||||
bestIdx = idx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bestIdx >= 0) {
|
|
||||||
breakPoint = bestIdx + 1; // 包含分隔符
|
|
||||||
}
|
|
||||||
|
|
||||||
final lineContent = remaining.substring(0, breakPoint).trim();
|
|
||||||
lines.add(currentPrefix + lineContent);
|
|
||||||
remaining = remaining.substring(breakPoint).trim();
|
|
||||||
isFirstLine = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 将长文档行拆分为多行,确保每行不超过80字符
|
|
||||||
List<String> _wrapDocLine(String text, {String prefix = ''}) {
|
|
||||||
const maxLength = 76; // 80 - '/// '.length,留一点余量
|
|
||||||
final effectiveMaxLength = maxLength - prefix.length;
|
|
||||||
|
|
||||||
if (text.length <= effectiveMaxLength) {
|
|
||||||
return [prefix + text];
|
|
||||||
}
|
|
||||||
|
|
||||||
final lines = <String>[];
|
|
||||||
var remaining = text;
|
|
||||||
|
|
||||||
while (remaining.length > effectiveMaxLength) {
|
|
||||||
// 优先在空格或常见标点处断行
|
|
||||||
var breakPoint = effectiveMaxLength;
|
|
||||||
final breakChars = [' ', ',', ',', '、', ';', ';', '|', '/'];
|
|
||||||
var bestIdx = -1;
|
|
||||||
for (final ch in breakChars) {
|
|
||||||
final idx = remaining.lastIndexOf(ch, effectiveMaxLength);
|
|
||||||
if (idx > bestIdx) bestIdx = idx;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bestIdx >= 0 && bestIdx > effectiveMaxLength * 0.5) {
|
|
||||||
// 如果找到合适的断点(不要太靠前),使用该断点
|
|
||||||
breakPoint = bestIdx + 1; // 包含分隔符,避免遗留前导分隔符
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.add(prefix + remaining.substring(0, breakPoint).trim());
|
|
||||||
remaining = remaining.substring(breakPoint).trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remaining.isNotEmpty) {
|
|
||||||
lines.add(prefix + remaining);
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> _buildAnnotations(ApiPath path) {
|
|
||||||
final annotations = <String>[];
|
|
||||||
if (_g.useRetrofit) {
|
|
||||||
final httpMethod = path.method.value.toUpperCase();
|
|
||||||
final cleanPath = StringUtils.cleanPath(path.path);
|
|
||||||
annotations.add("@$httpMethod('$cleanPath')");
|
|
||||||
|
|
||||||
if (path.requestBody?.content.containsKey('multipart/form-data') ??
|
|
||||||
false) {
|
|
||||||
annotations.add('@MultiPart()');
|
|
||||||
}
|
|
||||||
if (path.requestBody?.content
|
|
||||||
.containsKey('application/x-www-form-urlencoded') ??
|
|
||||||
false) {
|
|
||||||
annotations.add('@FormUrlEncoded()');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return annotations;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Map<String, dynamic>> _buildParamsData(ApiPath path) {
|
|
||||||
final parameters = _g._generateParameters(path);
|
|
||||||
return parameters.map((param) {
|
|
||||||
var annotation = '';
|
|
||||||
if (param.annotation.isNotEmpty) {
|
|
||||||
annotation = param.annotation;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'annotation': annotation,
|
|
||||||
'type': param.type,
|
|
||||||
'name': param.name,
|
|
||||||
'required': param.required,
|
|
||||||
'description': param.description,
|
|
||||||
};
|
|
||||||
}).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> _buildSecuritySchemesData(SwaggerDocument document) {
|
|
||||||
final schemes = <Map<String, dynamic>>[];
|
|
||||||
|
|
||||||
document.components.securitySchemes.forEach((name, scheme) {
|
|
||||||
final constantName = StringUtils.generateConstantName(name);
|
|
||||||
|
|
||||||
final schemeData = <String, dynamic>{
|
|
||||||
'name': name,
|
|
||||||
'description': scheme.description,
|
|
||||||
'constantName': constantName,
|
|
||||||
'isApiKey': scheme.type == SecuritySchemeType.apiKey,
|
|
||||||
'isHttp': scheme.type == SecuritySchemeType.http,
|
|
||||||
'isOAuth2': scheme.type == SecuritySchemeType.oauth2,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (scheme.type == SecuritySchemeType.apiKey) {
|
|
||||||
schemeData['paramName'] = scheme.name;
|
|
||||||
schemeData['location'] = scheme.location?.value;
|
|
||||||
} else if (scheme.type == SecuritySchemeType.http) {
|
|
||||||
schemeData['scheme'] = scheme.scheme;
|
|
||||||
schemeData['hasBearerFormat'] = scheme.bearerFormat != null;
|
|
||||||
schemeData['bearerFormat'] = scheme.bearerFormat;
|
|
||||||
}
|
|
||||||
|
|
||||||
schemes.add(schemeData);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {'schemes': schemes};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 生成简化的方法名称
|
|
||||||
String _generateSimpleMethodName(ApiPath path) {
|
|
||||||
final method = path.method.value.toLowerCase();
|
|
||||||
|
|
||||||
// 优先使用 operationId(如果存在且有意义)
|
|
||||||
if (path.operationId.isNotEmpty) {
|
|
||||||
final operationId = path.operationId;
|
|
||||||
if (operationId.toLowerCase().startsWith(method)) {
|
|
||||||
return StringUtils.toCamelCase(operationId);
|
|
||||||
}
|
|
||||||
return StringUtils.toCamelCase(operationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理路径,移除 /api/v1 前缀
|
|
||||||
var cleanedPath = path.path.replaceFirst(RegExp(r'^/api/v\d+'), '');
|
|
||||||
if (cleanedPath.isEmpty) {
|
|
||||||
cleanedPath = path.path;
|
|
||||||
}
|
|
||||||
|
|
||||||
final pathParts = cleanedPath
|
|
||||||
.split('/')
|
|
||||||
.where((part) => part.isNotEmpty && !part.startsWith('{'))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (pathParts.length >= 2) {
|
|
||||||
final action = StringUtils.toPascalCase(pathParts[1]);
|
|
||||||
return StringUtils.toCamelCase(action);
|
|
||||||
} else if (pathParts.length == 1) {
|
|
||||||
final action = StringUtils.toPascalCase(pathParts[0]);
|
|
||||||
return StringUtils.toCamelCase(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
final sanitizedPath = pathParts.map(StringUtils.toPascalCase).join();
|
|
||||||
return StringUtils.toCamelCase(sanitizedPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,122 +1,4 @@
|
||||||
import 'package:swagger_generator_flutter/core/config_repository.dart';
|
/// Backward-compat shim for RetrofitApiGenerator
|
||||||
import 'package:swagger_generator_flutter/core/models.dart';
|
library;
|
||||||
import 'package:swagger_generator_flutter/core/template_renderer.dart';
|
|
||||||
import 'package:swagger_generator_flutter/generators/base_generator.dart';
|
|
||||||
import 'package:swagger_generator_flutter/utils/string_utils.dart';
|
|
||||||
|
|
||||||
part 'retrofit_api/api_grouping.dart';
|
export 'package:swagger_generator_flutter/pipeline/generate/impl/retrofit_api_generator.dart';
|
||||||
part 'retrofit_api/api_method_parameter.dart';
|
|
||||||
part 'retrofit_api/api_parameter_entities.dart';
|
|
||||||
part 'retrofit_api/api_parameters.dart';
|
|
||||||
part 'retrofit_api/api_return_types.dart';
|
|
||||||
part 'retrofit_api/api_schema_composition.dart';
|
|
||||||
part 'retrofit_api/api_schema_extraction.dart';
|
|
||||||
part 'retrofit_api/api_template_data.dart';
|
|
||||||
|
|
||||||
/// Retrofit 风格的 API 生成器
|
|
||||||
/// 负责生成带有注解的 API 接口类
|
|
||||||
class RetrofitApiGenerator extends BaseGenerator
|
|
||||||
with
|
|
||||||
RetrofitApiGrouping,
|
|
||||||
RetrofitApiTemplateData,
|
|
||||||
RetrofitApiSchemaComposition,
|
|
||||||
RetrofitApiSchema,
|
|
||||||
RetrofitApiReturnTypes,
|
|
||||||
RetrofitApiParameters,
|
|
||||||
RetrofitApiParameterEntities {
|
|
||||||
RetrofitApiGenerator({
|
|
||||||
this.className = 'ApiClient',
|
|
||||||
this.useRetrofit = true,
|
|
||||||
this.useDio = true,
|
|
||||||
this.splitByTags = true, // 默认启用拆分模式
|
|
||||||
this.generateModels = true,
|
|
||||||
this.versionedApi = true, // 默认启用版本化
|
|
||||||
});
|
|
||||||
|
|
||||||
final String className;
|
|
||||||
final bool useRetrofit;
|
|
||||||
final bool useDio;
|
|
||||||
final bool splitByTags;
|
|
||||||
final bool generateModels;
|
|
||||||
final bool versionedApi; // 是否启用版本化 API
|
|
||||||
|
|
||||||
late SwaggerDocument document;
|
|
||||||
final templateRenderer = TemplateRenderer();
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get generatorType => 'RetrofitApiGenerator';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String generate() {
|
|
||||||
throw UnimplementedError('Use generateFromDocument instead');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 生成 API 代码
|
|
||||||
String generateFromDocument(SwaggerDocument document) {
|
|
||||||
this.document = document; // 设置文档引用
|
|
||||||
if (splitByTags) {
|
|
||||||
// 按 tags 分组生成多个文件时,返回主文件内容
|
|
||||||
return generateMainApiFile();
|
|
||||||
}
|
|
||||||
return generateSingleApiFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 生成单个 API 文件
|
|
||||||
String generateSingleApiFile() {
|
|
||||||
final paths = document.paths.values.toList();
|
|
||||||
|
|
||||||
// Build extra code
|
|
||||||
final extraCodeBuffer = StringBuffer()
|
|
||||||
..write(
|
|
||||||
templateRenderer.render(
|
|
||||||
'api/security_schemes',
|
|
||||||
_buildSecuritySchemesData(document),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
..write(templateRenderer.render('api/media_type_handlers', {}))
|
|
||||||
..write(templateRenderer.render('api/file_upload_handlers', {}))
|
|
||||||
..write(templateRenderer.render('api/encoding_handlers', {}));
|
|
||||||
|
|
||||||
final data = _buildApiClassData(paths);
|
|
||||||
data['extraCode'] = extraCodeBuffer.toString();
|
|
||||||
|
|
||||||
return templateRenderer.render('api/api_class', data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 生成主 API 文件(当按 tags 分组时)
|
|
||||||
String generateMainApiFile() {
|
|
||||||
final data = {
|
|
||||||
'description': '主 API 接口定义 - 集合所有 Tag 的 API',
|
|
||||||
'apiUrl': document.servers.isNotEmpty ? document.servers.first.url : '',
|
|
||||||
'imports': _getMainImports(),
|
|
||||||
'className': className,
|
|
||||||
'tagApis': _buildTagApisData(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return templateRenderer.render('api/main_api', data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 按 tags 分组生成多个 API 文件
|
|
||||||
Map<String, String> generateApiFilesByTags() {
|
|
||||||
final tagGroups = _groupPathsByTags();
|
|
||||||
final apiFiles = <String, String>{};
|
|
||||||
|
|
||||||
for (final entry in tagGroups.entries) {
|
|
||||||
final tagName = entry.key;
|
|
||||||
final paths = entry.value;
|
|
||||||
// Use ${tagName}Api to match old behavior (user -> user_api.dart)
|
|
||||||
final fileName = StringUtils.generateFileName('${tagName}Api');
|
|
||||||
final apiClassName = '${StringUtils.toPascalCase(tagName)}Api';
|
|
||||||
|
|
||||||
final data = _buildApiClassData(paths);
|
|
||||||
data['className'] = apiClassName;
|
|
||||||
data['description'] = '$tagName API 接口定义';
|
|
||||||
data['parts'] = [fileName.replaceAll('.dart', '.g.dart')];
|
|
||||||
data['extraCode'] = '';
|
|
||||||
|
|
||||||
apiFiles[fileName] = templateRenderer.render('api/api_class', data);
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiFiles;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,366 @@
|
||||||
|
import 'package:swagger_generator_flutter/core/config.dart';
|
||||||
|
import 'package:swagger_generator_flutter/core/exceptions.dart';
|
||||||
|
import 'package:swagger_generator_flutter/core/models.dart';
|
||||||
|
import 'package:swagger_generator_flutter/utils/string_utils.dart';
|
||||||
|
|
||||||
|
/// 代码生成器基类
|
||||||
|
/// 定义通用的接口和功能
|
||||||
|
abstract class BaseGenerator {
|
||||||
|
/// 生成器类型
|
||||||
|
String get generatorType;
|
||||||
|
|
||||||
|
/// 生成代码
|
||||||
|
String generate();
|
||||||
|
|
||||||
|
/// 生成文件头注释
|
||||||
|
/// [description] 文件描述
|
||||||
|
/// [fileName] 文件名(可选)
|
||||||
|
String generateFileHeader(String description, {String? fileName}) {
|
||||||
|
final header = StringUtils.generateFileHeader(
|
||||||
|
description,
|
||||||
|
SwaggerConfig.swaggerJsonUrls.isNotEmpty
|
||||||
|
? SwaggerConfig.swaggerJsonUrls.first
|
||||||
|
: '',
|
||||||
|
fileName: fileName,
|
||||||
|
fileType: description,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 添加 lint 忽略注释
|
||||||
|
return '$header\n// ignore_for_file: type=lint, invalid_annotation_target\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成类型安全的代码
|
||||||
|
String generateTypeCheckedCode(String code) {
|
||||||
|
// 基础类型检查和验证
|
||||||
|
try {
|
||||||
|
if (code.trim().isEmpty) {
|
||||||
|
throw CodeGenerationException(
|
||||||
|
'生成的代码不能为空',
|
||||||
|
generatorType: generatorType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保文件以换行符结尾
|
||||||
|
if (!code.endsWith('\n')) {
|
||||||
|
return '$code\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
return code;
|
||||||
|
} catch (e) {
|
||||||
|
throw CodeGenerationException(
|
||||||
|
'代码生成失败',
|
||||||
|
details: e.toString(),
|
||||||
|
generatorType: generatorType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 验证生成的代码是否符合Dart语法规范
|
||||||
|
bool validateDartCode(String code) {
|
||||||
|
// 基础验证规则
|
||||||
|
final validationRules = [
|
||||||
|
// 检查是否有未闭合的大括号
|
||||||
|
(String code) => _countMatches(code, '{') == _countMatches(code, '}'),
|
||||||
|
// 检查是否有未闭合的小括号
|
||||||
|
(String code) => _countMatches(code, '(') == _countMatches(code, ')'),
|
||||||
|
// 检查是否有未闭合的方括号
|
||||||
|
(String code) => _countMatches(code, '[') == _countMatches(code, ']'),
|
||||||
|
// 检查是否有基本的类声明
|
||||||
|
(String code) =>
|
||||||
|
code.contains('class ') ||
|
||||||
|
code.contains('enum ') ||
|
||||||
|
code.contains('const '),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final rule in validationRules) {
|
||||||
|
if (!rule(code)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 计算字符出现次数
|
||||||
|
int _countMatches(String text, String pattern) {
|
||||||
|
return pattern.allMatches(text).length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 模型代码生成器基类
|
||||||
|
abstract class ModelGenerator extends BaseGenerator {
|
||||||
|
ModelGenerator(this.document, {this.useSimpleModels = false});
|
||||||
|
final SwaggerDocument document;
|
||||||
|
final bool useSimpleModels;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get generatorType => 'ModelGenerator';
|
||||||
|
|
||||||
|
/// 生成单个模型的代码
|
||||||
|
String generateModelCode(ApiModel model);
|
||||||
|
|
||||||
|
/// 生成枚举代码
|
||||||
|
String generateEnumCode(ApiModel model) {
|
||||||
|
if (!model.isEnum) {
|
||||||
|
throw CodeGenerationException('模型不是枚举类型', generatorType: generatorType);
|
||||||
|
}
|
||||||
|
|
||||||
|
final className = StringUtils.generateClassName(model.name);
|
||||||
|
final enumType = model.enumType?.value ?? 'string';
|
||||||
|
final valueType =
|
||||||
|
enumType == 'integer' || enumType == 'number' ? 'int' : 'String';
|
||||||
|
final buffer = StringBuffer()
|
||||||
|
// 生成文件头
|
||||||
|
..writeln(generateFileHeader('${model.name} 枚举定义'))
|
||||||
|
..writeln();
|
||||||
|
|
||||||
|
// 生成枚举类
|
||||||
|
if (model.description.isNotEmpty) {
|
||||||
|
buffer.writeln(StringUtils.generateComment(model.description));
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.writeln('enum $className {');
|
||||||
|
|
||||||
|
// 生成枚举值
|
||||||
|
for (var i = 0; i < model.enumValues.length; i++) {
|
||||||
|
final value = model.enumValues[i];
|
||||||
|
final enumName = StringUtils.generateEnumValueName(value, i);
|
||||||
|
final enumLine = enumType == 'integer' || enumType == 'number'
|
||||||
|
? ' $enumName($value),'
|
||||||
|
: " $enumName('$value'),";
|
||||||
|
|
||||||
|
buffer.writeln(enumLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除最后一个逗号
|
||||||
|
final content = buffer.toString().trimRight();
|
||||||
|
buffer
|
||||||
|
..clear()
|
||||||
|
..writeAll(
|
||||||
|
[
|
||||||
|
content.substring(0, content.lastIndexOf(',')),
|
||||||
|
';',
|
||||||
|
'',
|
||||||
|
' const $className(this.value);',
|
||||||
|
' final $valueType value;',
|
||||||
|
'',
|
||||||
|
' static $className fromValue(dynamic value) {',
|
||||||
|
' for (final enumValue in $className.values) {',
|
||||||
|
' if (enumValue.value == value) {',
|
||||||
|
' return enumValue;',
|
||||||
|
' }',
|
||||||
|
' }',
|
||||||
|
r" throw ArgumentError('Unknown enum value: $value');",
|
||||||
|
' }',
|
||||||
|
'',
|
||||||
|
' factory $className.fromJson(dynamic json) {',
|
||||||
|
' return fromValue(json);',
|
||||||
|
' }',
|
||||||
|
'',
|
||||||
|
' dynamic toJson() => value;',
|
||||||
|
'',
|
||||||
|
'}',
|
||||||
|
],
|
||||||
|
'\n',
|
||||||
|
);
|
||||||
|
|
||||||
|
return generateTypeCheckedCode(buffer.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已移动到 StringUtils.generateEnumValueName
|
||||||
|
|
||||||
|
/// 获取导入的类型列表
|
||||||
|
Set<String> getImportedTypes(ApiModel model) {
|
||||||
|
final importedTypes = <String>{};
|
||||||
|
|
||||||
|
model.properties.forEach((_, property) {
|
||||||
|
if (property.type == PropertyType.reference &&
|
||||||
|
property.reference != null) {
|
||||||
|
importedTypes.add(property.reference!);
|
||||||
|
} else if (property.type == PropertyType.array) {
|
||||||
|
// 处理数组类型的引用
|
||||||
|
if (property.items != null) {
|
||||||
|
final itemType = _getItemType(property.items!);
|
||||||
|
// 如果是引用类型(不是基本类型),则添加到导入列表
|
||||||
|
if (itemType != 'String' &&
|
||||||
|
itemType != 'int' &&
|
||||||
|
itemType != 'double' &&
|
||||||
|
itemType != 'bool' &&
|
||||||
|
itemType != 'dynamic') {
|
||||||
|
importedTypes.add(property.items!.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return importedTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成属性的Dart类型
|
||||||
|
String getDartPropertyType(ApiProperty property) {
|
||||||
|
switch (property.type) {
|
||||||
|
case PropertyType.string:
|
||||||
|
switch (property.format) {
|
||||||
|
case 'date':
|
||||||
|
case 'date-time':
|
||||||
|
return 'DateTime';
|
||||||
|
default:
|
||||||
|
return 'String';
|
||||||
|
}
|
||||||
|
case PropertyType.integer:
|
||||||
|
return 'int';
|
||||||
|
case PropertyType.number:
|
||||||
|
return 'double';
|
||||||
|
case PropertyType.boolean:
|
||||||
|
return 'bool';
|
||||||
|
case PropertyType.enumType:
|
||||||
|
return 'String';
|
||||||
|
case PropertyType.array:
|
||||||
|
// 根据数组元素类型推导具体类型
|
||||||
|
if (property.items != null) {
|
||||||
|
final itemType = _getItemType(property.items!);
|
||||||
|
return 'List<$itemType>';
|
||||||
|
}
|
||||||
|
return 'List<dynamic>';
|
||||||
|
case PropertyType.object:
|
||||||
|
return 'Map<String, dynamic>';
|
||||||
|
case PropertyType.reference:
|
||||||
|
return property.reference != null
|
||||||
|
? StringUtils.generateClassName(property.reference!)
|
||||||
|
: 'dynamic';
|
||||||
|
case PropertyType.file:
|
||||||
|
return 'dynamic';
|
||||||
|
case PropertyType.bytes:
|
||||||
|
return 'List<int>';
|
||||||
|
case PropertyType.date:
|
||||||
|
return 'DateTime';
|
||||||
|
case PropertyType.dateTime:
|
||||||
|
return 'DateTime';
|
||||||
|
case PropertyType.unknown:
|
||||||
|
return 'dynamic';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取数组元素的类型
|
||||||
|
String _getItemType(ApiModel items) {
|
||||||
|
// 如果是引用类型,直接返回类名
|
||||||
|
if (items.name != 'string' &&
|
||||||
|
items.name != 'integer' &&
|
||||||
|
items.name != 'number' &&
|
||||||
|
items.name != 'boolean') {
|
||||||
|
return StringUtils.generateClassName(items.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是基本类型,转换为对应的Dart类型
|
||||||
|
switch (items.name) {
|
||||||
|
case 'string':
|
||||||
|
return 'String';
|
||||||
|
case 'integer':
|
||||||
|
return 'int';
|
||||||
|
case 'number':
|
||||||
|
return 'double';
|
||||||
|
case 'boolean':
|
||||||
|
return 'bool';
|
||||||
|
default:
|
||||||
|
return 'dynamic';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 选项配置类
|
||||||
|
class GeneratorOptions {
|
||||||
|
const GeneratorOptions({
|
||||||
|
this.generateEndpoints = true,
|
||||||
|
this.generateModels = true,
|
||||||
|
this.generateDocs = true,
|
||||||
|
this.useSimpleModels = false,
|
||||||
|
this.modelsDirectory = 'models',
|
||||||
|
this.outputDirectory = 'generator',
|
||||||
|
this.endpointsFileName = 'api_paths.dart',
|
||||||
|
this.docsFileName = 'api_documentation.md',
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 从命令行参数创建选项
|
||||||
|
factory GeneratorOptions.fromArgs(List<String> args) {
|
||||||
|
var generateEndpoints = false;
|
||||||
|
var generateModels = false;
|
||||||
|
var generateDocs = false;
|
||||||
|
var useSimpleModels = false;
|
||||||
|
var modelsDirectory = 'models';
|
||||||
|
var outputDirectory = 'generator';
|
||||||
|
var endpointsFileName = 'api_paths.dart';
|
||||||
|
var docsFileName = 'api_documentation.md';
|
||||||
|
|
||||||
|
var hasSpecificOption = false;
|
||||||
|
|
||||||
|
for (var i = 0; i < args.length; i++) {
|
||||||
|
final arg = args[i];
|
||||||
|
|
||||||
|
switch (arg) {
|
||||||
|
case '--endpoints':
|
||||||
|
generateEndpoints = true;
|
||||||
|
hasSpecificOption = true;
|
||||||
|
case '--models':
|
||||||
|
generateModels = true;
|
||||||
|
hasSpecificOption = true;
|
||||||
|
case '--docs':
|
||||||
|
generateDocs = true;
|
||||||
|
hasSpecificOption = true;
|
||||||
|
case '--all':
|
||||||
|
generateEndpoints = true;
|
||||||
|
generateModels = true;
|
||||||
|
generateDocs = true;
|
||||||
|
hasSpecificOption = true;
|
||||||
|
case '--simple':
|
||||||
|
useSimpleModels = true;
|
||||||
|
case '--models-dir':
|
||||||
|
if (i + 1 < args.length) {
|
||||||
|
modelsDirectory = args[i + 1];
|
||||||
|
i++; // 跳过下一个参数
|
||||||
|
}
|
||||||
|
case '--output-dir':
|
||||||
|
if (i + 1 < args.length) {
|
||||||
|
outputDirectory = args[i + 1];
|
||||||
|
i++; // 跳过下一个参数
|
||||||
|
}
|
||||||
|
case '--endpoints-file':
|
||||||
|
if (i + 1 < args.length) {
|
||||||
|
endpointsFileName = args[i + 1];
|
||||||
|
i++; // 跳过下一个参数
|
||||||
|
}
|
||||||
|
case '--docs-file':
|
||||||
|
if (i + 1 < args.length) {
|
||||||
|
docsFileName = args[i + 1];
|
||||||
|
i++; // 跳过下一个参数
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有指定特定选项,默认生成所有文件
|
||||||
|
if (!hasSpecificOption) {
|
||||||
|
generateEndpoints = true;
|
||||||
|
generateModels = true;
|
||||||
|
generateDocs = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GeneratorOptions(
|
||||||
|
generateEndpoints: generateEndpoints,
|
||||||
|
generateModels: generateModels,
|
||||||
|
generateDocs: generateDocs,
|
||||||
|
useSimpleModels: useSimpleModels,
|
||||||
|
modelsDirectory: modelsDirectory,
|
||||||
|
outputDirectory: outputDirectory,
|
||||||
|
endpointsFileName: endpointsFileName,
|
||||||
|
docsFileName: docsFileName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final bool generateEndpoints;
|
||||||
|
final bool generateModels;
|
||||||
|
final bool generateDocs;
|
||||||
|
final bool useSimpleModels;
|
||||||
|
final String modelsDirectory;
|
||||||
|
final String outputDirectory;
|
||||||
|
final String endpointsFileName;
|
||||||
|
final String docsFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
part of 'package:swagger_generator_flutter/generators/model_code_generator.dart';
|
part of '../model_code_generator.dart';
|
||||||
|
|
||||||
String _generateModelCodeWithoutImports(
|
String _generateModelCodeWithoutImports(
|
||||||
ModelCodeGenerator generator,
|
ModelCodeGenerator generator,
|
||||||
|
|
@ -237,3 +237,4 @@ String _needsJsonKeyAnnotation(
|
||||||
|
|
||||||
return annotations.join(', ');
|
return annotations.join(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
part of 'package:swagger_generator_flutter/generators/model_code_generator.dart';
|
part of '../model_code_generator.dart';
|
||||||
|
|
||||||
Map<String, String> buildSeparateModelFiles(ModelCodeGenerator generator) {
|
Map<String, String> buildSeparateModelFiles(ModelCodeGenerator generator) {
|
||||||
final files = <String, String>{};
|
final files = <String, String>{};
|
||||||
|
|
@ -118,3 +118,4 @@ String _getModelSubDirectory(ApiModel model) {
|
||||||
return 'result';
|
return 'result';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
part of 'package:swagger_generator_flutter/generators/model_code_generator.dart';
|
part of '../model_code_generator.dart';
|
||||||
|
|
||||||
String getDartPropertyTypeWithPagination(
|
String getDartPropertyTypeWithPagination(
|
||||||
ModelCodeGenerator generator,
|
ModelCodeGenerator generator,
|
||||||
|
|
@ -57,3 +57,4 @@ bool _isPaginationResponseModel(ApiModel model) {
|
||||||
|
|
||||||
return isTotalNumeric && isItemsArray;
|
return isTotalNumeric && isItemsArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import 'package:swagger_generator_flutter/core/config.dart';
|
||||||
|
import 'package:swagger_generator_flutter/core/models.dart';
|
||||||
|
import 'package:swagger_generator_flutter/generators/base_generator.dart';
|
||||||
|
import 'package:swagger_generator_flutter/utils/string_utils.dart';
|
||||||
|
|
||||||
|
part 'model/model_pagination_helpers.dart';
|
||||||
|
part 'model/model_file_writers.dart';
|
||||||
|
part 'model/model_content_builders.dart';
|
||||||
|
|
||||||
|
/// 模型代码生成器
|
||||||
|
/// 负责生成Dart模型类代码
|
||||||
|
class ModelCodeGenerator extends ModelGenerator {
|
||||||
|
ModelCodeGenerator(super.document);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get generatorType => 'ModelCodeGenerator';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String generate() {
|
||||||
|
throw UnimplementedError(
|
||||||
|
'Single file model generation is no longer supported.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String getDartPropertyType(ApiProperty property) {
|
||||||
|
return getDartPropertyTypeWithPagination(this, property);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 提供对父类实现的访问,便于分页检测逻辑复用
|
||||||
|
String superGetDartPropertyType(ApiProperty property) {
|
||||||
|
return super.getDartPropertyType(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@Deprecated(
|
||||||
|
'Use generateSingleModelFile or generateSeparateModelFiles instead',
|
||||||
|
)
|
||||||
|
String generateModelCode(ApiModel model) {
|
||||||
|
throw UnimplementedError(
|
||||||
|
'generateModelCode is no longer supported. Use generateSingleModelFile.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成所有模型文件,按子目录分组
|
||||||
|
Map<String, String> generateSeparateModelFiles() {
|
||||||
|
return buildSeparateModelFiles(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成单个模型文件
|
||||||
|
String generateSingleModelFile(ApiModel model, {String? fileName}) {
|
||||||
|
return buildSingleModelFile(this, model, fileName: fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成导出索引文件
|
||||||
|
String generateIndexFile(List<String> modelFileNames) {
|
||||||
|
return _buildIndexFile(this, modelFileNames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
part of 'package:swagger_generator_flutter/generators/retrofit_api_generator.dart';
|
part of '../retrofit_api_generator.dart';
|
||||||
|
|
||||||
mixin RetrofitApiGrouping {
|
mixin RetrofitApiGrouping {
|
||||||
RetrofitApiGenerator get _g => this as RetrofitApiGenerator;
|
RetrofitApiGenerator get _g => this as RetrofitApiGenerator;
|
||||||
|
|
@ -51,3 +51,4 @@ mixin RetrofitApiGrouping {
|
||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
part of 'package:swagger_generator_flutter/generators/retrofit_api_generator.dart';
|
part of '../retrofit_api_generator.dart';
|
||||||
|
|
||||||
/// API 方法参数
|
/// API 方法参数
|
||||||
class ApiMethodParameter {
|
class ApiMethodParameter {
|
||||||
|
|
@ -18,3 +18,4 @@ class ApiMethodParameter {
|
||||||
final String description;
|
final String description;
|
||||||
final dynamic defaultValue;
|
final dynamic defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
part of 'package:swagger_generator_flutter/generators/retrofit_api_generator.dart';
|
part of '../retrofit_api_generator.dart';
|
||||||
|
|
||||||
mixin RetrofitApiParameterEntities {
|
mixin RetrofitApiParameterEntities {
|
||||||
RetrofitApiGenerator get _g => this as RetrofitApiGenerator;
|
RetrofitApiGenerator get _g => this as RetrofitApiGenerator;
|
||||||
|
|
@ -120,3 +120,4 @@ mixin RetrofitApiParameterEntities {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
part of 'package:swagger_generator_flutter/generators/retrofit_api_generator.dart';
|
part of '../retrofit_api_generator.dart';
|
||||||
|
|
||||||
mixin RetrofitApiParameters {
|
mixin RetrofitApiParameters {
|
||||||
RetrofitApiGenerator get _g => this as RetrofitApiGenerator;
|
RetrofitApiGenerator get _g => this as RetrofitApiGenerator;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
part of 'package:swagger_generator_flutter/generators/retrofit_api_generator.dart';
|
part of '../retrofit_api_generator.dart';
|
||||||
|
|
||||||
mixin RetrofitApiReturnTypes {
|
mixin RetrofitApiReturnTypes {
|
||||||
RetrofitApiGenerator get _g => this as RetrofitApiGenerator;
|
RetrofitApiGenerator get _g => this as RetrofitApiGenerator;
|
||||||
|
|
@ -288,3 +288,4 @@ mixin RetrofitApiReturnTypes {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
part of 'package:swagger_generator_flutter/generators/retrofit_api_generator.dart';
|
part of '../retrofit_api_generator.dart';
|
||||||
|
|
||||||
mixin RetrofitApiSchemaComposition {
|
mixin RetrofitApiSchemaComposition {
|
||||||
RetrofitApiGenerator get _g => this as RetrofitApiGenerator;
|
RetrofitApiGenerator get _g => this as RetrofitApiGenerator;
|
||||||
|
|
@ -143,3 +143,4 @@ mixin RetrofitApiSchemaComposition {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
part of '../retrofit_api_generator.dart';
|
||||||
|
|
||||||
|
mixin RetrofitApiSchema {
|
||||||
|
RetrofitApiGenerator get _g => this as RetrofitApiGenerator;
|
||||||
|
|
||||||
|
/// 从路径的响应中提取返回类型
|
||||||
|
String? _extractResponseTypeFromPath(ApiPath path) {
|
||||||
|
final successResponses = ['200', '201', '202'];
|
||||||
|
|
||||||
|
for (final statusCode in successResponses) {
|
||||||
|
final response = path.responses[statusCode];
|
||||||
|
if (response != null) {
|
||||||
|
final type = _extractResponseType(response);
|
||||||
|
if (type != null) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从响应中提取返回类型
|
||||||
|
String? _extractResponseType(ApiResponse response) {
|
||||||
|
final applicationJsonMediaType = response.content['application/json'];
|
||||||
|
if (applicationJsonMediaType != null) {
|
||||||
|
final schema = applicationJsonMediaType.schema;
|
||||||
|
final type = _extractTypeFromSchema(schema);
|
||||||
|
if (type != null) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.schema != null) {
|
||||||
|
final type = _extractTypeFromSchema(response.schema);
|
||||||
|
if (type != null) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从 schema 中提取类型
|
||||||
|
String? _extractTypeFromSchema(Map<String, dynamic>? schema) {
|
||||||
|
if (schema == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema[r'$ref'] != null) {
|
||||||
|
final ref = schema[r'$ref'] as String;
|
||||||
|
final refName = ref.split('/').last;
|
||||||
|
return StringUtils.generateClassName(refName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema['type'] != null) {
|
||||||
|
final type = schema['type'] as String;
|
||||||
|
if (type == 'array') {
|
||||||
|
final items = schema['items'];
|
||||||
|
if (items is Map<String, dynamic>) {
|
||||||
|
final itemType = _extractTypeFromSchema(items);
|
||||||
|
return 'List<${itemType ?? 'dynamic'}>';
|
||||||
|
} else {
|
||||||
|
return 'List<dynamic>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return _mapJsonTypeToFlutterType(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema['allOf'] != null ||
|
||||||
|
schema['oneOf'] != null ||
|
||||||
|
schema['anyOf'] != null) {
|
||||||
|
return _extractTypeFromCompositionSchema(schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'dynamic';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查 schema 是否为数组类型
|
||||||
|
bool _isArraySchema(Map<String, dynamic> schema) {
|
||||||
|
if (schema['type'] == 'array') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema[r'$ref'] != null) {
|
||||||
|
final refName = (schema[r'$ref'] as String).split('/').last;
|
||||||
|
final refModel = _g.document.models[refName];
|
||||||
|
if (refModel != null) {
|
||||||
|
return refModel.type == 'array';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查 schema 是否为分页模型
|
||||||
|
bool _hasPaginationSchema(Map<String, dynamic> schema) {
|
||||||
|
if (schema[r'$ref'] != null) {
|
||||||
|
final refName = (schema[r'$ref'] as String).split('/').last;
|
||||||
|
final refModel = _g.document.models[refName];
|
||||||
|
if (refModel != null) {
|
||||||
|
return _isPaginationResponseModel(refModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成简单方法名
|
||||||
|
String _generateSimpleMethodName(ApiPath path) {
|
||||||
|
if (path.operationId.isNotEmpty) {
|
||||||
|
return StringUtils.toCamelCase(path.operationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
final pathParts = path.path
|
||||||
|
.split('/')
|
||||||
|
.where((part) => part.isNotEmpty && !part.startsWith('{'))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (pathParts.isEmpty) {
|
||||||
|
return 'unnamedMethod';
|
||||||
|
}
|
||||||
|
|
||||||
|
final lastPart = pathParts.last.replaceAll(RegExp(r'\W+'), '');
|
||||||
|
final methodName = '${path.method.value.toLowerCase()}${StringUtils.toPascalCase(lastPart)}';
|
||||||
|
|
||||||
|
return methodName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查是否是分页响应模型(包含 total 和 items 字段)
|
||||||
|
bool _isPaginationResponseModel(ApiModel model) {
|
||||||
|
if (!model.properties.containsKey('total') ||
|
||||||
|
!model.properties.containsKey('items')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final totalProp = model.properties['total']!;
|
||||||
|
final itemsProp = model.properties['items']!;
|
||||||
|
|
||||||
|
final isTotalNumeric = totalProp.type == PropertyType.integer ||
|
||||||
|
totalProp.type == PropertyType.number;
|
||||||
|
final isItemsArray = itemsProp.type == PropertyType.array;
|
||||||
|
|
||||||
|
return isTotalNumeric && isItemsArray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
part of '../retrofit_api_generator.dart';
|
||||||
|
|
||||||
|
mixin RetrofitApiTemplateData {
|
||||||
|
RetrofitApiGenerator get _g => this as RetrofitApiGenerator;
|
||||||
|
|
||||||
|
List<String> _getMainImports() {
|
||||||
|
final tagGroups = _g._groupPathsByTags();
|
||||||
|
final tagImports =
|
||||||
|
tagGroups.keys.map((tag) => "import '${StringUtils.generateFileName(tag)}.dart';").toList();
|
||||||
|
|
||||||
|
final config = ConfigRepository.loadSync();
|
||||||
|
final customImports = config.packageImports;
|
||||||
|
|
||||||
|
return [...customImports, ...tagImports];
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _buildTagApisData() {
|
||||||
|
final tagGroups = _g._groupPathsByTags();
|
||||||
|
return {
|
||||||
|
'apis': tagGroups.entries.map((entry) {
|
||||||
|
final tagName = entry.key;
|
||||||
|
return {
|
||||||
|
'name': StringUtils.toCamelCase(tagName),
|
||||||
|
'className': '${StringUtils.toPascalCase(tagName)}Api',
|
||||||
|
};
|
||||||
|
}).toList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _buildApiClassData(List<ApiPath> paths) {
|
||||||
|
final baseUrl =
|
||||||
|
_g.document.servers.isNotEmpty ? _g.document.servers.first.url : '';
|
||||||
|
final fileName =
|
||||||
|
_g.className.isNotEmpty ? StringUtils.generateFileName(_g.className) : '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
'description': _g.document.description,
|
||||||
|
'apiUrl': baseUrl,
|
||||||
|
'className': _g.className,
|
||||||
|
'imports': _getImportsForPaths(paths),
|
||||||
|
'methods': _buildMethodsData(paths),
|
||||||
|
'parts': [fileName.replaceAll('.dart', '.g.dart')],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> _getImportsForPaths(List<ApiPath> paths) {
|
||||||
|
final imports = <String>{};
|
||||||
|
imports.add("import 'package:dio/dio.dart';");
|
||||||
|
imports.add("import 'package:retrofit/retrofit.dart';");
|
||||||
|
|
||||||
|
final config = ConfigRepository.loadSync();
|
||||||
|
imports.addAll(config.packageImports.map((i) => "import '$i';"));
|
||||||
|
|
||||||
|
return imports.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Map<String, dynamic>> _buildMethodsData(List<ApiPath> paths) {
|
||||||
|
return paths.map(_buildMethodData).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _buildMethodData(ApiPath path) {
|
||||||
|
return {
|
||||||
|
'docLines': _buildDocLines(path),
|
||||||
|
'annotations': _buildAnnotations(path),
|
||||||
|
'returnType': _g._generateReturnType(path),
|
||||||
|
'methodName': _g._generateSimpleMethodName(path),
|
||||||
|
'parameters': _buildParametersData(path),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Map<String, String>> _buildDocLines(ApiPath path) {
|
||||||
|
final docLines = <String>[];
|
||||||
|
if (path.summary.isNotEmpty) {
|
||||||
|
docLines.add(path.summary);
|
||||||
|
}
|
||||||
|
if (path.description.isNotEmpty) {
|
||||||
|
docLines.add(path.description);
|
||||||
|
}
|
||||||
|
return docLines.map((line) => {'line': line}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Map<String, String>> _buildAnnotations(ApiPath path) {
|
||||||
|
final annotations = <String>[];
|
||||||
|
final method = path.method.value.toUpperCase();
|
||||||
|
annotations.add('@$method("${path.path}")');
|
||||||
|
|
||||||
|
if (path.isMultipart) {
|
||||||
|
annotations.add('@MultiPart()');
|
||||||
|
}
|
||||||
|
|
||||||
|
return annotations.map((line) => {'line': line}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Map<String, dynamic>> _buildParametersData(ApiPath path) {
|
||||||
|
return _g._generateParameters(path).map((p) {
|
||||||
|
return {
|
||||||
|
'annotation': p.annotation,
|
||||||
|
'type': p.type,
|
||||||
|
'name': p.name,
|
||||||
|
'required': p.required,
|
||||||
|
'isLast': false, // This will be updated later
|
||||||
|
};
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _buildSecuritySchemesData(SwaggerDocument document) {
|
||||||
|
final schemes = document.components.securitySchemes.values.toList();
|
||||||
|
return {
|
||||||
|
'hasSecuritySchemes': schemes.isNotEmpty,
|
||||||
|
'securitySchemes': schemes.map((s) => {'name': s.name, 'type': s.type, 'scheme': s.scheme}).toList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
import 'package:swagger_generator_flutter/core/config_repository.dart';
|
||||||
|
import 'package:swagger_generator_flutter/core/models.dart';
|
||||||
|
import 'package:swagger_generator_flutter/core/template_renderer.dart';
|
||||||
|
import 'package:swagger_generator_flutter/generators/base_generator.dart';
|
||||||
|
import 'package:swagger_generator_flutter/utils/string_utils.dart';
|
||||||
|
|
||||||
|
part 'retrofit_api/api_grouping.dart';
|
||||||
|
part 'retrofit_api/api_method_parameter.dart';
|
||||||
|
part 'retrofit_api/api_parameter_entities.dart';
|
||||||
|
part 'retrofit_api/api_parameters.dart';
|
||||||
|
part 'retrofit_api/api_return_types.dart';
|
||||||
|
part 'retrofit_api/api_schema_composition.dart';
|
||||||
|
part 'retrofit_api/api_schema_extraction.dart';
|
||||||
|
part 'retrofit_api/api_template_data.dart';
|
||||||
|
|
||||||
|
/// Retrofit 风格的 API 生成器
|
||||||
|
/// 负责生成带有注解的 API 接口类
|
||||||
|
class RetrofitApiGenerator extends BaseGenerator
|
||||||
|
with
|
||||||
|
RetrofitApiGrouping,
|
||||||
|
RetrofitApiTemplateData,
|
||||||
|
RetrofitApiSchemaComposition,
|
||||||
|
RetrofitApiSchema,
|
||||||
|
RetrofitApiReturnTypes,
|
||||||
|
RetrofitApiParameters,
|
||||||
|
RetrofitApiParameterEntities {
|
||||||
|
RetrofitApiGenerator({
|
||||||
|
this.className = 'ApiClient',
|
||||||
|
this.useRetrofit = true,
|
||||||
|
this.useDio = true,
|
||||||
|
this.splitByTags = true, // 默认启用拆分模式
|
||||||
|
this.generateModels = true,
|
||||||
|
this.versionedApi = true, // 默认启用版本化
|
||||||
|
});
|
||||||
|
|
||||||
|
final String className;
|
||||||
|
final bool useRetrofit;
|
||||||
|
final bool useDio;
|
||||||
|
final bool splitByTags;
|
||||||
|
final bool generateModels;
|
||||||
|
final bool versionedApi; // 是否启用版本化 API
|
||||||
|
|
||||||
|
late SwaggerDocument document;
|
||||||
|
final templateRenderer = TemplateRenderer();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get generatorType => 'RetrofitApiGenerator';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String generate() {
|
||||||
|
throw UnimplementedError('Use generateFromDocument instead');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成 API 代码
|
||||||
|
String generateFromDocument(SwaggerDocument document) {
|
||||||
|
this.document = document; // 设置文档引用
|
||||||
|
if (splitByTags) {
|
||||||
|
// 按 tags 分组生成多个文件时,返回主文件内容
|
||||||
|
return generateMainApiFile();
|
||||||
|
}
|
||||||
|
return generateSingleApiFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成单个 API 文件
|
||||||
|
String generateSingleApiFile() {
|
||||||
|
final paths = document.paths.values.toList();
|
||||||
|
|
||||||
|
// Build extra code
|
||||||
|
final extraCodeBuffer = StringBuffer()
|
||||||
|
..write(
|
||||||
|
templateRenderer.render(
|
||||||
|
'api/security_schemes',
|
||||||
|
_buildSecuritySchemesData(document),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
..write(templateRenderer.render('api/media_type_handlers', {}))
|
||||||
|
..write(templateRenderer.render('api/file_upload_handlers', {}))
|
||||||
|
..write(templateRenderer.render('api/encoding_handlers', {}));
|
||||||
|
|
||||||
|
final data = _buildApiClassData(paths);
|
||||||
|
data['extraCode'] = extraCodeBuffer.toString();
|
||||||
|
|
||||||
|
return templateRenderer.render('api/api_class', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成主 API 文件(当按 tags 分组时)
|
||||||
|
String generateMainApiFile() {
|
||||||
|
final data = {
|
||||||
|
'description': '主 API 接口定义 - 集合所有 Tag 的 API',
|
||||||
|
'apiUrl': document.servers.isNotEmpty ? document.servers.first.url : '',
|
||||||
|
'imports': _getMainImports(),
|
||||||
|
'className': className,
|
||||||
|
'tagApis': _buildTagApisData(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return templateRenderer.render('api/main_api', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 按 tags 分组生成多个 API 文件
|
||||||
|
Map<String, String> generateApiFilesByTags() {
|
||||||
|
final tagGroups = _groupPathsByTags();
|
||||||
|
final apiFiles = <String, String>{};
|
||||||
|
|
||||||
|
for (final entry in tagGroups.entries) {
|
||||||
|
final tagName = entry.key;
|
||||||
|
final paths = entry.value;
|
||||||
|
// Use ${tagName}Api to match old behavior (user -> user_api.dart)
|
||||||
|
final fileName = StringUtils.generateFileName('${tagName}Api');
|
||||||
|
final apiClassName = '${StringUtils.toPascalCase(tagName)}Api';
|
||||||
|
|
||||||
|
final data = _buildApiClassData(paths);
|
||||||
|
data['className'] = apiClassName;
|
||||||
|
data['description'] = '$tagName API 接口定义';
|
||||||
|
data['parts'] = [fileName.replaceAll('.dart', '.g.dart')];
|
||||||
|
data['extraCode'] = '';
|
||||||
|
|
||||||
|
apiFiles[fileName] = templateRenderer.render('api/api_class', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiFiles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,643 @@
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -82,3 +82,4 @@ class TemplateLoader {
|
||||||
_cache.clear();
|
_cache.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:mustache_template/mustache_template.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:swagger_generator_flutter/core/config_repository.dart';
|
||||||
|
import 'package:swagger_generator_flutter/utils/path_resolver.dart';
|
||||||
|
|
||||||
|
part 'template/template_loader.dart';
|
||||||
|
|
||||||
|
/// 模板渲染器
|
||||||
|
/// 负责加载和渲染 Mustache 模板,支持文件覆盖与内置模板
|
||||||
|
class TemplateRenderer {
|
||||||
|
TemplateRenderer({
|
||||||
|
String? templateRoot,
|
||||||
|
List<String>? extraTemplateRoots,
|
||||||
|
}) : _loader = TemplateLoader(
|
||||||
|
customRoot: templateRoot,
|
||||||
|
extraRoots: extraTemplateRoots,
|
||||||
|
),
|
||||||
|
_baseContext = _buildBaseContext();
|
||||||
|
|
||||||
|
final TemplateLoader _loader;
|
||||||
|
final Map<String, Template> _templateCache = {};
|
||||||
|
final Map<String, dynamic> _baseContext;
|
||||||
|
|
||||||
|
/// 渲染模板
|
||||||
|
///
|
||||||
|
/// [templateName] 模板名称(不含 .mustache 扩展名)
|
||||||
|
/// [data] 模板数据
|
||||||
|
String render(
|
||||||
|
String templateName,
|
||||||
|
Map<String, dynamic> data, {
|
||||||
|
Map<String, String>? partials,
|
||||||
|
}) {
|
||||||
|
final template = _getTemplate(templateName);
|
||||||
|
final context = {..._baseContext, ...data};
|
||||||
|
return template.renderString(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 部分模板解析器
|
||||||
|
Template? _partialResolver(String name) {
|
||||||
|
try {
|
||||||
|
return _getTemplate(name);
|
||||||
|
} on Exception {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取模板(带缓存)
|
||||||
|
Template _getTemplate(String templateName) {
|
||||||
|
if (_templateCache.containsKey(templateName)) {
|
||||||
|
return _templateCache[templateName]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
final source = _getTemplateSource(templateName);
|
||||||
|
final template = Template(
|
||||||
|
source,
|
||||||
|
name: templateName,
|
||||||
|
lenient: true,
|
||||||
|
htmlEscapeValues: false,
|
||||||
|
partialResolver: _partialResolver,
|
||||||
|
);
|
||||||
|
_templateCache[templateName] = template;
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取模板源码:优先文件,其次内嵌
|
||||||
|
String _getTemplateSource(String templateName) {
|
||||||
|
final fileTemplate = _loader.load(templateName);
|
||||||
|
if (fileTemplate != null) {
|
||||||
|
return fileTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception('Template not found in file system: $templateName');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 清除模板缓存
|
||||||
|
void clearCache() {
|
||||||
|
_templateCache.clear();
|
||||||
|
_loader.clearCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, dynamic> _buildBaseContext() {
|
||||||
|
// Load once synchronously to avoid repeated disk IO
|
||||||
|
final config = ConfigRepository.loadSync();
|
||||||
|
return {
|
||||||
|
'generatorName': config.generatorName,
|
||||||
|
'author': config.author,
|
||||||
|
'copyright': config.copyright,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue