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:
Max 2025-11-22 22:11:12 +08:00
parent ceab0b6f19
commit 7627236650
24 changed files with 1580 additions and 1896 deletions

View File

@ -1,657 +1,4 @@
import 'dart:io';
/// Backward-compat shim for GenerationOutputService
library;
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());
}
}
export 'package:swagger_generator_flutter/pipeline/output/impl/generation_output_service.dart';

View File

@ -1,92 +1,4 @@
import 'dart:io';
/// Backward-compat shim for TemplateRenderer
library;
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,
};
}
}
export 'package:swagger_generator_flutter/pipeline/render/impl/template_renderer.dart';

View File

@ -1,365 +1,4 @@
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';
/// Backward-compat shim for BaseGenerator
library;
///
///
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;
}
export 'package:swagger_generator_flutter/pipeline/generate/impl/base_generator.dart';

View File

@ -1,59 +1,4 @@
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';
/// Backward-compat shim for ModelCodeGenerator
library;
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);
}
}
export 'package:swagger_generator_flutter/pipeline/generate/impl/model_code_generator.dart';

View File

@ -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';
}
}

View File

@ -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);
}
}

View File

@ -1,122 +1,4 @@
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';
/// Backward-compat shim for RetrofitApiGenerator
library;
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;
}
}
export 'package:swagger_generator_flutter/pipeline/generate/impl/retrofit_api_generator.dart';

View File

@ -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;
}

View File

@ -1,4 +1,4 @@
part of 'package:swagger_generator_flutter/generators/model_code_generator.dart';
part of '../model_code_generator.dart';
String _generateModelCodeWithoutImports(
ModelCodeGenerator generator,
@ -237,3 +237,4 @@ String _needsJsonKeyAnnotation(
return annotations.join(', ');
}

View File

@ -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) {
final files = <String, String>{};
@ -118,3 +118,4 @@ String _getModelSubDirectory(ApiModel model) {
return 'result';
}
}

View File

@ -1,4 +1,4 @@
part of 'package:swagger_generator_flutter/generators/model_code_generator.dart';
part of '../model_code_generator.dart';
String getDartPropertyTypeWithPagination(
ModelCodeGenerator generator,
@ -57,3 +57,4 @@ bool _isPaginationResponseModel(ApiModel model) {
return isTotalNumeric && isItemsArray;
}

View File

@ -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);
}
}

View File

@ -1,4 +1,4 @@
part of 'package:swagger_generator_flutter/generators/retrofit_api_generator.dart';
part of '../retrofit_api_generator.dart';
mixin RetrofitApiGrouping {
RetrofitApiGenerator get _g => this as RetrofitApiGenerator;
@ -51,3 +51,4 @@ mixin RetrofitApiGrouping {
return groups;
}
}

View File

@ -1,4 +1,4 @@
part of 'package:swagger_generator_flutter/generators/retrofit_api_generator.dart';
part of '../retrofit_api_generator.dart';
/// API
class ApiMethodParameter {
@ -18,3 +18,4 @@ class ApiMethodParameter {
final String description;
final dynamic defaultValue;
}

View File

@ -1,4 +1,4 @@
part of 'package:swagger_generator_flutter/generators/retrofit_api_generator.dart';
part of '../retrofit_api_generator.dart';
mixin RetrofitApiParameterEntities {
RetrofitApiGenerator get _g => this as RetrofitApiGenerator;
@ -120,3 +120,4 @@ mixin RetrofitApiParameterEntities {
}
}
}

View File

@ -1,4 +1,4 @@
part of 'package:swagger_generator_flutter/generators/retrofit_api_generator.dart';
part of '../retrofit_api_generator.dart';
mixin RetrofitApiParameters {
RetrofitApiGenerator get _g => this as RetrofitApiGenerator;

View File

@ -1,4 +1,4 @@
part of 'package:swagger_generator_flutter/generators/retrofit_api_generator.dart';
part of '../retrofit_api_generator.dart';
mixin RetrofitApiReturnTypes {
RetrofitApiGenerator get _g => this as RetrofitApiGenerator;
@ -288,3 +288,4 @@ mixin RetrofitApiReturnTypes {
return false;
}
}

View File

@ -1,4 +1,4 @@
part of 'package:swagger_generator_flutter/generators/retrofit_api_generator.dart';
part of '../retrofit_api_generator.dart';
mixin RetrofitApiSchemaComposition {
RetrofitApiGenerator get _g => this as RetrofitApiGenerator;
@ -143,3 +143,4 @@ mixin RetrofitApiSchemaComposition {
}
}
}

View File

@ -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;
}
}

View File

@ -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(),
};
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -82,3 +82,4 @@ class TemplateLoader {
_cache.clear();
}
}

View File

@ -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,
};
}
}