diff --git a/lib/commands/services/generation_output_service.dart b/lib/commands/services/generation_output_service.dart index c44d3fa..ad58acb 100644 --- a/lib/commands/services/generation_output_service.dart +++ b/lib/commands/services/generation_output_service.dart @@ -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 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 _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 _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> _groupPathsByVersion(SwaggerDocument document) { - final pathsByVersion = >{}; - for (final path in document.paths.values) { - final version = _extractVersionFromPath(path.path); - pathsByVersion.putIfAbsent(version, () => []).add(path); - } - return pathsByVersion; - } - - Future>> _buildVersionedApis( - SwaggerDocument document, - Map> pathsByVersion, - LogCallback progress, - ) async { - final versionedFiles = >{}; - 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 _writeVersionedApis( - String apiDir, - Map> 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 _writeMainApiFile( - String apiDir, - Map> 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 _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 _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> _getAllModelFiles(String modelsDir) async { - try { - final directory = Directory(modelsDir); - if (!directory.existsSync()) { - return []; - } - - final files = directory.listSync(); - final exportPaths = []; - - 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 _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 = []; - - 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 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 _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> 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 = >{}; - - 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 _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 _generateVersionIndexFile( - String versionDir, - List 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'; diff --git a/lib/core/template_renderer.dart b/lib/core/template_renderer.dart index 5500229..b8c1abe 100644 --- a/lib/core/template_renderer.dart +++ b/lib/core/template_renderer.dart @@ -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? extraTemplateRoots, - }) : _loader = TemplateLoader( - customRoot: templateRoot, - extraRoots: extraTemplateRoots, - ), - _baseContext = _buildBaseContext(); - - final TemplateLoader _loader; - final Map _templateCache = {}; - final Map _baseContext; - - /// 渲染模板 - /// - /// [templateName] 模板名称(不含 .mustache 扩展名) - /// [data] 模板数据 - String render( - String templateName, - Map data, { - Map? 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 _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'; diff --git a/lib/generators/base_generator.dart b/lib/generators/base_generator.dart index 37eba9e..b97b129 100644 --- a/lib/generators/base_generator.dart +++ b/lib/generators/base_generator.dart @@ -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 getImportedTypes(ApiModel model) { - final importedTypes = {}; - - 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'; - case PropertyType.object: - return 'Map'; - case PropertyType.reference: - return property.reference != null - ? StringUtils.generateClassName(property.reference!) - : 'dynamic'; - case PropertyType.file: - return 'dynamic'; - case PropertyType.bytes: - return 'List'; - 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 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'; diff --git a/lib/generators/model_code_generator.dart b/lib/generators/model_code_generator.dart index b398aed..3756f58 100644 --- a/lib/generators/model_code_generator.dart +++ b/lib/generators/model_code_generator.dart @@ -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 generateSeparateModelFiles() { - return buildSeparateModelFiles(this); - } - - /// 生成单个模型文件 - String generateSingleModelFile(ApiModel model, {String? fileName}) { - return buildSingleModelFile(this, model, fileName: fileName); - } - - /// 生成导出索引文件 - String generateIndexFile(List modelFileNames) { - return _buildIndexFile(this, modelFileNames); - } -} +export 'package:swagger_generator_flutter/pipeline/generate/impl/model_code_generator.dart'; diff --git a/lib/generators/retrofit_api/api_schema_extraction.dart b/lib/generators/retrofit_api/api_schema_extraction.dart deleted file mode 100644 index 6aeaac7..0000000 --- a/lib/generators/retrofit_api/api_schema_extraction.dart +++ /dev/null @@ -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? 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; - 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; - - if (properties.containsKey('total') && - properties.containsKey('items')) { - final totalProp = properties['total'] as Map?; - final itemsProp = properties['items'] as Map?; - - 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; - final itemType = _extractTypeFromSchema(itemsSchema); - if (itemType != null) { - return 'List<$itemType>'; - } - } - } - - return 'Map'; - } - if (schema['additionalProperties'] != null) { - return 'Map'; - } - if (schema['allOf'] != null || - schema['anyOf'] != null || - schema['oneOf'] != null) { - return 'Map'; - } - } - - 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?; - if (items != null) { - final itemType = _extractTypeFromSchema(items); - return 'List<${itemType ?? 'dynamic'}>'; - } - return 'List'; - case 'null': - return 'dynamic'; - default: - return 'dynamic'; - } - } - - if (schema['enum'] != null) { - return 'String'; - } - - return null; - } - - /// 处理高级 Schema 特性 - String? _handleAdvancedSchemaFeatures(Map 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' : 'Map'; - } else if (additionalProps is Map) { - final valueType = _extractTypeFromSchema(additionalProps); - return 'Map'; - } - } - - if (schema['patternProperties'] != null) { - final patternProps = schema['patternProperties'] as Map?; - if (patternProps != null && patternProps.isNotEmpty) { - return 'Map'; - } - } - - if (schema['if'] != null || - schema['then'] != null || - schema['else'] != null) { - if (schema['then'] != null) { - final thenType = - _extractTypeFromSchema(schema['then'] as Map?); - if (thenType != null) return thenType; - } - if (schema['else'] != null) { - final elseType = - _extractTypeFromSchema(schema['else'] as Map?); - if (elseType != null) return elseType; - } - return 'dynamic'; - } - - return null; - } - - /// 检查 schema 是否包含分页结构(total 和 items 字段) - bool _hasPaginationSchema(Map schema) { - if (schema['type'] != 'object') return false; - - final properties = schema['properties'] as Map?; - if (properties == null) return false; - - if (!properties.containsKey('total') || !properties.containsKey('items')) { - return false; - } - - final totalProp = properties['total'] as Map?; - final itemsProp = properties['items'] as Map?; - - 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 schema) { - return schema['type'] == 'array'; - } -} diff --git a/lib/generators/retrofit_api/api_template_data.dart b/lib/generators/retrofit_api/api_template_data.dart deleted file mode 100644 index b018904..0000000 --- a/lib/generators/retrofit_api/api_template_data.dart +++ /dev/null @@ -1,325 +0,0 @@ -part of 'package:swagger_generator_flutter/generators/retrofit_api_generator.dart'; - -mixin RetrofitApiTemplateData { - RetrofitApiGenerator get _g => this as RetrofitApiGenerator; - List _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> _buildTagApisData() { - final tagGroups = _g._groupPathsByTags(); - return tagGroups.keys - .map( - (tagName) => { - 'tagName': tagName, - 'apiClassName': '${StringUtils.toPascalCase(tagName)}Api', - 'propertyName': StringUtils.toCamelCase(tagName), - }, - ) - .toList(); - } - - Map _buildApiClassData(List 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 _getImports() { - final config = ConfigRepository.loadSync(); - return [ - ...config.packageImports, - '../../api_models/index.dart', - ]; - } - - List> _buildMethodsData(List paths) { - return paths.map(_buildMethodData).toList(); - } - - Map _buildMethodData(ApiPath path) { - return { - 'docLines': _buildDocLines(path), - 'annotations': _buildAnnotations(path), - 'returnType': _g._generateReturnType(path), - 'methodName': _g._generateSimpleMethodName(path), - 'params': _buildParamsData(path), - }; - } - - List _buildDocLines(ApiPath path) { - final lines = []; - 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 = []; - 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 _wrapParamDocLine(String paramDoc) { - const maxLength = 76; // 80 - '/// '.length - - if (paramDoc.length <= maxLength) { - return [paramDoc]; - } - - final lines = []; - - // 提取参数名和描述部分 - 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 _wrapDocLine(String text, {String prefix = ''}) { - const maxLength = 76; // 80 - '/// '.length,留一点余量 - final effectiveMaxLength = maxLength - prefix.length; - - if (text.length <= effectiveMaxLength) { - return [prefix + text]; - } - - final lines = []; - 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 _buildAnnotations(ApiPath path) { - final annotations = []; - 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> _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 _buildSecuritySchemesData(SwaggerDocument document) { - final schemes = >[]; - - document.components.securitySchemes.forEach((name, scheme) { - final constantName = StringUtils.generateConstantName(name); - - final schemeData = { - '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); - } -} diff --git a/lib/generators/retrofit_api_generator.dart b/lib/generators/retrofit_api_generator.dart index 0b7c2c2..0b253ed 100644 --- a/lib/generators/retrofit_api_generator.dart +++ b/lib/generators/retrofit_api_generator.dart @@ -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 generateApiFilesByTags() { - final tagGroups = _groupPathsByTags(); - final apiFiles = {}; - - 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'; diff --git a/lib/pipeline/generate/impl/base_generator.dart b/lib/pipeline/generate/impl/base_generator.dart new file mode 100644 index 0000000..6016fec --- /dev/null +++ b/lib/pipeline/generate/impl/base_generator.dart @@ -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 getImportedTypes(ApiModel model) { + final importedTypes = {}; + + 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'; + case PropertyType.object: + return 'Map'; + case PropertyType.reference: + return property.reference != null + ? StringUtils.generateClassName(property.reference!) + : 'dynamic'; + case PropertyType.file: + return 'dynamic'; + case PropertyType.bytes: + return 'List'; + 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 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; +} + diff --git a/lib/generators/model/model_content_builders.dart b/lib/pipeline/generate/impl/model/model_content_builders.dart similarity index 98% rename from lib/generators/model/model_content_builders.dart rename to lib/pipeline/generate/impl/model/model_content_builders.dart index d6a42d6..e46728f 100644 --- a/lib/generators/model/model_content_builders.dart +++ b/lib/pipeline/generate/impl/model/model_content_builders.dart @@ -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(', '); } + diff --git a/lib/generators/model/model_file_writers.dart b/lib/pipeline/generate/impl/model/model_file_writers.dart similarity index 97% rename from lib/generators/model/model_file_writers.dart rename to lib/pipeline/generate/impl/model/model_file_writers.dart index 94b3009..178746e 100644 --- a/lib/generators/model/model_file_writers.dart +++ b/lib/pipeline/generate/impl/model/model_file_writers.dart @@ -1,4 +1,4 @@ -part of 'package:swagger_generator_flutter/generators/model_code_generator.dart'; +part of '../model_code_generator.dart'; Map buildSeparateModelFiles(ModelCodeGenerator generator) { final files = {}; @@ -118,3 +118,4 @@ String _getModelSubDirectory(ApiModel model) { return 'result'; } } + diff --git a/lib/generators/model/model_pagination_helpers.dart b/lib/pipeline/generate/impl/model/model_pagination_helpers.dart similarity index 95% rename from lib/generators/model/model_pagination_helpers.dart rename to lib/pipeline/generate/impl/model/model_pagination_helpers.dart index f774e30..f7b5386 100644 --- a/lib/generators/model/model_pagination_helpers.dart +++ b/lib/pipeline/generate/impl/model/model_pagination_helpers.dart @@ -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; } + diff --git a/lib/pipeline/generate/impl/model_code_generator.dart b/lib/pipeline/generate/impl/model_code_generator.dart new file mode 100644 index 0000000..2df1259 --- /dev/null +++ b/lib/pipeline/generate/impl/model_code_generator.dart @@ -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 generateSeparateModelFiles() { + return buildSeparateModelFiles(this); + } + + /// 生成单个模型文件 + String generateSingleModelFile(ApiModel model, {String? fileName}) { + return buildSingleModelFile(this, model, fileName: fileName); + } + + /// 生成导出索引文件 + String generateIndexFile(List modelFileNames) { + return _buildIndexFile(this, modelFileNames); + } +} + diff --git a/lib/generators/retrofit_api/api_grouping.dart b/lib/pipeline/generate/impl/retrofit_api/api_grouping.dart similarity index 94% rename from lib/generators/retrofit_api/api_grouping.dart rename to lib/pipeline/generate/impl/retrofit_api/api_grouping.dart index 8696f54..e8aef73 100644 --- a/lib/generators/retrofit_api/api_grouping.dart +++ b/lib/pipeline/generate/impl/retrofit_api/api_grouping.dart @@ -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; } } + diff --git a/lib/generators/retrofit_api/api_method_parameter.dart b/lib/pipeline/generate/impl/retrofit_api/api_method_parameter.dart similarity index 82% rename from lib/generators/retrofit_api/api_method_parameter.dart rename to lib/pipeline/generate/impl/retrofit_api/api_method_parameter.dart index fb1db82..295da91 100644 --- a/lib/generators/retrofit_api/api_method_parameter.dart +++ b/lib/pipeline/generate/impl/retrofit_api/api_method_parameter.dart @@ -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; } + diff --git a/lib/generators/retrofit_api/api_parameter_entities.dart b/lib/pipeline/generate/impl/retrofit_api/api_parameter_entities.dart similarity index 97% rename from lib/generators/retrofit_api/api_parameter_entities.dart rename to lib/pipeline/generate/impl/retrofit_api/api_parameter_entities.dart index 61896a9..ede0b4d 100644 --- a/lib/generators/retrofit_api/api_parameter_entities.dart +++ b/lib/pipeline/generate/impl/retrofit_api/api_parameter_entities.dart @@ -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 { } } } + diff --git a/lib/generators/retrofit_api/api_parameters.dart b/lib/pipeline/generate/impl/retrofit_api/api_parameters.dart similarity index 98% rename from lib/generators/retrofit_api/api_parameters.dart rename to lib/pipeline/generate/impl/retrofit_api/api_parameters.dart index 6d41baf..62268de 100644 --- a/lib/generators/retrofit_api/api_parameters.dart +++ b/lib/pipeline/generate/impl/retrofit_api/api_parameters.dart @@ -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; diff --git a/lib/generators/retrofit_api/api_return_types.dart b/lib/pipeline/generate/impl/retrofit_api/api_return_types.dart similarity index 98% rename from lib/generators/retrofit_api/api_return_types.dart rename to lib/pipeline/generate/impl/retrofit_api/api_return_types.dart index c65f552..206f0f2 100644 --- a/lib/generators/retrofit_api/api_return_types.dart +++ b/lib/pipeline/generate/impl/retrofit_api/api_return_types.dart @@ -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; } } + diff --git a/lib/generators/retrofit_api/api_schema_composition.dart b/lib/pipeline/generate/impl/retrofit_api/api_schema_composition.dart similarity index 98% rename from lib/generators/retrofit_api/api_schema_composition.dart rename to lib/pipeline/generate/impl/retrofit_api/api_schema_composition.dart index 0ee348c..6225d3e 100644 --- a/lib/generators/retrofit_api/api_schema_composition.dart +++ b/lib/pipeline/generate/impl/retrofit_api/api_schema_composition.dart @@ -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 { } } } + diff --git a/lib/pipeline/generate/impl/retrofit_api/api_schema_extraction.dart b/lib/pipeline/generate/impl/retrofit_api/api_schema_extraction.dart new file mode 100644 index 0000000..c42c2b9 --- /dev/null +++ b/lib/pipeline/generate/impl/retrofit_api/api_schema_extraction.dart @@ -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? 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) { + final itemType = _extractTypeFromSchema(items); + return 'List<${itemType ?? 'dynamic'}>'; + } else { + return 'List'; + } + } else { + return _mapJsonTypeToFlutterType(type); + } + } + + if (schema['allOf'] != null || + schema['oneOf'] != null || + schema['anyOf'] != null) { + return _extractTypeFromCompositionSchema(schema); + } + + return 'dynamic'; + } + + /// 检查 schema 是否为数组类型 + bool _isArraySchema(Map 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 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; + } +} + diff --git a/lib/pipeline/generate/impl/retrofit_api/api_template_data.dart b/lib/pipeline/generate/impl/retrofit_api/api_template_data.dart new file mode 100644 index 0000000..0d3938b --- /dev/null +++ b/lib/pipeline/generate/impl/retrofit_api/api_template_data.dart @@ -0,0 +1,114 @@ +part of '../retrofit_api_generator.dart'; + +mixin RetrofitApiTemplateData { + RetrofitApiGenerator get _g => this as RetrofitApiGenerator; + + List _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 _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 _buildApiClassData(List 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 _getImportsForPaths(List paths) { + final imports = {}; + 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> _buildMethodsData(List paths) { + return paths.map(_buildMethodData).toList(); + } + + Map _buildMethodData(ApiPath path) { + return { + 'docLines': _buildDocLines(path), + 'annotations': _buildAnnotations(path), + 'returnType': _g._generateReturnType(path), + 'methodName': _g._generateSimpleMethodName(path), + 'parameters': _buildParametersData(path), + }; + } + + List> _buildDocLines(ApiPath path) { + final docLines = []; + if (path.summary.isNotEmpty) { + docLines.add(path.summary); + } + if (path.description.isNotEmpty) { + docLines.add(path.description); + } + return docLines.map((line) => {'line': line}).toList(); + } + + List> _buildAnnotations(ApiPath path) { + final annotations = []; + 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> _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 _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(), + }; + } +} + diff --git a/lib/pipeline/generate/impl/retrofit_api_generator.dart b/lib/pipeline/generate/impl/retrofit_api_generator.dart new file mode 100644 index 0000000..434c82c --- /dev/null +++ b/lib/pipeline/generate/impl/retrofit_api_generator.dart @@ -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 generateApiFilesByTags() { + final tagGroups = _groupPathsByTags(); + final apiFiles = {}; + + 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; + } +} + diff --git a/lib/pipeline/output/impl/generation_output_service.dart b/lib/pipeline/output/impl/generation_output_service.dart new file mode 100644 index 0000000..cb8d985 --- /dev/null +++ b/lib/pipeline/output/impl/generation_output_service.dart @@ -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 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 _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 _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> _groupPathsByVersion(SwaggerDocument document) { + final pathsByVersion = >{}; + for (final path in document.paths.values) { + final version = _extractVersionFromPath(path.path); + pathsByVersion.putIfAbsent(version, () => []).add(path); + } + return pathsByVersion; + } + + Future>> _buildVersionedApis( + SwaggerDocument document, + Map> pathsByVersion, + LogCallback progress, + ) async { + final versionedFiles = >{}; + 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 _writeVersionedApis( + String apiDir, + Map> 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 _writeMainApiFile( + String apiDir, + Map> 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 _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 _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> _getAllModelFiles(String modelsDir) async { + try { + final directory = Directory(modelsDir); + if (!directory.existsSync()) { + return []; + } + + final files = directory.listSync(); + final exportPaths = []; + + 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 _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 = []; + + 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 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 _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> 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 = >{}; + + 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 _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 _generateVersionIndexFile( + String versionDir, + List 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()); + } +} + diff --git a/lib/core/template/template_loader.dart b/lib/pipeline/render/impl/template/template_loader.dart similarity index 99% rename from lib/core/template/template_loader.dart rename to lib/pipeline/render/impl/template/template_loader.dart index f2e6622..da5c208 100644 --- a/lib/core/template/template_loader.dart +++ b/lib/pipeline/render/impl/template/template_loader.dart @@ -82,3 +82,4 @@ class TemplateLoader { _cache.clear(); } } + diff --git a/lib/pipeline/render/impl/template_renderer.dart b/lib/pipeline/render/impl/template_renderer.dart new file mode 100644 index 0000000..001766a --- /dev/null +++ b/lib/pipeline/render/impl/template_renderer.dart @@ -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? extraTemplateRoots, + }) : _loader = TemplateLoader( + customRoot: templateRoot, + extraRoots: extraTemplateRoots, + ), + _baseContext = _buildBaseContext(); + + final TemplateLoader _loader; + final Map _templateCache = {}; + final Map _baseContext; + + /// 渲染模板 + /// + /// [templateName] 模板名称(不含 .mustache 扩展名) + /// [data] 模板数据 + String render( + String templateName, + Map data, { + Map? 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 _buildBaseContext() { + // Load once synchronously to avoid repeated disk IO + final config = ConfigRepository.loadSync(); + return { + 'generatorName': config.generatorName, + 'author': config.author, + 'copyright': config.copyright, + }; + } +} +