init
This commit is contained in:
parent
f2faffebfc
commit
5c9d2c8d36
|
|
@ -0,0 +1,82 @@
|
|||
#!/usr/bin/env dart
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import '../lib/swagger_cli_new.dart';
|
||||
|
||||
/// Swagger CLI 工具主入口
|
||||
///
|
||||
/// 这是一个强大的 Swagger API 代码生成工具,可以:
|
||||
/// - 解析 Swagger/OpenAPI 文档
|
||||
/// - 生成 Dart 模型类
|
||||
/// - 生成 API 端点常量
|
||||
/// - 生成完整的 API 文档
|
||||
/// - 提供类型安全的代码生成
|
||||
///
|
||||
/// 使用方法:
|
||||
/// dart run swagger_cli <command> [options]
|
||||
///
|
||||
/// 可用命令:
|
||||
/// - generate: 生成代码文件
|
||||
/// - help: 显示帮助信息
|
||||
/// - version: 显示版本信息
|
||||
Future<void> main(List<String> arguments) async {
|
||||
// 检查是否有参数
|
||||
if (arguments.isEmpty) {
|
||||
_showWelcome();
|
||||
arguments = ['help'];
|
||||
}
|
||||
|
||||
// 检查特殊命令
|
||||
if (arguments.contains('--version') || arguments.contains('-v')) {
|
||||
_showVersion();
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用新版本CLI
|
||||
final cli = SwaggerCLI();
|
||||
final exitCode = await cli.run(arguments);
|
||||
|
||||
// 设置退出代码
|
||||
exit(exitCode);
|
||||
}
|
||||
|
||||
/// 显示欢迎信息
|
||||
void _showWelcome() {
|
||||
print('');
|
||||
print('🚀 欢迎使用 Swagger CLI 工具!');
|
||||
print('');
|
||||
print('这是一个强大的 Swagger API 代码生成工具,可以帮助您:');
|
||||
print('');
|
||||
print(' 📋 解析 Swagger/OpenAPI 文档');
|
||||
print(' 🛠️ 生成 Dart 模型类');
|
||||
print(' 📡 生成 API 端点常量');
|
||||
print(' 📚 生成完整的 API 文档');
|
||||
print(' 🔒 提供类型安全的代码生成');
|
||||
print('');
|
||||
print('使用 --help 查看详细帮助信息');
|
||||
print('');
|
||||
}
|
||||
|
||||
/// 显示版本信息
|
||||
void _showVersion() {
|
||||
print('');
|
||||
print('🚀 Swagger CLI 工具 v2.0.0');
|
||||
print('');
|
||||
print('构建信息:');
|
||||
print(' - Dart SDK: ${Platform.version}');
|
||||
print(' - 平台: ${Platform.operatingSystem}');
|
||||
print(' - 架构: ${Platform.version}');
|
||||
print('');
|
||||
print('特性:');
|
||||
print(' ✨ 现代化的命令行界面');
|
||||
print(' 🏗️ 模块化架构设计');
|
||||
print(' 🚀 高性能代码生成');
|
||||
print(' 🔍 智能类型验证');
|
||||
print(' 📊 性能监控和分析');
|
||||
print(' 💾 智能缓存机制');
|
||||
print(' 📝 丰富的文档生成');
|
||||
print('');
|
||||
print('更多信息请访问: https://github.com/yourorg/swagger_cli');
|
||||
print('');
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
targets:
|
||||
$default:
|
||||
sources:
|
||||
- "lib/**"
|
||||
builders:
|
||||
json_serializable:
|
||||
generate_for:
|
||||
- "lib/**"
|
||||
|
|
@ -0,0 +1,356 @@
|
|||
import '../core/exceptions.dart';
|
||||
|
||||
/// 命令基类
|
||||
/// 实现命令模式,提供统一的命令接口
|
||||
abstract class BaseCommand {
|
||||
/// 命令名称
|
||||
String get name;
|
||||
|
||||
/// 命令描述
|
||||
String get description;
|
||||
|
||||
/// 命令用法
|
||||
String get usage;
|
||||
|
||||
/// 支持的选项
|
||||
List<CommandOption> get options => [];
|
||||
|
||||
/// 支持的参数
|
||||
List<CommandArgument> get arguments => [];
|
||||
|
||||
/// 执行命令
|
||||
Future<int> execute(List<String> args);
|
||||
|
||||
/// 显示帮助信息
|
||||
void showHelp() {
|
||||
print('');
|
||||
print('命令: $name');
|
||||
print('描述: $description');
|
||||
print('用法: $usage');
|
||||
|
||||
if (arguments.isNotEmpty) {
|
||||
print('');
|
||||
print('参数:');
|
||||
for (final arg in arguments) {
|
||||
final required = arg.required ? '(必填)' : '(可选)';
|
||||
print(' ${arg.name} ${arg.description} $required');
|
||||
}
|
||||
}
|
||||
|
||||
if (options.isNotEmpty) {
|
||||
print('');
|
||||
print('选项:');
|
||||
for (final option in options) {
|
||||
final short = option.shortName != null ? '-${option.shortName}, ' : '';
|
||||
final defaultValue = option.defaultValue != null
|
||||
? ' (默认: ${option.defaultValue})'
|
||||
: '';
|
||||
print(' $short--${option.name} ${option.description}$defaultValue');
|
||||
}
|
||||
}
|
||||
|
||||
print('');
|
||||
}
|
||||
|
||||
/// 解析命令行参数
|
||||
ParsedArguments parseArguments(List<String> args) {
|
||||
final parser = ArgumentParser(this);
|
||||
return parser.parse(args);
|
||||
}
|
||||
|
||||
/// 验证参数
|
||||
void validateArguments(ParsedArguments parsedArgs) {
|
||||
// 验证必填参数
|
||||
for (final arg in arguments.where((a) => a.required)) {
|
||||
if (!parsedArgs.hasArgument(arg.name)) {
|
||||
throw CommandException('缺少必填参数: ${arg.name}');
|
||||
}
|
||||
}
|
||||
|
||||
// 验证必填选项
|
||||
for (final option in options.where((o) => o.required)) {
|
||||
if (!parsedArgs.hasOption(option.name)) {
|
||||
throw CommandException('缺少必填选项: --${option.name}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理执行前的准备工作
|
||||
Future<void> prepare(ParsedArguments parsedArgs) async {
|
||||
// 子类可以重写此方法进行准备工作
|
||||
}
|
||||
|
||||
/// 处理执行后的清理工作
|
||||
Future<void> cleanup(ParsedArguments parsedArgs) async {
|
||||
// 子类可以重写此方法进行清理工作
|
||||
}
|
||||
|
||||
/// 错误处理
|
||||
void handleError(dynamic error, StackTrace stackTrace) {
|
||||
if (error is CommandException) {
|
||||
print('❌ 错误: ${error.message}');
|
||||
if (error.details != null) {
|
||||
print('详细信息: ${error.details}');
|
||||
}
|
||||
} else if (error is SwaggerException) {
|
||||
print('❌ Swagger错误: ${error.message}');
|
||||
if (error.details != null) {
|
||||
print('详细信息: ${error.details}');
|
||||
}
|
||||
} else {
|
||||
print('❌ 未知错误: $error');
|
||||
print('堆栈跟踪: $stackTrace');
|
||||
}
|
||||
}
|
||||
|
||||
/// 成功消息
|
||||
void success(String message) {
|
||||
print('✅ $message');
|
||||
}
|
||||
|
||||
/// 信息消息
|
||||
void info(String message) {
|
||||
print('ℹ️ $message');
|
||||
}
|
||||
|
||||
/// 警告消息
|
||||
void warning(String message) {
|
||||
print('⚠️ $message');
|
||||
}
|
||||
|
||||
/// 进度消息
|
||||
void progress(String message) {
|
||||
print('🔄 $message');
|
||||
}
|
||||
}
|
||||
|
||||
/// 命令选项
|
||||
class CommandOption {
|
||||
final String name;
|
||||
final String? shortName;
|
||||
final String description;
|
||||
final bool required;
|
||||
final dynamic defaultValue;
|
||||
final OptionType type;
|
||||
|
||||
const CommandOption({
|
||||
required this.name,
|
||||
this.shortName,
|
||||
required this.description,
|
||||
this.required = false,
|
||||
this.defaultValue,
|
||||
this.type = OptionType.flag,
|
||||
});
|
||||
}
|
||||
|
||||
/// 命令参数
|
||||
class CommandArgument {
|
||||
final String name;
|
||||
final String description;
|
||||
final bool required;
|
||||
final dynamic defaultValue;
|
||||
|
||||
const CommandArgument({
|
||||
required this.name,
|
||||
required this.description,
|
||||
this.required = true,
|
||||
this.defaultValue,
|
||||
});
|
||||
}
|
||||
|
||||
/// 选项类型
|
||||
enum OptionType { flag, string, int, double, list }
|
||||
|
||||
/// 解析后的参数
|
||||
class ParsedArguments {
|
||||
final Map<String, dynamic> _options = {};
|
||||
final Map<String, dynamic> _arguments = {};
|
||||
|
||||
/// 设置选项值
|
||||
void setOption(String name, dynamic value) {
|
||||
_options[name] = value;
|
||||
}
|
||||
|
||||
/// 设置参数值
|
||||
void setArgument(String name, dynamic value) {
|
||||
_arguments[name] = value;
|
||||
}
|
||||
|
||||
/// 获取选项值
|
||||
T? getOption<T>(String name) {
|
||||
return _options[name] as T?;
|
||||
}
|
||||
|
||||
/// 获取参数值
|
||||
T? getArgument<T>(String name) {
|
||||
return _arguments[name] as T?;
|
||||
}
|
||||
|
||||
/// 检查是否有选项
|
||||
bool hasOption(String name) {
|
||||
return _options.containsKey(name);
|
||||
}
|
||||
|
||||
/// 检查是否有参数
|
||||
bool hasArgument(String name) {
|
||||
return _arguments.containsKey(name);
|
||||
}
|
||||
|
||||
/// 获取所有选项
|
||||
Map<String, dynamic> get options => Map.unmodifiable(_options);
|
||||
|
||||
/// 获取所有参数
|
||||
Map<String, dynamic> get arguments => Map.unmodifiable(_arguments);
|
||||
}
|
||||
|
||||
/// 参数解析器
|
||||
class ArgumentParser {
|
||||
final BaseCommand command;
|
||||
|
||||
ArgumentParser(this.command);
|
||||
|
||||
/// 解析参数
|
||||
ParsedArguments parse(List<String> args) {
|
||||
final result = ParsedArguments();
|
||||
final argQueue = List<String>.from(args);
|
||||
|
||||
int argumentIndex = 0;
|
||||
|
||||
while (argQueue.isNotEmpty) {
|
||||
final current = argQueue.removeAt(0);
|
||||
|
||||
if (current.startsWith('--')) {
|
||||
// 长选项
|
||||
_parseLongOption(current, argQueue, result);
|
||||
} else if (current.startsWith('-') && current.length > 1) {
|
||||
// 短选项
|
||||
_parseShortOption(current, argQueue, result);
|
||||
} else {
|
||||
// 位置参数
|
||||
if (argumentIndex < command.arguments.length) {
|
||||
final arg = command.arguments[argumentIndex];
|
||||
result.setArgument(arg.name, current);
|
||||
argumentIndex++;
|
||||
} else {
|
||||
throw CommandException('未知参数: $current');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
_setDefaultValues(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 解析长选项
|
||||
void _parseLongOption(
|
||||
String current,
|
||||
List<String> argQueue,
|
||||
ParsedArguments result,
|
||||
) {
|
||||
String optionName;
|
||||
String? optionValue;
|
||||
|
||||
if (current.contains('=')) {
|
||||
final parts = current.split('=');
|
||||
optionName = parts[0].substring(2);
|
||||
optionValue = parts.sublist(1).join('=');
|
||||
} else {
|
||||
optionName = current.substring(2);
|
||||
}
|
||||
|
||||
final option = command.options.firstWhere(
|
||||
(o) => o.name == optionName,
|
||||
orElse: () => throw CommandException('未知选项: --$optionName'),
|
||||
);
|
||||
|
||||
if (option.type == OptionType.flag) {
|
||||
result.setOption(optionName, true);
|
||||
} else {
|
||||
if (optionValue == null) {
|
||||
if (argQueue.isEmpty) {
|
||||
throw CommandException('选项 --$optionName 需要一个值');
|
||||
}
|
||||
optionValue = argQueue.removeAt(0);
|
||||
}
|
||||
|
||||
final convertedValue = _convertValue(optionValue, option.type);
|
||||
result.setOption(optionName, convertedValue);
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析短选项
|
||||
void _parseShortOption(
|
||||
String current,
|
||||
List<String> argQueue,
|
||||
ParsedArguments result,
|
||||
) {
|
||||
final shortName = current.substring(1);
|
||||
|
||||
final option = command.options.firstWhere(
|
||||
(o) => o.shortName == shortName,
|
||||
orElse: () => throw CommandException('未知选项: -$shortName'),
|
||||
);
|
||||
|
||||
if (option.type == OptionType.flag) {
|
||||
result.setOption(option.name, true);
|
||||
} else {
|
||||
if (argQueue.isEmpty) {
|
||||
throw CommandException('选项 -$shortName 需要一个值');
|
||||
}
|
||||
|
||||
final optionValue = argQueue.removeAt(0);
|
||||
final convertedValue = _convertValue(optionValue, option.type);
|
||||
result.setOption(option.name, convertedValue);
|
||||
}
|
||||
}
|
||||
|
||||
/// 转换值类型
|
||||
dynamic _convertValue(String value, OptionType type) {
|
||||
switch (type) {
|
||||
case OptionType.string:
|
||||
return value;
|
||||
case OptionType.int:
|
||||
return int.tryParse(value) ??
|
||||
(throw CommandException('无效的整数值: $value'));
|
||||
case OptionType.double:
|
||||
return double.tryParse(value) ??
|
||||
(throw CommandException('无效的浮点数值: $value'));
|
||||
case OptionType.list:
|
||||
return value.split(',').map((s) => s.trim()).toList();
|
||||
case OptionType.flag:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置默认值
|
||||
void _setDefaultValues(ParsedArguments result) {
|
||||
// 设置选项默认值
|
||||
for (final option in command.options) {
|
||||
if (!result.hasOption(option.name) && option.defaultValue != null) {
|
||||
result.setOption(option.name, option.defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置参数默认值
|
||||
for (final argument in command.arguments) {
|
||||
if (!result.hasArgument(argument.name) && argument.defaultValue != null) {
|
||||
result.setArgument(argument.name, argument.defaultValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 命令异常
|
||||
class CommandException implements Exception {
|
||||
final String message;
|
||||
final String? details;
|
||||
|
||||
const CommandException(this.message, {this.details});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'CommandException: $message${details != null ? ' ($details)' : ''}';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,312 @@
|
|||
import '../core/models.dart';
|
||||
import '../generators/documentation_generator.dart';
|
||||
import '../generators/endpoint_code_generator.dart';
|
||||
import '../generators/model_code_generator.dart';
|
||||
import '../generators/retrofit_api_generator.dart';
|
||||
import '../parsers/swagger_data_parser.dart';
|
||||
import '../utils/file_utils.dart';
|
||||
import 'base_command.dart';
|
||||
|
||||
/// Generate命令
|
||||
/// 用于生成各种代码文件
|
||||
class GenerateCommand extends BaseCommand {
|
||||
@override
|
||||
String get name => 'generate';
|
||||
|
||||
@override
|
||||
String get description => '生成API代码文件(模型、端点、文档等)';
|
||||
|
||||
@override
|
||||
String get usage => 'dart swagger_cli.dart generate [options]';
|
||||
|
||||
@override
|
||||
List<CommandOption> get options => [
|
||||
const CommandOption(
|
||||
name: 'endpoints',
|
||||
shortName: 'e',
|
||||
description: '生成API端点常量',
|
||||
type: OptionType.flag,
|
||||
),
|
||||
const CommandOption(
|
||||
name: 'models',
|
||||
shortName: 'm',
|
||||
description: '生成数据模型',
|
||||
type: OptionType.flag,
|
||||
),
|
||||
const CommandOption(
|
||||
name: 'docs',
|
||||
shortName: 'd',
|
||||
description: '生成API文档',
|
||||
type: OptionType.flag,
|
||||
),
|
||||
const CommandOption(
|
||||
name: 'api',
|
||||
shortName: 'r',
|
||||
description: '生成Retrofit风格API接口',
|
||||
type: OptionType.flag,
|
||||
),
|
||||
const CommandOption(
|
||||
name: 'split-by-tags',
|
||||
shortName: 't',
|
||||
description: '按tags分组生成多个API文件',
|
||||
type: OptionType.flag,
|
||||
),
|
||||
const CommandOption(
|
||||
name: 'all',
|
||||
shortName: 'a',
|
||||
description: '生成所有文件(默认)',
|
||||
type: OptionType.flag,
|
||||
),
|
||||
const CommandOption(
|
||||
name: 'simple',
|
||||
shortName: 's',
|
||||
description: '使用简洁版模型生成',
|
||||
type: OptionType.flag,
|
||||
),
|
||||
const CommandOption(
|
||||
name: 'output-dir',
|
||||
shortName: 'o',
|
||||
description: '输出目录',
|
||||
type: OptionType.string,
|
||||
defaultValue: 'generator',
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Future<int> execute(List<String> args) async {
|
||||
try {
|
||||
final parsedArgs = parseArguments(args);
|
||||
validateArguments(parsedArgs);
|
||||
|
||||
await prepare(parsedArgs);
|
||||
|
||||
// 解析Swagger文档
|
||||
progress('正在解析Swagger文档...');
|
||||
final parser = SwaggerDataParser();
|
||||
final document = await parser.fetchAndParseSwaggerDocument();
|
||||
|
||||
// 解析生成选项
|
||||
final options = _parseGenerateOptions(parsedArgs);
|
||||
final outputDir =
|
||||
parsedArgs.getOption<String>('output-dir') ?? 'generator';
|
||||
final fullOutputDir = FileUtils.getProjectRootGeneratorDir();
|
||||
|
||||
progress('输出目录: $fullOutputDir');
|
||||
|
||||
// 确保输出目录存在
|
||||
await FileUtils.ensureDirectoryExists(fullOutputDir);
|
||||
|
||||
int generatedFiles = 0;
|
||||
|
||||
// 生成端点代码
|
||||
if (options.generateEndpoints) {
|
||||
progress('正在生成API端点常量...');
|
||||
final generator = EndpointCodeGenerator(document);
|
||||
final code = generator.generate();
|
||||
|
||||
final filePath = '$fullOutputDir/api_paths.dart';
|
||||
await FileUtils.writeFile(filePath, code);
|
||||
success('API端点常量已保存到: $filePath');
|
||||
generatedFiles++;
|
||||
}
|
||||
|
||||
// 生成模型代码
|
||||
if (options.generateModels) {
|
||||
progress('正在生成数据模型...');
|
||||
final generator = ModelCodeGenerator(
|
||||
document,
|
||||
useSimpleModels: options.useSimpleModels,
|
||||
);
|
||||
|
||||
final modelsDir = '$fullOutputDir/api_models';
|
||||
await FileUtils.ensureDirectoryExists(modelsDir);
|
||||
|
||||
final modelFiles = generator.generateSeparateModelFiles();
|
||||
|
||||
for (final entry in modelFiles.entries) {
|
||||
final filePath = '$modelsDir/${entry.key}';
|
||||
await FileUtils.writeFile(filePath, entry.value);
|
||||
success('模型文件已保存到: $filePath');
|
||||
generatedFiles++;
|
||||
}
|
||||
}
|
||||
|
||||
// 生成 Retrofit 风格的 API 接口
|
||||
if (options.generateApi) {
|
||||
if (options.splitByTags) {
|
||||
progress('正在按tags分组生成Retrofit风格API接口...');
|
||||
final generator = RetrofitApiGenerator(
|
||||
document,
|
||||
className: 'ApiClient',
|
||||
useRetrofit: true,
|
||||
useDio: true,
|
||||
splitByTags: true,
|
||||
);
|
||||
|
||||
// 确保参数实体类已生成
|
||||
generator.ensureParameterEntitiesGenerated();
|
||||
|
||||
// 生成按 tags 分组的多个 API 文件
|
||||
final tagApiFiles = generator.generateApiFilesByTags();
|
||||
|
||||
final apiDir = '$fullOutputDir/api';
|
||||
await FileUtils.ensureDirectoryExists(apiDir);
|
||||
|
||||
for (final entry in tagApiFiles.entries) {
|
||||
final fileName = entry.key;
|
||||
final code = entry.value;
|
||||
final filePath = '$apiDir/$fileName';
|
||||
await FileUtils.writeFile(filePath, code);
|
||||
success('API接口文件已保存到: $filePath');
|
||||
generatedFiles++;
|
||||
}
|
||||
|
||||
// 生成主 API 文件
|
||||
final mainCode = generator.generateMainApiFile();
|
||||
final mainFilePath = '$apiDir/api_client.dart';
|
||||
await FileUtils.writeFile(mainFilePath, mainCode);
|
||||
success('主API接口文件已保存到: $mainFilePath');
|
||||
generatedFiles++;
|
||||
|
||||
// 生成参数实体类文件
|
||||
final parameterEntityFiles = generator.generateParameterEntityFiles();
|
||||
if (parameterEntityFiles.isNotEmpty) {
|
||||
final modelsDir = '$fullOutputDir/api_models';
|
||||
await FileUtils.ensureDirectoryExists(modelsDir);
|
||||
|
||||
for (final entry in parameterEntityFiles.entries) {
|
||||
final filePath = '$modelsDir/${entry.key}';
|
||||
await FileUtils.writeFile(filePath, entry.value);
|
||||
success('参数实体类文件已保存到: $filePath');
|
||||
generatedFiles++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
progress('正在生成Retrofit风格API接口...');
|
||||
final generator = RetrofitApiGenerator(
|
||||
document,
|
||||
className: 'ApiClient',
|
||||
useRetrofit: true,
|
||||
useDio: true,
|
||||
splitByTags: false,
|
||||
);
|
||||
|
||||
// 确保参数实体类已生成
|
||||
generator.ensureParameterEntitiesGenerated();
|
||||
|
||||
final code = generator.generate();
|
||||
|
||||
final apiDir = '$fullOutputDir/api';
|
||||
await FileUtils.ensureDirectoryExists(apiDir);
|
||||
|
||||
final filePath = '$apiDir/api.dart';
|
||||
await FileUtils.writeFile(filePath, code);
|
||||
success('Retrofit API接口已保存到: $filePath');
|
||||
generatedFiles++;
|
||||
|
||||
// 生成参数实体类文件
|
||||
final parameterEntityFiles = generator.generateParameterEntityFiles();
|
||||
if (parameterEntityFiles.isNotEmpty) {
|
||||
final modelsDir = '$fullOutputDir/api_models';
|
||||
await FileUtils.ensureDirectoryExists(modelsDir);
|
||||
|
||||
for (final entry in parameterEntityFiles.entries) {
|
||||
final filePath = '$modelsDir/${entry.key}';
|
||||
await FileUtils.writeFile(filePath, entry.value);
|
||||
success('参数实体类文件已保存到: $filePath');
|
||||
generatedFiles++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 生成文档
|
||||
if (options.generateDocs) {
|
||||
progress('正在生成API文档...');
|
||||
final generator = DocumentationGenerator(document);
|
||||
final code = generator.generate();
|
||||
|
||||
final filePath = '$fullOutputDir/api_documentation.md';
|
||||
await FileUtils.writeFile(filePath, code);
|
||||
success('API文档已保存到: $filePath');
|
||||
generatedFiles++;
|
||||
}
|
||||
|
||||
// 生成摘要
|
||||
_generateSummary(document, fullOutputDir);
|
||||
|
||||
success('代码生成完成!共生成 $generatedFiles 个文件');
|
||||
return 0;
|
||||
} catch (e) {
|
||||
print('❌ 生成失败: $e');
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析生成选项
|
||||
GenerateOptions _parseGenerateOptions(ParsedArguments args) {
|
||||
final hasAnyFlag = args.hasOption('endpoints') ||
|
||||
args.hasOption('models') ||
|
||||
args.hasOption('docs') ||
|
||||
args.hasOption('api');
|
||||
|
||||
return GenerateOptions(
|
||||
generateEndpoints: hasAnyFlag
|
||||
? (args.getOption<bool>('endpoints') ?? false)
|
||||
: (args.getOption<bool>('all') ?? true),
|
||||
generateModels: hasAnyFlag
|
||||
? (args.getOption<bool>('models') ?? false)
|
||||
: (args.getOption<bool>('all') ?? true),
|
||||
generateDocs: hasAnyFlag
|
||||
? (args.getOption<bool>('docs') ?? false)
|
||||
: (args.getOption<bool>('all') ?? true),
|
||||
generateApi: hasAnyFlag
|
||||
? (args.getOption<bool>('api') ?? false)
|
||||
: (args.getOption<bool>('all') ?? true),
|
||||
useSimpleModels: args.getOption<bool>('simple') ?? false,
|
||||
splitByTags: args.getOption<bool>('split-by-tags') ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
/// 生成摘要信息
|
||||
void _generateSummary(SwaggerDocument document, String outputDir) {
|
||||
final summary = StringBuffer();
|
||||
summary.writeln('# 代码生成摘要');
|
||||
summary.writeln('');
|
||||
summary.writeln('**API标题**: ${document.title}');
|
||||
summary.writeln('**API版本**: ${document.version}');
|
||||
summary.writeln('**生成时间**: ${DateTime.now().toIso8601String()}');
|
||||
summary.writeln('');
|
||||
summary.writeln('## 统计信息');
|
||||
summary.writeln('- 控制器数量: ${document.controllers.length}');
|
||||
summary.writeln('- API路径数量: ${document.paths.length}');
|
||||
summary.writeln('- 数据模型数量: ${document.models.length}');
|
||||
summary.writeln('');
|
||||
summary.writeln('## 控制器列表');
|
||||
document.controllers.forEach((name, controller) {
|
||||
summary.writeln(
|
||||
'- **$name**: ${controller.description} (${controller.paths.length} 个路径)');
|
||||
});
|
||||
|
||||
FileUtils.writeFile('$outputDir/SUMMARY.md', summary.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成选项
|
||||
class GenerateOptions {
|
||||
final bool generateEndpoints;
|
||||
final bool generateModels;
|
||||
final bool generateDocs;
|
||||
final bool generateApi;
|
||||
final bool useSimpleModels;
|
||||
final bool splitByTags;
|
||||
|
||||
const GenerateOptions({
|
||||
required this.generateEndpoints,
|
||||
required this.generateModels,
|
||||
required this.generateDocs,
|
||||
required this.generateApi,
|
||||
required this.useSimpleModels,
|
||||
required this.splitByTags,
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
/// Swagger配置管理
|
||||
/// 集中管理所有Swagger相关的配置项
|
||||
class SwaggerConfig {
|
||||
/// Swagger JSON文档的URL
|
||||
static const String swaggerJsonUrl =
|
||||
'https://quanxue-test-api.w.23544.com:8843/swagger/v1/swagger.json';
|
||||
|
||||
/// 基础API URL
|
||||
static const String baseUrl = 'https://quanxue-test-api.w.23544.com:8843';
|
||||
|
||||
/// API版本
|
||||
static const String apiVersion = '/api/v1';
|
||||
|
||||
/// 默认生成器输出目录
|
||||
static const String defaultGeneratorDir = 'generator';
|
||||
|
||||
/// 默认API文件目录
|
||||
static const String defaultApiDir = 'api';
|
||||
|
||||
/// 默认模型文件目录
|
||||
static const String defaultModelsDir = 'api_models';
|
||||
|
||||
/// 默认端点文件名
|
||||
static const String defaultEndpointsFile = 'generated_api_paths.dart';
|
||||
|
||||
/// 默认文档文件名
|
||||
static const String defaultDocumentationFile =
|
||||
'generated_api_documentation.md';
|
||||
|
||||
/// HTTP请求头配置
|
||||
static const Map<String, String> httpHeaders = {
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'Flutter-SwaggerParser/1.0',
|
||||
};
|
||||
|
||||
/// 生成选项配置
|
||||
static const Map<String, dynamic> defaultGenerateOptions = {
|
||||
'generateEndpoints': true,
|
||||
'generateModels': true,
|
||||
'generateDocs': true,
|
||||
'useSimpleModels': false,
|
||||
'separateModelFiles': true,
|
||||
};
|
||||
|
||||
/// 获取完整的API基础URL
|
||||
static String get fullApiUrl => '$baseUrl$apiVersion';
|
||||
|
||||
/// 获取控制器描述
|
||||
/// 优先使用Swagger文档中的描述,否则使用控制器名称
|
||||
static String getControllerDescription(
|
||||
String controllerName, {
|
||||
String? swaggerDescription,
|
||||
}) {
|
||||
// 1. 使用Swagger文档中的描述
|
||||
if (swaggerDescription != null && swaggerDescription.isNotEmpty) {
|
||||
return swaggerDescription;
|
||||
}
|
||||
|
||||
// 2. 使用控制器名称
|
||||
return controllerName;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,608 @@
|
|||
import 'dart:io';
|
||||
|
||||
/// Swagger CLI 基础异常类
|
||||
abstract class SwaggerException implements Exception {
|
||||
final String message;
|
||||
final String? details;
|
||||
final DateTime timestamp;
|
||||
|
||||
SwaggerException(this.message, {this.details}) : timestamp = DateTime.now();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
if (details != null) {
|
||||
return '$runtimeType: $message\n详细信息: $details';
|
||||
}
|
||||
return '$runtimeType: $message';
|
||||
}
|
||||
}
|
||||
|
||||
/// Swagger解析异常
|
||||
class SwaggerParseException extends SwaggerException {
|
||||
final String? url;
|
||||
final int? statusCode;
|
||||
final String? operation;
|
||||
|
||||
SwaggerParseException(
|
||||
super.message, {
|
||||
super.details,
|
||||
this.url,
|
||||
this.statusCode,
|
||||
this.operation,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final buffer = StringBuffer();
|
||||
buffer.writeln('SwaggerParseException: $message');
|
||||
|
||||
if (url != null) {
|
||||
buffer.writeln('URL: $url');
|
||||
}
|
||||
|
||||
if (statusCode != null) {
|
||||
buffer.writeln('状态码: $statusCode');
|
||||
}
|
||||
|
||||
if (operation != null) {
|
||||
buffer.writeln('操作: $operation');
|
||||
}
|
||||
|
||||
if (details != null) {
|
||||
buffer.writeln('详细信息: $details');
|
||||
}
|
||||
|
||||
return buffer.toString().trim();
|
||||
}
|
||||
}
|
||||
|
||||
/// 代码生成异常
|
||||
class CodeGenerationException extends SwaggerException {
|
||||
final String? generatorType;
|
||||
final String? modelName;
|
||||
final String? phase;
|
||||
|
||||
CodeGenerationException(
|
||||
super.message, {
|
||||
super.details,
|
||||
this.generatorType,
|
||||
this.modelName,
|
||||
this.phase,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final buffer = StringBuffer();
|
||||
buffer.writeln('CodeGenerationException: $message');
|
||||
|
||||
if (generatorType != null) {
|
||||
buffer.writeln('生成器类型: $generatorType');
|
||||
}
|
||||
|
||||
if (modelName != null) {
|
||||
buffer.writeln('模型名称: $modelName');
|
||||
}
|
||||
|
||||
if (phase != null) {
|
||||
buffer.writeln('生成阶段: $phase');
|
||||
}
|
||||
|
||||
if (details != null) {
|
||||
buffer.writeln('详细信息: $details');
|
||||
}
|
||||
|
||||
return buffer.toString().trim();
|
||||
}
|
||||
}
|
||||
|
||||
/// 文件操作异常
|
||||
class FileOperationException extends SwaggerException {
|
||||
final String? filePath;
|
||||
final String? operation;
|
||||
final int? errorCode;
|
||||
|
||||
FileOperationException(
|
||||
super.message, {
|
||||
super.details,
|
||||
this.filePath,
|
||||
this.operation,
|
||||
this.errorCode,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final buffer = StringBuffer();
|
||||
buffer.writeln('FileOperationException: $message');
|
||||
|
||||
if (filePath != null) {
|
||||
buffer.writeln('文件路径: $filePath');
|
||||
}
|
||||
|
||||
if (operation != null) {
|
||||
buffer.writeln('操作: $operation');
|
||||
}
|
||||
|
||||
if (errorCode != null) {
|
||||
buffer.writeln('错误代码: $errorCode');
|
||||
}
|
||||
|
||||
if (details != null) {
|
||||
buffer.writeln('详细信息: $details');
|
||||
}
|
||||
|
||||
return buffer.toString().trim();
|
||||
}
|
||||
}
|
||||
|
||||
/// 命令异常
|
||||
class CommandException extends SwaggerException {
|
||||
final String? commandName;
|
||||
final List<String>? arguments;
|
||||
final int? exitCode;
|
||||
|
||||
CommandException(
|
||||
super.message, {
|
||||
super.details,
|
||||
this.commandName,
|
||||
this.arguments,
|
||||
this.exitCode,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final buffer = StringBuffer();
|
||||
buffer.writeln('CommandException: $message');
|
||||
|
||||
if (commandName != null) {
|
||||
buffer.writeln('命令: $commandName');
|
||||
}
|
||||
|
||||
if (arguments != null && arguments!.isNotEmpty) {
|
||||
buffer.writeln('参数: ${arguments!.join(' ')}');
|
||||
}
|
||||
|
||||
if (exitCode != null) {
|
||||
buffer.writeln('退出代码: $exitCode');
|
||||
}
|
||||
|
||||
if (details != null) {
|
||||
buffer.writeln('详细信息: $details');
|
||||
}
|
||||
|
||||
return buffer.toString().trim();
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证异常
|
||||
class ValidationException extends SwaggerException {
|
||||
final String? field;
|
||||
final dynamic value;
|
||||
final String? rule;
|
||||
|
||||
ValidationException(
|
||||
super.message, {
|
||||
super.details,
|
||||
this.field,
|
||||
this.value,
|
||||
this.rule,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final buffer = StringBuffer();
|
||||
buffer.writeln('ValidationException: $message');
|
||||
|
||||
if (field != null) {
|
||||
buffer.writeln('字段: $field');
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
buffer.writeln('值: $value');
|
||||
}
|
||||
|
||||
if (rule != null) {
|
||||
buffer.writeln('验证规则: $rule');
|
||||
}
|
||||
|
||||
if (details != null) {
|
||||
buffer.writeln('详细信息: $details');
|
||||
}
|
||||
|
||||
return buffer.toString().trim();
|
||||
}
|
||||
}
|
||||
|
||||
/// 配置异常
|
||||
class ConfigurationException extends SwaggerException {
|
||||
final String? configKey;
|
||||
final dynamic configValue;
|
||||
final String? source;
|
||||
|
||||
ConfigurationException(
|
||||
super.message, {
|
||||
super.details,
|
||||
this.configKey,
|
||||
this.configValue,
|
||||
this.source,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final buffer = StringBuffer();
|
||||
buffer.writeln('ConfigurationException: $message');
|
||||
|
||||
if (configKey != null) {
|
||||
buffer.writeln('配置键: $configKey');
|
||||
}
|
||||
|
||||
if (configValue != null) {
|
||||
buffer.writeln('配置值: $configValue');
|
||||
}
|
||||
|
||||
if (source != null) {
|
||||
buffer.writeln('来源: $source');
|
||||
}
|
||||
|
||||
if (details != null) {
|
||||
buffer.writeln('详细信息: $details');
|
||||
}
|
||||
|
||||
return buffer.toString().trim();
|
||||
}
|
||||
}
|
||||
|
||||
/// 网络异常
|
||||
class NetworkException extends SwaggerException {
|
||||
final String? url;
|
||||
final int? statusCode;
|
||||
final String? method;
|
||||
final Duration? timeout;
|
||||
|
||||
NetworkException(
|
||||
super.message, {
|
||||
super.details,
|
||||
this.url,
|
||||
this.statusCode,
|
||||
this.method,
|
||||
this.timeout,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final buffer = StringBuffer();
|
||||
buffer.writeln('NetworkException: $message');
|
||||
|
||||
if (url != null) {
|
||||
buffer.writeln('URL: $url');
|
||||
}
|
||||
|
||||
if (method != null) {
|
||||
buffer.writeln('方法: $method');
|
||||
}
|
||||
|
||||
if (statusCode != null) {
|
||||
buffer.writeln('状态码: $statusCode');
|
||||
}
|
||||
|
||||
if (timeout != null) {
|
||||
buffer.writeln('超时: ${timeout!.inSeconds}秒');
|
||||
}
|
||||
|
||||
if (details != null) {
|
||||
buffer.writeln('详细信息: $details');
|
||||
}
|
||||
|
||||
return buffer.toString().trim();
|
||||
}
|
||||
}
|
||||
|
||||
/// 缓存异常
|
||||
class CacheException extends SwaggerException {
|
||||
final String? cacheKey;
|
||||
final String? operation;
|
||||
final String? cacheType;
|
||||
|
||||
CacheException(
|
||||
super.message, {
|
||||
super.details,
|
||||
this.cacheKey,
|
||||
this.operation,
|
||||
this.cacheType,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final buffer = StringBuffer();
|
||||
buffer.writeln('CacheException: $message');
|
||||
|
||||
if (cacheKey != null) {
|
||||
buffer.writeln('缓存键: $cacheKey');
|
||||
}
|
||||
|
||||
if (operation != null) {
|
||||
buffer.writeln('操作: $operation');
|
||||
}
|
||||
|
||||
if (cacheType != null) {
|
||||
buffer.writeln('缓存类型: $cacheType');
|
||||
}
|
||||
|
||||
if (details != null) {
|
||||
buffer.writeln('详细信息: $details');
|
||||
}
|
||||
|
||||
return buffer.toString().trim();
|
||||
}
|
||||
}
|
||||
|
||||
/// 性能异常
|
||||
class PerformanceException extends SwaggerException {
|
||||
final String? operation;
|
||||
final Duration? duration;
|
||||
final Duration? threshold;
|
||||
|
||||
PerformanceException(
|
||||
super.message, {
|
||||
super.details,
|
||||
this.operation,
|
||||
this.duration,
|
||||
this.threshold,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final buffer = StringBuffer();
|
||||
buffer.writeln('PerformanceException: $message');
|
||||
|
||||
if (operation != null) {
|
||||
buffer.writeln('操作: $operation');
|
||||
}
|
||||
|
||||
if (duration != null) {
|
||||
buffer.writeln('耗时: ${duration!.inMilliseconds}ms');
|
||||
}
|
||||
|
||||
if (threshold != null) {
|
||||
buffer.writeln('阈值: ${threshold!.inMilliseconds}ms');
|
||||
}
|
||||
|
||||
if (details != null) {
|
||||
buffer.writeln('详细信息: $details');
|
||||
}
|
||||
|
||||
return buffer.toString().trim();
|
||||
}
|
||||
}
|
||||
|
||||
/// 类型异常
|
||||
class TypeException extends SwaggerException {
|
||||
final String? propertyName;
|
||||
final String? expectedType;
|
||||
final String? actualType;
|
||||
final dynamic value;
|
||||
|
||||
TypeException(
|
||||
super.message, {
|
||||
super.details,
|
||||
this.propertyName,
|
||||
this.expectedType,
|
||||
this.actualType,
|
||||
this.value,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final buffer = StringBuffer();
|
||||
buffer.writeln('TypeException: $message');
|
||||
|
||||
if (propertyName != null) {
|
||||
buffer.writeln('属性名: $propertyName');
|
||||
}
|
||||
|
||||
if (expectedType != null) {
|
||||
buffer.writeln('期望类型: $expectedType');
|
||||
}
|
||||
|
||||
if (actualType != null) {
|
||||
buffer.writeln('实际类型: $actualType');
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
buffer.writeln('值: $value');
|
||||
}
|
||||
|
||||
if (details != null) {
|
||||
buffer.writeln('详细信息: $details');
|
||||
}
|
||||
|
||||
return buffer.toString().trim();
|
||||
}
|
||||
}
|
||||
|
||||
/// 异常处理器
|
||||
class ExceptionHandler {
|
||||
static final Map<Type, void Function(SwaggerException)> _handlers = {};
|
||||
|
||||
/// 注册异常处理器
|
||||
static void register<T extends SwaggerException>(
|
||||
void Function(T exception) handler,
|
||||
) {
|
||||
_handlers[T] = (exception) => handler(exception as T);
|
||||
}
|
||||
|
||||
/// 处理异常
|
||||
static void handle(SwaggerException exception) {
|
||||
final handler = _handlers[exception.runtimeType];
|
||||
if (handler != null) {
|
||||
handler(exception);
|
||||
} else {
|
||||
// 默认处理
|
||||
_defaultHandler(exception);
|
||||
}
|
||||
}
|
||||
|
||||
/// 默认异常处理
|
||||
static void _defaultHandler(SwaggerException exception) {
|
||||
print('🚨 异常: ${exception.toString()}');
|
||||
print('时间: ${exception.timestamp.toIso8601String()}');
|
||||
print('');
|
||||
}
|
||||
|
||||
/// 记录异常到文件
|
||||
static Future<void> logException(
|
||||
SwaggerException exception, {
|
||||
String? logFilePath,
|
||||
}) async {
|
||||
try {
|
||||
final logFile = logFilePath != null
|
||||
? File(logFilePath)
|
||||
: File('swagger_cli_errors.log');
|
||||
|
||||
final logEntry = [
|
||||
'[${'=' * 50}]',
|
||||
'时间: ${exception.timestamp.toIso8601String()}',
|
||||
'类型: ${exception.runtimeType}',
|
||||
'消息: ${exception.message}',
|
||||
if (exception.details != null) '详细信息: ${exception.details}',
|
||||
'堆栈跟踪: ${StackTrace.current}',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
await logFile.writeAsString(logEntry, mode: FileMode.append);
|
||||
} catch (e) {
|
||||
print('记录异常到文件失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 清理异常处理器
|
||||
static void clear() {
|
||||
_handlers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// 异常工厂
|
||||
class ExceptionFactory {
|
||||
/// 创建解析异常
|
||||
static SwaggerParseException createParseException(
|
||||
String message, {
|
||||
String? url,
|
||||
int? statusCode,
|
||||
String? operation,
|
||||
dynamic cause,
|
||||
}) {
|
||||
return SwaggerParseException(
|
||||
message,
|
||||
details: cause?.toString(),
|
||||
url: url,
|
||||
statusCode: statusCode,
|
||||
operation: operation,
|
||||
);
|
||||
}
|
||||
|
||||
/// 创建代码生成异常
|
||||
static CodeGenerationException createCodeGenerationException(
|
||||
String message, {
|
||||
String? generatorType,
|
||||
String? modelName,
|
||||
String? phase,
|
||||
dynamic cause,
|
||||
}) {
|
||||
return CodeGenerationException(
|
||||
message,
|
||||
details: cause?.toString(),
|
||||
generatorType: generatorType,
|
||||
modelName: modelName,
|
||||
phase: phase,
|
||||
);
|
||||
}
|
||||
|
||||
/// 创建文件操作异常
|
||||
static FileOperationException createFileOperationException(
|
||||
String message, {
|
||||
String? filePath,
|
||||
String? operation,
|
||||
int? errorCode,
|
||||
dynamic cause,
|
||||
}) {
|
||||
return FileOperationException(
|
||||
message,
|
||||
details: cause?.toString(),
|
||||
filePath: filePath,
|
||||
operation: operation,
|
||||
errorCode: errorCode,
|
||||
);
|
||||
}
|
||||
|
||||
/// 创建验证异常
|
||||
static ValidationException createValidationException(
|
||||
String message, {
|
||||
String? field,
|
||||
dynamic value,
|
||||
String? rule,
|
||||
dynamic cause,
|
||||
}) {
|
||||
return ValidationException(
|
||||
message,
|
||||
details: cause?.toString(),
|
||||
field: field,
|
||||
value: value,
|
||||
rule: rule,
|
||||
);
|
||||
}
|
||||
|
||||
/// 创建网络异常
|
||||
static NetworkException createNetworkException(
|
||||
String message, {
|
||||
String? url,
|
||||
int? statusCode,
|
||||
String? method,
|
||||
Duration? timeout,
|
||||
dynamic cause,
|
||||
}) {
|
||||
return NetworkException(
|
||||
message,
|
||||
details: cause?.toString(),
|
||||
url: url,
|
||||
statusCode: statusCode,
|
||||
method: method,
|
||||
timeout: timeout,
|
||||
);
|
||||
}
|
||||
|
||||
/// 从标准异常创建
|
||||
static SwaggerException fromStandardException(
|
||||
Exception exception, {
|
||||
String? context,
|
||||
}) {
|
||||
if (exception is FileSystemException) {
|
||||
return FileOperationException(
|
||||
'文件系统错误',
|
||||
details: exception.message,
|
||||
filePath: exception.path,
|
||||
operation: context,
|
||||
);
|
||||
} else if (exception is SocketException) {
|
||||
return NetworkException(
|
||||
'网络连接错误',
|
||||
details: exception.message,
|
||||
url: context,
|
||||
);
|
||||
} else if (exception is FormatException) {
|
||||
return SwaggerParseException(
|
||||
'格式错误',
|
||||
details: exception.message,
|
||||
operation: context,
|
||||
);
|
||||
} else {
|
||||
return GeneralSwaggerException(
|
||||
'未知错误',
|
||||
details: exception.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 通用Swagger异常(当无法确定具体类型时使用)
|
||||
class GeneralSwaggerException extends SwaggerException {
|
||||
GeneralSwaggerException(super.message, {super.details});
|
||||
}
|
||||
|
|
@ -0,0 +1,502 @@
|
|||
/// Swagger数据模型定义
|
||||
/// 提供类型安全的数据结构
|
||||
library;
|
||||
|
||||
/// HTTP方法枚举
|
||||
/// 表示常见的 RESTful API 方法。
|
||||
enum HttpMethod {
|
||||
/// GET 方法
|
||||
get('GET'),
|
||||
|
||||
/// POST 方法
|
||||
post('POST'),
|
||||
|
||||
/// PUT 方法
|
||||
put('PUT'),
|
||||
|
||||
/// DELETE 方法
|
||||
delete('DELETE'),
|
||||
|
||||
/// PATCH 方法
|
||||
patch('PATCH'),
|
||||
|
||||
/// HEAD 方法
|
||||
head('HEAD'),
|
||||
|
||||
/// OPTIONS 方法
|
||||
options('OPTIONS');
|
||||
|
||||
/// 枚举值对应的字符串
|
||||
const HttpMethod(this.value);
|
||||
final String value;
|
||||
|
||||
/// 通过字符串获取 HttpMethod 枚举
|
||||
static HttpMethod fromString(String value) {
|
||||
return HttpMethod.values.firstWhere(
|
||||
(method) => method.value == value.toUpperCase(),
|
||||
orElse: () => HttpMethod.get,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 属性类型枚举
|
||||
/// 用于描述 API 属性的数据类型。
|
||||
enum PropertyType {
|
||||
/// 字符串类型
|
||||
string('string'),
|
||||
|
||||
/// 整数类型
|
||||
integer('integer'),
|
||||
|
||||
/// 浮点数类型
|
||||
number('number'),
|
||||
|
||||
/// 布尔类型
|
||||
boolean('boolean'),
|
||||
|
||||
/// 数组类型
|
||||
array('array'),
|
||||
|
||||
/// 对象类型
|
||||
object('object'),
|
||||
|
||||
/// 文件类型
|
||||
file('file'),
|
||||
|
||||
/// 日期类型
|
||||
date('date'),
|
||||
|
||||
/// 日期时间类型
|
||||
dateTime('date-time'),
|
||||
|
||||
/// 引用类型
|
||||
reference('reference'),
|
||||
|
||||
/// 枚举类型
|
||||
enumType('enum');
|
||||
|
||||
/// 枚举值对应的字符串
|
||||
const PropertyType(this.value);
|
||||
final String value;
|
||||
|
||||
/// 通过字符串获取 PropertyType 枚举
|
||||
static PropertyType fromString(String value) {
|
||||
return PropertyType.values.firstWhere(
|
||||
(type) => type.value == value.toLowerCase(),
|
||||
orElse: () => PropertyType.string,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 参数位置枚举
|
||||
/// 用于描述 API 参数在请求中的位置。
|
||||
enum ParameterLocation {
|
||||
/// 查询参数
|
||||
query('query'),
|
||||
|
||||
/// 路径参数
|
||||
path('path'),
|
||||
|
||||
/// 请求头参数
|
||||
header('header'),
|
||||
|
||||
/// 请求体参数
|
||||
body('body'),
|
||||
|
||||
/// 表单参数
|
||||
form('formData'),
|
||||
|
||||
/// Cookie 参数
|
||||
cookie('cookie');
|
||||
|
||||
/// 枚举值对应的字符串
|
||||
const ParameterLocation(this.value);
|
||||
final String value;
|
||||
|
||||
/// 通过字符串获取 ParameterLocation 枚举
|
||||
static ParameterLocation fromString(String value) {
|
||||
return ParameterLocation.values.firstWhere(
|
||||
(location) => location.value == value.toLowerCase(),
|
||||
orElse: () => ParameterLocation.query,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Swagger文档信息
|
||||
/// 描述整个 API 的元数据和结构。
|
||||
class SwaggerDocument {
|
||||
/// 文档标题
|
||||
final String title;
|
||||
|
||||
/// 版本号
|
||||
final String version;
|
||||
|
||||
/// 文档描述
|
||||
final String description;
|
||||
|
||||
/// 主机名
|
||||
final String host;
|
||||
|
||||
/// 基础路径
|
||||
final String basePath;
|
||||
|
||||
/// 支持的协议
|
||||
final List<String> schemes;
|
||||
|
||||
/// 支持的请求类型
|
||||
final List<String> consumes;
|
||||
|
||||
/// 支持的响应类型
|
||||
final List<String> produces;
|
||||
|
||||
/// 路径定义
|
||||
final Map<String, ApiPath> paths;
|
||||
|
||||
/// 数据模型定义
|
||||
final Map<String, ApiModel> models;
|
||||
|
||||
/// 控制器定义
|
||||
final Map<String, ApiController> controllers;
|
||||
|
||||
/// 构造函数
|
||||
const SwaggerDocument({
|
||||
required this.title,
|
||||
required this.version,
|
||||
required this.description,
|
||||
required this.host,
|
||||
required this.basePath,
|
||||
required this.schemes,
|
||||
required this.consumes,
|
||||
required this.produces,
|
||||
required this.paths,
|
||||
required this.models,
|
||||
required this.controllers,
|
||||
});
|
||||
|
||||
/// 从JSON创建SwaggerDocument
|
||||
factory SwaggerDocument.fromJson(Map<String, dynamic> json) {
|
||||
final info = json['info'] as Map<String, dynamic>? ?? {};
|
||||
return SwaggerDocument(
|
||||
title: info['title'] as String? ?? 'API',
|
||||
version: info['version'] as String? ?? '1.0.0',
|
||||
description: info['description'] as String? ?? '',
|
||||
host: json['host'] as String? ?? '',
|
||||
basePath: json['basePath'] as String? ?? '',
|
||||
schemes: List<String>.from(json['schemes'] ?? ['https']),
|
||||
consumes: List<String>.from(json['consumes'] ?? ['application/json']),
|
||||
produces: List<String>.from(json['produces'] ?? ['application/json']),
|
||||
paths: {},
|
||||
models: {},
|
||||
controllers: {},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// API路径信息
|
||||
class ApiPath {
|
||||
final String path;
|
||||
final HttpMethod method;
|
||||
final String summary;
|
||||
final String description;
|
||||
final String operationId;
|
||||
final List<String> tags;
|
||||
final List<ApiParameter> parameters;
|
||||
final Map<String, ApiResponse> responses;
|
||||
final ApiRequestBody? requestBody;
|
||||
final bool deprecated;
|
||||
|
||||
const ApiPath({
|
||||
required this.path,
|
||||
required this.method,
|
||||
required this.summary,
|
||||
required this.description,
|
||||
required this.operationId,
|
||||
required this.tags,
|
||||
required this.parameters,
|
||||
required this.responses,
|
||||
this.requestBody,
|
||||
this.deprecated = false,
|
||||
});
|
||||
|
||||
/// 从JSON创建ApiPath
|
||||
factory ApiPath.fromJson(
|
||||
String path,
|
||||
String method,
|
||||
Map<String, dynamic> json,
|
||||
) {
|
||||
return ApiPath(
|
||||
path: path,
|
||||
method: HttpMethod.fromString(method),
|
||||
summary: json['summary'] as String? ?? '',
|
||||
description: json['description'] as String? ?? '',
|
||||
operationId: json['operationId'] as String? ?? '',
|
||||
tags: List<String>.from(json['tags'] ?? []),
|
||||
parameters: (json['parameters'] as List?)
|
||||
?.map((p) => ApiParameter.fromJson(p as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
responses: (json['responses'] as Map<String, dynamic>?)?.map(
|
||||
(code, response) => MapEntry(
|
||||
code,
|
||||
ApiResponse.fromJson(code, response as Map<String, dynamic>),
|
||||
),
|
||||
) ??
|
||||
{},
|
||||
requestBody: json['requestBody'] != null
|
||||
? ApiRequestBody.fromJson(json['requestBody'] as Map<String, dynamic>)
|
||||
: null,
|
||||
deprecated: json['deprecated'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// API参数信息
|
||||
class ApiParameter {
|
||||
final String name;
|
||||
final ParameterLocation location;
|
||||
final bool required;
|
||||
final PropertyType type;
|
||||
final String description;
|
||||
final String? format;
|
||||
final dynamic example;
|
||||
final dynamic defaultValue;
|
||||
|
||||
const ApiParameter({
|
||||
required this.name,
|
||||
required this.location,
|
||||
required this.required,
|
||||
required this.type,
|
||||
required this.description,
|
||||
this.format,
|
||||
this.example,
|
||||
this.defaultValue,
|
||||
});
|
||||
|
||||
/// 从JSON创建ApiParameter
|
||||
factory ApiParameter.fromJson(Map<String, dynamic> json) {
|
||||
final schema = json['schema'] as Map<String, dynamic>?;
|
||||
final type =
|
||||
schema?['type'] as String? ?? json['type'] as String? ?? 'string';
|
||||
|
||||
return ApiParameter(
|
||||
name: json['name'] as String? ?? '',
|
||||
location: ParameterLocation.fromString(json['in'] as String? ?? 'query'),
|
||||
required: json['required'] as bool? ?? false,
|
||||
type: PropertyType.fromString(type),
|
||||
description: json['description'] as String? ?? '',
|
||||
format: schema?['format'] as String? ?? json['format'] as String?,
|
||||
example: json['example'],
|
||||
defaultValue: json['default'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// API响应信息
|
||||
class ApiResponse {
|
||||
final String code;
|
||||
final String description;
|
||||
final Map<String, dynamic>? schema;
|
||||
final Map<String, dynamic>? content;
|
||||
|
||||
const ApiResponse({
|
||||
required this.code,
|
||||
required this.description,
|
||||
this.schema,
|
||||
this.content,
|
||||
});
|
||||
|
||||
/// 从JSON创建ApiResponse
|
||||
factory ApiResponse.fromJson(String code, Map<String, dynamic> json) {
|
||||
return ApiResponse(
|
||||
code: code,
|
||||
description: json['description'] as String? ?? '',
|
||||
schema: json['schema'] as Map<String, dynamic>?,
|
||||
content: json['content'] as Map<String, dynamic>?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// API请求体信息
|
||||
class ApiRequestBody {
|
||||
final String description;
|
||||
final bool required;
|
||||
final Map<String, dynamic>? content;
|
||||
|
||||
const ApiRequestBody({
|
||||
required this.description,
|
||||
required this.required,
|
||||
this.content,
|
||||
});
|
||||
|
||||
/// 从JSON创建ApiRequestBody
|
||||
factory ApiRequestBody.fromJson(Map<String, dynamic> json) {
|
||||
return ApiRequestBody(
|
||||
description: json['description'] as String? ?? '',
|
||||
required: json['required'] as bool? ?? false,
|
||||
content: json['content'] as Map<String, dynamic>?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// API模型信息
|
||||
class ApiModel {
|
||||
final String name;
|
||||
final String description;
|
||||
final Map<String, ApiProperty> properties;
|
||||
final List<String> required;
|
||||
final bool isEnum;
|
||||
final List<dynamic> enumValues;
|
||||
final PropertyType? enumType;
|
||||
|
||||
const ApiModel({
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.properties,
|
||||
required this.required,
|
||||
this.isEnum = false,
|
||||
this.enumValues = const [],
|
||||
this.enumType,
|
||||
});
|
||||
|
||||
/// 从JSON创建ApiModel
|
||||
factory ApiModel.fromJson(String name, Map<String, dynamic> json) {
|
||||
final isEnum = json['enum'] != null;
|
||||
final enumValues = isEnum ? List<dynamic>.from(json['enum']) : <dynamic>[];
|
||||
final properties = json['properties'] as Map<String, dynamic>? ?? {};
|
||||
List<String> required;
|
||||
if (json.containsKey('required')) {
|
||||
required = List<String>.from(json['required']);
|
||||
} else {
|
||||
// 没有 required 字段时,凡 nullable != true 的都视为 required
|
||||
required = properties.entries
|
||||
.where((e) => !(e.value['nullable'] as bool? ?? false))
|
||||
.map((e) => e.key)
|
||||
.toList();
|
||||
}
|
||||
|
||||
return ApiModel(
|
||||
name: name,
|
||||
description: json['description'] as String? ?? '',
|
||||
required: required,
|
||||
isEnum: isEnum,
|
||||
enumValues: enumValues,
|
||||
enumType: isEnum
|
||||
? PropertyType.fromString(json['type'] as String? ?? 'string')
|
||||
: null,
|
||||
properties: properties.map(
|
||||
(propName, propData) => MapEntry(
|
||||
propName,
|
||||
ApiProperty.fromJson(
|
||||
propName,
|
||||
propData as Map<String, dynamic>,
|
||||
required,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// API属性信息
|
||||
class ApiProperty {
|
||||
final String name;
|
||||
final PropertyType type;
|
||||
final String? format;
|
||||
final String description;
|
||||
final bool required;
|
||||
final bool nullable;
|
||||
final dynamic example;
|
||||
final dynamic defaultValue;
|
||||
final String? reference;
|
||||
final ApiModel? items; // 用于数组类型
|
||||
|
||||
const ApiProperty({
|
||||
required this.name,
|
||||
required this.type,
|
||||
this.format,
|
||||
required this.description,
|
||||
required this.required,
|
||||
this.nullable = false,
|
||||
this.example,
|
||||
this.defaultValue,
|
||||
this.reference,
|
||||
this.items,
|
||||
});
|
||||
|
||||
/// 从JSON创建ApiProperty
|
||||
factory ApiProperty.fromJson(
|
||||
String name,
|
||||
Map<String, dynamic> json,
|
||||
List<String> requiredFields,
|
||||
) {
|
||||
final type = PropertyType.fromString(json['type'] as String? ?? 'string');
|
||||
String? reference;
|
||||
ApiModel? items;
|
||||
|
||||
// 处理引用类型
|
||||
if (json['\$ref'] != null) {
|
||||
final ref = json['\$ref'] as String;
|
||||
reference = ref.split('/').last;
|
||||
}
|
||||
|
||||
// 处理数组类型的 items
|
||||
if (type == PropertyType.array && json['items'] != null) {
|
||||
final itemsJson = json['items'] as Map<String, dynamic>;
|
||||
|
||||
// 如果 items 是引用类型
|
||||
if (itemsJson['\$ref'] != null) {
|
||||
final itemRef = itemsJson['\$ref'] as String;
|
||||
final itemRefName = itemRef.split('/').last;
|
||||
items = ApiModel(
|
||||
name: itemRefName,
|
||||
description: '',
|
||||
properties: {},
|
||||
required: [],
|
||||
isEnum: false,
|
||||
);
|
||||
} else {
|
||||
// 如果 items 是基本类型
|
||||
final itemType =
|
||||
PropertyType.fromString(itemsJson['type'] as String? ?? 'string');
|
||||
items = ApiModel(
|
||||
name: itemType.value,
|
||||
description: '',
|
||||
properties: {},
|
||||
required: [],
|
||||
isEnum: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return ApiProperty(
|
||||
name: name,
|
||||
type: reference != null ? PropertyType.reference : type,
|
||||
format: json['format'] as String?,
|
||||
description: json['description'] as String? ?? '',
|
||||
required: requiredFields.contains(name),
|
||||
nullable: json['nullable'] as bool? ?? false,
|
||||
example: json['example'],
|
||||
defaultValue: json['default'],
|
||||
reference: reference,
|
||||
items: items,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// API控制器信息
|
||||
class ApiController {
|
||||
final String name;
|
||||
final String description;
|
||||
final List<ApiPath> paths;
|
||||
|
||||
const ApiController({
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.paths,
|
||||
});
|
||||
|
||||
/// 从路径列表创建ApiController
|
||||
factory ApiController.fromPaths(String name, List<ApiPath> paths) {
|
||||
return ApiController(name: name, description: name, paths: paths);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,361 @@
|
|||
import '../core/config.dart';
|
||||
import '../core/exceptions.dart';
|
||||
import '../core/models.dart';
|
||||
import '../utils/string_utils.dart';
|
||||
|
||||
/// 代码生成器基类
|
||||
/// 定义通用的接口和功能
|
||||
abstract class BaseGenerator {
|
||||
/// 生成器类型
|
||||
String get generatorType;
|
||||
|
||||
/// 生成代码
|
||||
String generate();
|
||||
|
||||
/// 生成文件头注释
|
||||
String generateFileHeader(String description) {
|
||||
return StringUtils.generateFileHeader(
|
||||
description,
|
||||
SwaggerConfig.swaggerJsonUrl,
|
||||
);
|
||||
}
|
||||
|
||||
/// 生成类型安全的代码
|
||||
String generateTypeCheckedCode(String code) {
|
||||
// 基础类型检查和验证
|
||||
try {
|
||||
if (code.trim().isEmpty) {
|
||||
throw CodeGenerationException(
|
||||
'生成的代码不能为空',
|
||||
generatorType: generatorType,
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
final SwaggerDocument document;
|
||||
final bool useSimpleModels;
|
||||
|
||||
ModelGenerator(this.document, {this.useSimpleModels = false});
|
||||
|
||||
@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 buffer = StringBuffer();
|
||||
|
||||
// 生成文件头
|
||||
buffer.writeln(generateFileHeader('${model.name} 枚举定义'));
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成枚举类
|
||||
if (model.description.isNotEmpty) {
|
||||
buffer.writeln(StringUtils.generateComment(model.description));
|
||||
}
|
||||
|
||||
buffer.writeln('enum $className {');
|
||||
|
||||
// 生成枚举值
|
||||
for (int i = 0; i < model.enumValues.length; i++) {
|
||||
final value = model.enumValues[i];
|
||||
final enumName = StringUtils.generateEnumValueName(value, i);
|
||||
|
||||
if (enumType == 'integer' || enumType == 'number') {
|
||||
buffer.writeln(' $enumName($value),');
|
||||
} else {
|
||||
buffer.writeln(' $enumName(\'$value\'),');
|
||||
}
|
||||
}
|
||||
|
||||
// 移除最后一个逗号
|
||||
final content = buffer.toString().trimRight();
|
||||
buffer.clear();
|
||||
buffer.writeln(content.substring(0, content.lastIndexOf(',')));
|
||||
buffer.writeln(';');
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成构造函数和方法
|
||||
buffer.writeln(' const $className(this.value);');
|
||||
buffer.writeln(
|
||||
' final ${enumType == 'integer' || enumType == 'number' ? 'int' : 'String'} value;',
|
||||
);
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成 fromValue 方法
|
||||
buffer.writeln(' static $className fromValue(dynamic value) {');
|
||||
buffer.writeln(' for (final enumValue in $className.values) {');
|
||||
buffer.writeln(' if (enumValue.value == value) {');
|
||||
buffer.writeln(' return enumValue;');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln(' throw ArgumentError(\'Unknown enum value: \$value\');');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成 fromJson 方法
|
||||
buffer.writeln(' factory $className.fromJson(dynamic json) {');
|
||||
buffer.writeln(' return fromValue(json);');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成 toJson 方法
|
||||
buffer.writeln(' dynamic toJson() => value;');
|
||||
buffer.writeln('');
|
||||
|
||||
buffer.writeln('}');
|
||||
|
||||
return generateTypeCheckedCode(buffer.toString());
|
||||
}
|
||||
|
||||
// 已移动到 StringUtils.generateEnumValueName
|
||||
|
||||
/// 获取导入的类型列表
|
||||
Set<String> getImportedTypes(ApiModel model) {
|
||||
final importedTypes = <String>{};
|
||||
|
||||
model.properties.forEach((_, property) {
|
||||
if (property.type == PropertyType.reference &&
|
||||
property.reference != null) {
|
||||
importedTypes.add(property.reference!);
|
||||
} else if (property.type == PropertyType.array) {
|
||||
// 处理数组类型的引用
|
||||
if (property.items != null) {
|
||||
final itemType = _getItemType(property.items!);
|
||||
// 如果是引用类型(不是基本类型),则添加到导入列表
|
||||
if (itemType != 'String' &&
|
||||
itemType != 'int' &&
|
||||
itemType != 'double' &&
|
||||
itemType != 'bool' &&
|
||||
itemType != 'dynamic') {
|
||||
importedTypes.add(property.items!.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return importedTypes;
|
||||
}
|
||||
|
||||
/// 生成属性的Dart类型
|
||||
String getDartPropertyType(ApiProperty property) {
|
||||
switch (property.type) {
|
||||
case PropertyType.string:
|
||||
switch (property.format) {
|
||||
case 'date':
|
||||
case 'date-time':
|
||||
return 'DateTime';
|
||||
default:
|
||||
return 'String';
|
||||
}
|
||||
case PropertyType.integer:
|
||||
return 'int';
|
||||
case PropertyType.number:
|
||||
return 'double';
|
||||
case PropertyType.boolean:
|
||||
return 'bool';
|
||||
case PropertyType.array:
|
||||
// 根据数组元素类型推导具体类型
|
||||
if (property.items != null) {
|
||||
final itemType = _getItemType(property.items!);
|
||||
return 'List<$itemType>';
|
||||
}
|
||||
return 'List<dynamic>';
|
||||
case PropertyType.object:
|
||||
return 'Map<String, dynamic>';
|
||||
case PropertyType.reference:
|
||||
return property.reference != null
|
||||
? StringUtils.generateClassName(property.reference!)
|
||||
: 'dynamic';
|
||||
default:
|
||||
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 {
|
||||
final bool generateEndpoints;
|
||||
final bool generateModels;
|
||||
final bool generateDocs;
|
||||
final bool useSimpleModels;
|
||||
final bool separateModelFiles;
|
||||
final String modelsDirectory;
|
||||
final String outputDirectory;
|
||||
final String endpointsFileName;
|
||||
final String docsFileName;
|
||||
|
||||
const GeneratorOptions({
|
||||
this.generateEndpoints = true,
|
||||
this.generateModels = true,
|
||||
this.generateDocs = true,
|
||||
this.useSimpleModels = false,
|
||||
this.separateModelFiles = true,
|
||||
this.modelsDirectory = 'models',
|
||||
this.outputDirectory = 'generator',
|
||||
this.endpointsFileName = 'api_paths.dart',
|
||||
this.docsFileName = 'api_documentation.md',
|
||||
});
|
||||
|
||||
/// 从命令行参数创建选项
|
||||
factory GeneratorOptions.fromArgs(List<String> args) {
|
||||
bool generateEndpoints = false;
|
||||
bool generateModels = false;
|
||||
bool generateDocs = false;
|
||||
bool useSimpleModels = false;
|
||||
bool separateModelFiles = true;
|
||||
String modelsDirectory = 'models';
|
||||
String outputDirectory = 'generator';
|
||||
String endpointsFileName = 'api_paths.dart';
|
||||
String docsFileName = 'api_documentation.md';
|
||||
|
||||
bool hasSpecificOption = false;
|
||||
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
final arg = args[i];
|
||||
|
||||
switch (arg) {
|
||||
case '--endpoints':
|
||||
generateEndpoints = true;
|
||||
hasSpecificOption = true;
|
||||
break;
|
||||
case '--models':
|
||||
generateModels = true;
|
||||
hasSpecificOption = true;
|
||||
break;
|
||||
case '--docs':
|
||||
generateDocs = true;
|
||||
hasSpecificOption = true;
|
||||
break;
|
||||
case '--all':
|
||||
generateEndpoints = true;
|
||||
generateModels = true;
|
||||
generateDocs = true;
|
||||
hasSpecificOption = true;
|
||||
break;
|
||||
case '--simple':
|
||||
useSimpleModels = true;
|
||||
break;
|
||||
case '--models-dir':
|
||||
if (i + 1 < args.length) {
|
||||
modelsDirectory = args[i + 1];
|
||||
i++; // 跳过下一个参数
|
||||
}
|
||||
break;
|
||||
case '--output-dir':
|
||||
if (i + 1 < args.length) {
|
||||
outputDirectory = args[i + 1];
|
||||
i++; // 跳过下一个参数
|
||||
}
|
||||
break;
|
||||
case '--endpoints-file':
|
||||
if (i + 1 < args.length) {
|
||||
endpointsFileName = args[i + 1];
|
||||
i++; // 跳过下一个参数
|
||||
}
|
||||
break;
|
||||
case '--docs-file':
|
||||
if (i + 1 < args.length) {
|
||||
docsFileName = args[i + 1];
|
||||
i++; // 跳过下一个参数
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有指定特定选项,默认生成所有文件
|
||||
if (!hasSpecificOption) {
|
||||
generateEndpoints = true;
|
||||
generateModels = true;
|
||||
generateDocs = true;
|
||||
}
|
||||
|
||||
return GeneratorOptions(
|
||||
generateEndpoints: generateEndpoints,
|
||||
generateModels: generateModels,
|
||||
generateDocs: generateDocs,
|
||||
useSimpleModels: useSimpleModels,
|
||||
separateModelFiles: separateModelFiles,
|
||||
modelsDirectory: modelsDirectory,
|
||||
outputDirectory: outputDirectory,
|
||||
endpointsFileName: endpointsFileName,
|
||||
docsFileName: docsFileName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,702 @@
|
|||
import '../core/models.dart';
|
||||
import '../utils/string_utils.dart';
|
||||
import 'base_generator.dart';
|
||||
|
||||
/// 文档生成器
|
||||
/// 负责生成API文档
|
||||
class DocumentationGenerator extends BaseGenerator {
|
||||
final SwaggerDocument document;
|
||||
final bool includeExamples;
|
||||
final bool includeSchemas;
|
||||
final bool includeResponses;
|
||||
final String? customTitle;
|
||||
|
||||
DocumentationGenerator(
|
||||
this.document, {
|
||||
this.includeExamples = true,
|
||||
this.includeSchemas = true,
|
||||
this.includeResponses = true,
|
||||
this.customTitle,
|
||||
});
|
||||
|
||||
@override
|
||||
String get generatorType => 'DocumentationGenerator';
|
||||
|
||||
@override
|
||||
String generate() {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
// 生成文档头部
|
||||
_generateHeader(buffer);
|
||||
|
||||
// 生成目录
|
||||
_generateTableOfContents(buffer);
|
||||
|
||||
// 生成API概述
|
||||
_generateApiOverview(buffer);
|
||||
|
||||
// 生成认证信息
|
||||
_generateAuthenticationInfo(buffer);
|
||||
|
||||
// 生成API端点文档
|
||||
_generateEndpointsDocumentation(buffer);
|
||||
|
||||
// 生成数据模型文档
|
||||
if (includeSchemas) {
|
||||
_generateSchemasDocumentation(buffer);
|
||||
}
|
||||
|
||||
// 生成错误代码文档
|
||||
_generateErrorCodesDocumentation(buffer);
|
||||
|
||||
// 生成示例代码
|
||||
if (includeExamples) {
|
||||
_generateExamplesDocumentation(buffer);
|
||||
}
|
||||
|
||||
// 生成更新日志
|
||||
_generateChangeLog(buffer);
|
||||
|
||||
return generateTypeCheckedCode(buffer.toString());
|
||||
}
|
||||
|
||||
/// 生成文档头部
|
||||
void _generateHeader(StringBuffer buffer) {
|
||||
final title = customTitle ?? document.title;
|
||||
|
||||
buffer.writeln('# $title');
|
||||
buffer.writeln('');
|
||||
|
||||
if (document.description.isNotEmpty) {
|
||||
buffer.writeln('${document.description}');
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
buffer.writeln('**版本**: ${document.version}');
|
||||
buffer.writeln('**基础URL**: ${_getBaseUrl()}');
|
||||
buffer.writeln('**生成时间**: ${DateTime.now().toIso8601String()}');
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成徽章
|
||||
buffer.writeln(
|
||||
'');
|
||||
buffer.writeln('');
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
/// 生成目录
|
||||
void _generateTableOfContents(StringBuffer buffer) {
|
||||
buffer.writeln('## 📋 目录');
|
||||
buffer.writeln('');
|
||||
buffer.writeln('- [API概述](#api概述)');
|
||||
buffer.writeln('- [认证](#认证)');
|
||||
buffer.writeln('- [API端点](#api端点)');
|
||||
|
||||
// 按控制器分组的端点
|
||||
final controllerGroups = _groupPathsByController();
|
||||
for (final controllerName in controllerGroups.keys) {
|
||||
final anchor = controllerName.toLowerCase().replaceAll(' ', '-');
|
||||
buffer.writeln(' - [$controllerName](#$anchor)');
|
||||
}
|
||||
|
||||
if (includeSchemas) {
|
||||
buffer.writeln('- [数据模型](#数据模型)');
|
||||
}
|
||||
|
||||
buffer.writeln('- [错误代码](#错误代码)');
|
||||
|
||||
if (includeExamples) {
|
||||
buffer.writeln('- [示例代码](#示例代码)');
|
||||
}
|
||||
|
||||
buffer.writeln('- [更新日志](#更新日志)');
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
/// 生成API概述
|
||||
void _generateApiOverview(StringBuffer buffer) {
|
||||
buffer.writeln('## 🚀 API概述');
|
||||
buffer.writeln('');
|
||||
|
||||
// 统计信息
|
||||
final stats = _generateStats();
|
||||
buffer.writeln('### 📊 统计信息');
|
||||
buffer.writeln('');
|
||||
buffer.writeln('- **总端点数**: ${stats['totalEndpoints']}');
|
||||
buffer.writeln('- **控制器数**: ${stats['controllersCount']}');
|
||||
buffer.writeln('- **数据模型数**: ${stats['modelsCount']}');
|
||||
buffer.writeln('');
|
||||
|
||||
// HTTP方法统计
|
||||
final methodStats = stats['methodStats'] as Map<String, int>;
|
||||
buffer.writeln('### 🔗 HTTP方法分布');
|
||||
buffer.writeln('');
|
||||
for (final entry in methodStats.entries) {
|
||||
final method = entry.key;
|
||||
final count = entry.value;
|
||||
final percentage =
|
||||
((count / stats['totalEndpoints']) * 100).toStringAsFixed(1);
|
||||
buffer.writeln('- **$method**: $count个 ($percentage%)');
|
||||
}
|
||||
buffer.writeln('');
|
||||
|
||||
// 支持的格式
|
||||
buffer.writeln('### 📝 支持的格式');
|
||||
buffer.writeln('');
|
||||
buffer.writeln('**请求格式**:');
|
||||
for (final format in document.consumes) {
|
||||
buffer.writeln('- `$format`');
|
||||
}
|
||||
buffer.writeln('');
|
||||
|
||||
buffer.writeln('**响应格式**:');
|
||||
for (final format in document.produces) {
|
||||
buffer.writeln('- `$format`');
|
||||
}
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
/// 生成认证信息
|
||||
void _generateAuthenticationInfo(StringBuffer buffer) {
|
||||
buffer.writeln('## 🔐 认证');
|
||||
buffer.writeln('');
|
||||
buffer.writeln('本API使用以下认证方式:');
|
||||
buffer.writeln('');
|
||||
buffer.writeln('### Bearer Token');
|
||||
buffer.writeln('');
|
||||
buffer.writeln('在请求头中包含Authorization字段:');
|
||||
buffer.writeln('');
|
||||
buffer.writeln('```');
|
||||
buffer.writeln('Authorization: Bearer YOUR_TOKEN_HERE');
|
||||
buffer.writeln('```');
|
||||
buffer.writeln('');
|
||||
buffer.writeln('### 获取Token');
|
||||
buffer.writeln('');
|
||||
buffer.writeln('请使用登录接口获取访问令牌。');
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
/// 生成端点文档
|
||||
void _generateEndpointsDocumentation(StringBuffer buffer) {
|
||||
buffer.writeln('## 📡 API端点');
|
||||
buffer.writeln('');
|
||||
|
||||
final controllerGroups = _groupPathsByController();
|
||||
|
||||
for (final entry in controllerGroups.entries) {
|
||||
final controllerName = entry.key;
|
||||
final paths = entry.value;
|
||||
|
||||
buffer.writeln('### $controllerName');
|
||||
buffer.writeln('');
|
||||
|
||||
// 按HTTP方法和路径排序
|
||||
paths.sort((a, b) {
|
||||
final methodOrder = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
|
||||
final aIndex = methodOrder.indexOf(a.method.value);
|
||||
final bIndex = methodOrder.indexOf(b.method.value);
|
||||
|
||||
if (aIndex != bIndex) {
|
||||
return aIndex.compareTo(bIndex);
|
||||
}
|
||||
|
||||
return a.path.compareTo(b.path);
|
||||
});
|
||||
|
||||
for (final path in paths) {
|
||||
_generateEndpointDocumentation(buffer, path);
|
||||
}
|
||||
|
||||
buffer.writeln('');
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成单个端点文档
|
||||
void _generateEndpointDocumentation(StringBuffer buffer, ApiPath path) {
|
||||
// 端点标题
|
||||
final title = path.summary.isNotEmpty ? path.summary : path.operationId;
|
||||
buffer.writeln('#### ${path.method.value} ${path.path}');
|
||||
buffer.writeln('');
|
||||
|
||||
if (title.isNotEmpty) {
|
||||
buffer.writeln('**$title**');
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
// 描述
|
||||
if (path.description.isNotEmpty) {
|
||||
buffer.writeln(path.description);
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
// 标签
|
||||
if (path.tags.isNotEmpty) {
|
||||
buffer.writeln('**标签**: ${path.tags.join(', ')}');
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
// 参数
|
||||
if (path.parameters.isNotEmpty) {
|
||||
buffer.writeln('**参数**:');
|
||||
buffer.writeln('');
|
||||
|
||||
// 按参数位置分组
|
||||
final paramGroups = <ParameterLocation, List<ApiParameter>>{};
|
||||
for (final param in path.parameters) {
|
||||
paramGroups.putIfAbsent(param.location, () => []).add(param);
|
||||
}
|
||||
|
||||
for (final entry in paramGroups.entries) {
|
||||
final location = entry.key;
|
||||
final params = entry.value;
|
||||
|
||||
buffer.writeln('*${_getLocationName(location)}参数*:');
|
||||
buffer.writeln('');
|
||||
|
||||
buffer.writeln('| 参数名 | 类型 | 必填 | 描述 | 示例 |');
|
||||
buffer.writeln('|--------|------|------|------|------|');
|
||||
|
||||
for (final param in params) {
|
||||
final required = param.required ? '✅' : '❌';
|
||||
final example = param.example?.toString() ?? '-';
|
||||
final description =
|
||||
param.description.isNotEmpty ? param.description : '-';
|
||||
|
||||
buffer.writeln(
|
||||
'| ${param.name} | ${param.type.value} | $required | $description | $example |');
|
||||
}
|
||||
|
||||
buffer.writeln('');
|
||||
}
|
||||
}
|
||||
|
||||
// 响应
|
||||
if (includeResponses && path.responses.isNotEmpty) {
|
||||
buffer.writeln('**响应**:');
|
||||
buffer.writeln('');
|
||||
|
||||
for (final entry in path.responses.entries) {
|
||||
final code = entry.key;
|
||||
final response = entry.value;
|
||||
|
||||
buffer.writeln('*HTTP $code*:');
|
||||
if (response.description.isNotEmpty) {
|
||||
buffer.writeln('- ${response.description}');
|
||||
}
|
||||
buffer.writeln('');
|
||||
}
|
||||
}
|
||||
|
||||
// 示例
|
||||
if (includeExamples) {
|
||||
_generateEndpointExample(buffer, path);
|
||||
}
|
||||
|
||||
buffer.writeln('---');
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
/// 生成端点示例
|
||||
void _generateEndpointExample(StringBuffer buffer, ApiPath path) {
|
||||
buffer.writeln('**示例**:');
|
||||
buffer.writeln('');
|
||||
|
||||
// cURL示例
|
||||
buffer.writeln('```bash');
|
||||
buffer.write('curl -X ${path.method.value} ');
|
||||
buffer.write('${_getBaseUrl()}${path.path}');
|
||||
|
||||
if (path.parameters.any((p) => p.location == ParameterLocation.header)) {
|
||||
buffer.write(' \\');
|
||||
buffer.writeln('');
|
||||
buffer.write(' -H "Authorization: Bearer YOUR_TOKEN"');
|
||||
}
|
||||
|
||||
if (path.method == HttpMethod.post || path.method == HttpMethod.put) {
|
||||
buffer.write(' \\');
|
||||
buffer.writeln('');
|
||||
buffer.write(' -H "Content-Type: application/json"');
|
||||
buffer.write(' \\');
|
||||
buffer.writeln('');
|
||||
buffer.write(' -d \'{"key": "value"}\'');
|
||||
}
|
||||
|
||||
buffer.writeln('');
|
||||
buffer.writeln('```');
|
||||
buffer.writeln('');
|
||||
|
||||
// Dart示例
|
||||
buffer.writeln('```dart');
|
||||
buffer.writeln('import \'dart:convert\';');
|
||||
buffer.writeln('import \'package:http/http.dart\' as http;');
|
||||
buffer.writeln('');
|
||||
buffer.writeln('class ApiClient {');
|
||||
buffer.writeln(' static const String baseUrl = \'${_getBaseUrl()}\';');
|
||||
buffer.writeln(' String? _token;');
|
||||
buffer.writeln('');
|
||||
buffer.writeln(' void setToken(String token) {');
|
||||
buffer.writeln(' _token = token;');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln('');
|
||||
buffer.writeln(' Map<String, String> get _headers => {');
|
||||
buffer.writeln(' \'Content-Type\': \'application/json\',');
|
||||
buffer.writeln(
|
||||
' if (_token != null) \'Authorization\': \'Bearer \$_token\',');
|
||||
buffer.writeln(' };');
|
||||
buffer.writeln('');
|
||||
buffer
|
||||
.writeln(' Future<Map<String, dynamic>> get(String endpoint) async {');
|
||||
buffer.writeln(' final response = await http.get(');
|
||||
buffer.writeln(' Uri.parse(\'\$baseUrl\$endpoint\'),');
|
||||
buffer.writeln(' headers: _headers,');
|
||||
buffer.writeln(' );');
|
||||
buffer.writeln('');
|
||||
buffer.writeln(' if (response.statusCode == 200) {');
|
||||
buffer.writeln(' return jsonDecode(response.body);');
|
||||
buffer.writeln(' } else {');
|
||||
buffer.writeln(
|
||||
' throw Exception(\'Failed to load data: \${response.statusCode}\');');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln('');
|
||||
buffer.writeln(
|
||||
' Future<Map<String, dynamic>> post(String endpoint, Map<String, dynamic> data) async {');
|
||||
buffer.writeln(' final response = await http.post(');
|
||||
buffer.writeln(' Uri.parse(\'\$baseUrl\$endpoint\'),');
|
||||
buffer.writeln(' headers: _headers,');
|
||||
buffer.writeln(' body: jsonEncode(data),');
|
||||
buffer.writeln(' );');
|
||||
buffer.writeln('');
|
||||
buffer.writeln(
|
||||
' if (response.statusCode == 200 || response.statusCode == 201) {');
|
||||
buffer.writeln(' return jsonDecode(response.body);');
|
||||
buffer.writeln(' } else {');
|
||||
buffer.writeln(
|
||||
' throw Exception(\'Failed to post data: \${response.statusCode}\');');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln('}');
|
||||
buffer.writeln('```');
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
/// 生成数据模型文档
|
||||
void _generateSchemasDocumentation(StringBuffer buffer) {
|
||||
if (document.models.isEmpty) return;
|
||||
|
||||
buffer.writeln('## 📋 数据模型');
|
||||
buffer.writeln('');
|
||||
|
||||
final sortedModels = document.models.values.toList()
|
||||
..sort((a, b) => a.name.compareTo(b.name));
|
||||
|
||||
for (final model in sortedModels) {
|
||||
_generateModelDocumentation(buffer, model);
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成模型文档
|
||||
void _generateModelDocumentation(StringBuffer buffer, ApiModel model) {
|
||||
buffer.writeln('### ${model.name}');
|
||||
buffer.writeln('');
|
||||
|
||||
if (model.description.isNotEmpty) {
|
||||
buffer.writeln(model.description);
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
if (model.isEnum) {
|
||||
buffer.writeln('**枚举值**:');
|
||||
buffer.writeln('');
|
||||
|
||||
for (final value in model.enumValues) {
|
||||
buffer.writeln('- `$value`');
|
||||
}
|
||||
|
||||
buffer.writeln('');
|
||||
} else {
|
||||
buffer.writeln('**属性**:');
|
||||
buffer.writeln('');
|
||||
|
||||
if (model.properties.isNotEmpty) {
|
||||
buffer.writeln('| 属性名 | 类型 | 必填 | 描述 |');
|
||||
buffer.writeln('|--------|------|------|------|');
|
||||
|
||||
for (final entry in model.properties.entries) {
|
||||
final propName = entry.key;
|
||||
final prop = entry.value;
|
||||
|
||||
final required = model.required.contains(propName) ? '✅' : '❌';
|
||||
final type = _getPropertyTypeDescription(prop);
|
||||
final description =
|
||||
prop.description.isNotEmpty ? prop.description : '-';
|
||||
|
||||
buffer.writeln('| $propName | $type | $required | $description |');
|
||||
}
|
||||
}
|
||||
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
// JSON示例
|
||||
if (includeExamples) {
|
||||
buffer.writeln('**JSON示例**:');
|
||||
buffer.writeln('');
|
||||
buffer.writeln('```json');
|
||||
buffer.writeln(_generateModelExample(model));
|
||||
buffer.writeln('```');
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
buffer.writeln('---');
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
/// 生成错误代码文档
|
||||
void _generateErrorCodesDocumentation(StringBuffer buffer) {
|
||||
buffer.writeln('## ❌ 错误代码');
|
||||
buffer.writeln('');
|
||||
|
||||
// 标准HTTP状态码
|
||||
final errorCodes = {
|
||||
'400': '请求参数错误',
|
||||
'401': '未授权访问',
|
||||
'403': '禁止访问',
|
||||
'404': '资源不存在',
|
||||
'405': '方法不允许',
|
||||
'422': '参数验证失败',
|
||||
'500': '服务器内部错误',
|
||||
'502': '网关错误',
|
||||
'503': '服务不可用',
|
||||
};
|
||||
|
||||
buffer.writeln('| 状态码 | 描述 |');
|
||||
buffer.writeln('|--------|------|');
|
||||
|
||||
for (final entry in errorCodes.entries) {
|
||||
buffer.writeln('| ${entry.key} | ${entry.value} |');
|
||||
}
|
||||
|
||||
buffer.writeln('');
|
||||
|
||||
// 错误响应格式
|
||||
buffer.writeln('### 错误响应格式');
|
||||
buffer.writeln('');
|
||||
buffer.writeln('```json');
|
||||
buffer.writeln('{');
|
||||
buffer.writeln(' "error": {');
|
||||
buffer.writeln(' "code": "ERROR_CODE",');
|
||||
buffer.writeln(' "message": "错误描述",');
|
||||
buffer.writeln(' "details": "详细信息"');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln('}');
|
||||
buffer.writeln('```');
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
/// 生成示例代码
|
||||
void _generateExamplesDocumentation(StringBuffer buffer) {
|
||||
buffer.writeln('## 💡 示例代码');
|
||||
buffer.writeln('');
|
||||
|
||||
// Dart HTTP客户端示例
|
||||
buffer.writeln('### Dart HTTP客户端');
|
||||
buffer.writeln('');
|
||||
buffer.writeln('```dart');
|
||||
buffer.writeln('import \'dart:convert\';');
|
||||
buffer.writeln('import \'package:http/http.dart\' as http;');
|
||||
buffer.writeln('');
|
||||
buffer.writeln('class ApiClient {');
|
||||
buffer.writeln(' static const String baseUrl = \'${_getBaseUrl()}\';');
|
||||
buffer.writeln(' String? _token;');
|
||||
buffer.writeln('');
|
||||
buffer.writeln(' void setToken(String token) {');
|
||||
buffer.writeln(' _token = token;');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln('');
|
||||
buffer.writeln(' Map<String, String> get _headers => {');
|
||||
buffer.writeln(' \'Content-Type\': \'application/json\',');
|
||||
buffer.writeln(
|
||||
' if (_token != null) \'Authorization\': \'Bearer \$_token\',');
|
||||
buffer.writeln(' };');
|
||||
buffer.writeln('');
|
||||
buffer
|
||||
.writeln(' Future<Map<String, dynamic>> get(String endpoint) async {');
|
||||
buffer.writeln(' final response = await http.get(');
|
||||
buffer.writeln(' Uri.parse(\'\$baseUrl\$endpoint\'),');
|
||||
buffer.writeln(' headers: _headers,');
|
||||
buffer.writeln(' );');
|
||||
buffer.writeln('');
|
||||
buffer.writeln(' if (response.statusCode == 200) {');
|
||||
buffer.writeln(' return jsonDecode(response.body);');
|
||||
buffer.writeln(' } else {');
|
||||
buffer.writeln(
|
||||
' throw Exception(\'Failed to load data: \${response.statusCode}\');');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln('');
|
||||
buffer.writeln(
|
||||
' Future<Map<String, dynamic>> post(String endpoint, Map<String, dynamic> data) async {');
|
||||
buffer.writeln(' final response = await http.post(');
|
||||
buffer.writeln(' Uri.parse(\'\$baseUrl\$endpoint\'),');
|
||||
buffer.writeln(' headers: _headers,');
|
||||
buffer.writeln(' body: jsonEncode(data),');
|
||||
buffer.writeln(' );');
|
||||
buffer.writeln('');
|
||||
buffer.writeln(
|
||||
' if (response.statusCode == 200 || response.statusCode == 201) {');
|
||||
buffer.writeln(' return jsonDecode(response.body);');
|
||||
buffer.writeln(' } else {');
|
||||
buffer.writeln(
|
||||
' throw Exception(\'Failed to post data: \${response.statusCode}\');');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln('}');
|
||||
buffer.writeln('```');
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
/// 生成更新日志
|
||||
void _generateChangeLog(StringBuffer buffer) {
|
||||
buffer.writeln('## 📝 更新日志');
|
||||
buffer.writeln('');
|
||||
|
||||
buffer.writeln(
|
||||
'### ${document.version} - ${DateTime.now().toIso8601String().split('T')[0]}');
|
||||
buffer.writeln('');
|
||||
buffer.writeln('- 🎉 初始版本发布');
|
||||
buffer.writeln('- 📡 ${document.paths.length} 个API端点');
|
||||
buffer.writeln('- 📋 ${document.models.length} 个数据模型');
|
||||
buffer.writeln('- 🔧 完整的API文档');
|
||||
buffer.writeln('');
|
||||
|
||||
buffer.writeln('---');
|
||||
buffer.writeln('');
|
||||
buffer.writeln('*文档由 Swagger CLI By Max 自动生成*');
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
/// 按控制器分组路径
|
||||
Map<String, List<ApiPath>> _groupPathsByController() {
|
||||
final groups = <String, List<ApiPath>>{};
|
||||
|
||||
for (final path in document.paths.values) {
|
||||
final controllerName = StringUtils.extractControllerName(path);
|
||||
groups.putIfAbsent(controllerName, () => []).add(path);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
// 已移动到 StringUtils.extractControllerName
|
||||
|
||||
/// 获取基础URL
|
||||
String _getBaseUrl() {
|
||||
return document.schemes.isNotEmpty
|
||||
? '${document.schemes.first}://${document.host}${document.basePath}'
|
||||
: 'https://${document.host}${document.basePath}';
|
||||
}
|
||||
|
||||
/// 获取参数位置名称
|
||||
String _getLocationName(ParameterLocation location) {
|
||||
switch (location) {
|
||||
case ParameterLocation.query:
|
||||
return '查询';
|
||||
case ParameterLocation.path:
|
||||
return '路径';
|
||||
case ParameterLocation.header:
|
||||
return '请求头';
|
||||
case ParameterLocation.body:
|
||||
return '请求体';
|
||||
case ParameterLocation.form:
|
||||
return '表单';
|
||||
case ParameterLocation.cookie:
|
||||
return 'Cookie';
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取属性类型描述
|
||||
String _getPropertyTypeDescription(ApiProperty prop) {
|
||||
String baseType = prop.type.value;
|
||||
|
||||
if (prop.format != null) {
|
||||
baseType += ' (${prop.format})';
|
||||
}
|
||||
|
||||
if (prop.nullable) {
|
||||
baseType += '?';
|
||||
}
|
||||
|
||||
return baseType;
|
||||
}
|
||||
|
||||
/// 生成模型示例
|
||||
String _generateModelExample(ApiModel model) {
|
||||
if (model.isEnum) {
|
||||
return '"${model.enumValues.first}"';
|
||||
}
|
||||
|
||||
final buffer = StringBuffer();
|
||||
buffer.writeln('{');
|
||||
|
||||
final properties = model.properties.entries.toList();
|
||||
for (int i = 0; i < properties.length; i++) {
|
||||
final entry = properties[i];
|
||||
final propName = entry.key;
|
||||
final prop = entry.value;
|
||||
|
||||
final exampleValue = _generatePropertyExample(prop);
|
||||
buffer.write(' "$propName": $exampleValue');
|
||||
|
||||
if (i < properties.length - 1) {
|
||||
buffer.write(',');
|
||||
}
|
||||
|
||||
buffer.writeln();
|
||||
}
|
||||
|
||||
buffer.write('}');
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// 生成属性示例
|
||||
String _generatePropertyExample(ApiProperty prop) {
|
||||
switch (prop.type) {
|
||||
case PropertyType.string:
|
||||
return '"string"';
|
||||
case PropertyType.integer:
|
||||
return '0';
|
||||
case PropertyType.number:
|
||||
return '0.0';
|
||||
case PropertyType.boolean:
|
||||
return 'true';
|
||||
case PropertyType.array:
|
||||
return '[]';
|
||||
case PropertyType.object:
|
||||
return '{}';
|
||||
case PropertyType.reference:
|
||||
return '{}';
|
||||
default:
|
||||
return 'null';
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成统计信息
|
||||
Map<String, dynamic> _generateStats() {
|
||||
final stats = <String, dynamic>{};
|
||||
|
||||
stats['totalEndpoints'] = document.paths.length;
|
||||
stats['controllersCount'] = _groupPathsByController().length;
|
||||
stats['modelsCount'] = document.models.length;
|
||||
|
||||
// HTTP方法统计
|
||||
final methodStats = <String, int>{};
|
||||
for (final path in document.paths.values) {
|
||||
final method = path.method.value;
|
||||
methodStats[method] = (methodStats[method] ?? 0) + 1;
|
||||
}
|
||||
stats['methodStats'] = methodStats;
|
||||
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
import '../core/models.dart';
|
||||
import '../utils/string_utils.dart';
|
||||
import 'base_generator.dart';
|
||||
|
||||
/// 端点代码生成器
|
||||
/// 负责生成API端点常量代码
|
||||
class EndpointCodeGenerator extends BaseGenerator {
|
||||
final SwaggerDocument document;
|
||||
final bool includeBaseUrl;
|
||||
final String? customBaseUrl;
|
||||
|
||||
EndpointCodeGenerator(
|
||||
this.document, {
|
||||
this.includeBaseUrl = true,
|
||||
this.customBaseUrl,
|
||||
});
|
||||
|
||||
@override
|
||||
String get generatorType => 'EndpointCodeGenerator';
|
||||
|
||||
@override
|
||||
String generate() {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
// 生成文件头
|
||||
buffer.writeln(generateFileHeader('API 端点常量定义'));
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成端点类
|
||||
buffer.writeln('/// API路径常量定义');
|
||||
buffer.writeln('/// 统一管理所有API端点路径,便于维护和修改');
|
||||
buffer.writeln('class ApiPaths {');
|
||||
buffer.writeln(' ApiPaths._(); // 私有构造函数,防止实例化');
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成基础URL常量
|
||||
if (includeBaseUrl) {
|
||||
final baseUrl = customBaseUrl ??
|
||||
(document.schemes.isNotEmpty
|
||||
? '${document.schemes.first}://${document.host}${document.basePath}'
|
||||
: 'https://${document.host}${document.basePath}');
|
||||
|
||||
buffer.writeln(' /// 基础URL');
|
||||
buffer.writeln(' static const String baseUrl = \'$baseUrl\';');
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
// 按控制器分组生成端点
|
||||
final controllerGroups = _groupPathsByController();
|
||||
|
||||
for (final entry in controllerGroups.entries) {
|
||||
final controllerName = entry.key;
|
||||
final paths = entry.value;
|
||||
|
||||
buffer.writeln(' // ${controllerName}相关端点');
|
||||
|
||||
for (final path in paths) {
|
||||
final constantName = _generateConstantName(path);
|
||||
final cleanPath = StringUtils.cleanPath(path.path);
|
||||
|
||||
// 生成注释
|
||||
if (path.summary.isNotEmpty) {
|
||||
buffer.writeln(' /// ${path.summary}');
|
||||
}
|
||||
if (path.description.isNotEmpty && path.description != path.summary) {
|
||||
buffer.writeln(' /// ${path.description}');
|
||||
}
|
||||
|
||||
buffer.writeln(' static const String $constantName = \'$cleanPath\';');
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
// 生成所有端点的列表
|
||||
buffer.writeln(' /// 所有端点列表');
|
||||
buffer.writeln(' static const List<String> allEndpoints = [');
|
||||
|
||||
for (final entry in controllerGroups.entries) {
|
||||
final paths = entry.value;
|
||||
for (final path in paths) {
|
||||
final constantName = _generateConstantName(path);
|
||||
buffer.writeln(' $constantName,');
|
||||
}
|
||||
}
|
||||
|
||||
buffer.writeln(' ];');
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成HTTP方法常量
|
||||
buffer.writeln(' /// HTTP方法常量');
|
||||
buffer.writeln(' static const Map<String, String> httpMethods = {');
|
||||
|
||||
for (final entry in controllerGroups.entries) {
|
||||
final paths = entry.value;
|
||||
for (final path in paths) {
|
||||
final constantName = _generateConstantName(path);
|
||||
buffer.writeln(' \'$constantName\': \'${path.method.value}\',');
|
||||
}
|
||||
}
|
||||
|
||||
buffer.writeln(' };');
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成完整URL构建方法
|
||||
if (includeBaseUrl) {
|
||||
buffer.writeln(' /// 构建完整URL');
|
||||
buffer.writeln(
|
||||
' static String buildUrl(String endpoint, {Map<String, dynamic>? params}) {');
|
||||
buffer.writeln(' String url = baseUrl + endpoint;');
|
||||
buffer.writeln(' ');
|
||||
buffer.writeln(' if (params != null && params.isNotEmpty) {');
|
||||
buffer.writeln(' final queryParams = <String>[];');
|
||||
buffer.writeln(' params.forEach((key, value) {');
|
||||
buffer.writeln(' if (value != null) {');
|
||||
buffer.writeln(
|
||||
' queryParams.add(\'\\\${Uri.encodeComponent(key)}=\\\${Uri.encodeComponent(value.toString())}\');');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln(' });');
|
||||
buffer.writeln(' ');
|
||||
buffer.writeln(' if (queryParams.isNotEmpty) {');
|
||||
buffer.writeln(' url += \'?\' + queryParams.join(\'&\');');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln(' ');
|
||||
buffer.writeln(' return url;');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成路径参数替换方法
|
||||
buffer.writeln(' /// 替换路径参数');
|
||||
buffer.writeln(
|
||||
' static String replacePathParams(String endpoint, Map<String, dynamic> params) {');
|
||||
buffer.writeln(' String result = endpoint;');
|
||||
buffer.writeln(' params.forEach((key, value) {');
|
||||
buffer.writeln(
|
||||
' result = result.replaceAll(\'{\\\$key}\', value.toString());');
|
||||
buffer.writeln(' });');
|
||||
buffer.writeln(' return result;');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
// 生成端点验证方法
|
||||
buffer.writeln(' /// 验证端点是否存在');
|
||||
buffer.writeln(' static bool isValidEndpoint(String endpoint) {');
|
||||
buffer.writeln(' return allEndpoints.contains(endpoint);');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成获取HTTP方法的方法
|
||||
buffer.writeln(' /// 获取端点的HTTP方法');
|
||||
buffer.writeln(' static String? getHttpMethod(String endpoint) {');
|
||||
buffer.writeln(' return httpMethods[endpoint];');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln('');
|
||||
|
||||
buffer.writeln('}');
|
||||
|
||||
// 生成枚举类型的端点定义(可选)
|
||||
buffer.writeln('');
|
||||
buffer.writeln('/// API端点枚举');
|
||||
buffer.writeln('/// 提供类型安全的端点访问');
|
||||
buffer.writeln('enum ApiEndpoint {');
|
||||
|
||||
for (final entry in controllerGroups.entries) {
|
||||
final paths = entry.value;
|
||||
for (final path in paths) {
|
||||
final enumName = _generateEnumName(path);
|
||||
final constantName = _generateConstantName(path);
|
||||
|
||||
if (path.summary.isNotEmpty) {
|
||||
buffer.writeln(' /// ${path.summary}');
|
||||
}
|
||||
buffer.writeln(
|
||||
' $enumName(ApiPaths.$constantName, \'${path.method.value}\'),');
|
||||
}
|
||||
}
|
||||
|
||||
buffer.writeln(';');
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成枚举的构造函数和方法
|
||||
buffer.writeln(' const ApiEndpoint(this.path, this.method);');
|
||||
buffer.writeln('');
|
||||
buffer.writeln(' /// 端点路径');
|
||||
buffer.writeln(' final String path;');
|
||||
buffer.writeln('');
|
||||
buffer.writeln(' /// HTTP方法');
|
||||
buffer.writeln(' final String method;');
|
||||
buffer.writeln('');
|
||||
|
||||
if (includeBaseUrl) {
|
||||
buffer.writeln(' /// 获取完整URL');
|
||||
buffer.writeln(' String get fullUrl => ApiPaths.baseUrl + path;');
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
buffer.writeln(' /// 根据路径查找端点');
|
||||
buffer.writeln(' static ApiEndpoint? findByPath(String path) {');
|
||||
buffer.writeln(' for (final endpoint in values) {');
|
||||
buffer.writeln(' if (endpoint.path == path) {');
|
||||
buffer.writeln(' return endpoint;');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln(' return null;');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln('');
|
||||
|
||||
buffer.writeln(' /// 根据HTTP方法过滤端点');
|
||||
buffer
|
||||
.writeln(' static List<ApiEndpoint> filterByMethod(String method) {');
|
||||
buffer.writeln(
|
||||
' return values.where((endpoint) => endpoint.method == method).toList();');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln('');
|
||||
|
||||
buffer.writeln('}');
|
||||
|
||||
return generateTypeCheckedCode(buffer.toString());
|
||||
}
|
||||
|
||||
/// 按控制器分组路径
|
||||
Map<String, List<ApiPath>> _groupPathsByController() {
|
||||
final groups = <String, List<ApiPath>>{};
|
||||
|
||||
for (final path in document.paths.values) {
|
||||
final controllerName = StringUtils.extractControllerName(path);
|
||||
groups.putIfAbsent(controllerName, () => []).add(path);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
// 已移动到 StringUtils.extractControllerName
|
||||
|
||||
/// 生成常量名称
|
||||
String _generateConstantName(ApiPath path) {
|
||||
final baseName =
|
||||
StringUtils.generateEndpointName(path.path, path.operationId);
|
||||
final methodPrefix = path.method.value.toLowerCase();
|
||||
|
||||
return StringUtils.toCamelCase('${methodPrefix}_$baseName');
|
||||
}
|
||||
|
||||
/// 生成枚举名称
|
||||
String _generateEnumName(ApiPath path) {
|
||||
final baseName =
|
||||
StringUtils.generateEndpointName(path.path, path.operationId);
|
||||
final methodPrefix = path.method.value.toLowerCase();
|
||||
|
||||
return StringUtils.toCamelCase('${methodPrefix}_$baseName');
|
||||
}
|
||||
|
||||
// 已移动到 StringUtils.cleanPath
|
||||
}
|
||||
|
|
@ -0,0 +1,682 @@
|
|||
import '../core/models.dart';
|
||||
import '../utils/string_utils.dart';
|
||||
import 'base_generator.dart';
|
||||
|
||||
/// 模型代码生成器
|
||||
/// 负责生成Dart模型类代码
|
||||
class ModelCodeGenerator extends ModelGenerator {
|
||||
ModelCodeGenerator(super.document, {super.useSimpleModels});
|
||||
|
||||
@override
|
||||
String get generatorType => 'ModelCodeGenerator';
|
||||
|
||||
@override
|
||||
String generate() {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
// 生成文件头
|
||||
buffer.writeln(generateFileHeader('API 数据模型定义'));
|
||||
buffer.writeln('');
|
||||
|
||||
if (!useSimpleModels) {
|
||||
buffer.writeln(
|
||||
'import \'package:json_annotation/json_annotation.dart\';',
|
||||
);
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
// 生成所有模型
|
||||
final models = document.models.values.toList();
|
||||
for (int i = 0; i < models.length; i++) {
|
||||
final model = models[i];
|
||||
buffer.writeln(generateModelCode(model));
|
||||
|
||||
// 添加模型间的分隔符
|
||||
if (i < models.length - 1) {
|
||||
buffer.writeln('');
|
||||
buffer.writeln('// ${'=' * 60}');
|
||||
buffer.writeln('');
|
||||
}
|
||||
}
|
||||
|
||||
return generateTypeCheckedCode(buffer.toString());
|
||||
}
|
||||
|
||||
@override
|
||||
String generateModelCode(ApiModel model) {
|
||||
if (model.isEnum) {
|
||||
return generateEnumCode(model);
|
||||
}
|
||||
|
||||
return useSimpleModels
|
||||
? generateSimpleModelCode(model)
|
||||
: generateAnnotatedModelCode(model);
|
||||
}
|
||||
|
||||
/// 生成简洁版模型代码
|
||||
String generateSimpleModelCode(ApiModel model) {
|
||||
final className = StringUtils.generateClassName(model.name);
|
||||
final buffer = StringBuffer();
|
||||
|
||||
// 生成导入依赖
|
||||
final importedTypes = getImportedTypes(model);
|
||||
for (final importType in importedTypes) {
|
||||
final importFileName = StringUtils.generateFileName(importType);
|
||||
buffer.writeln('import \'$importFileName\';');
|
||||
}
|
||||
|
||||
if (importedTypes.isNotEmpty) {
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
// 生成类注释
|
||||
if (model.description.isNotEmpty) {
|
||||
buffer.writeln(StringUtils.generateComment(model.description));
|
||||
}
|
||||
|
||||
buffer.writeln('class $className {');
|
||||
|
||||
// 生成属性
|
||||
model.properties.forEach((propName, property) {
|
||||
final dartType = getDartPropertyType(property);
|
||||
final nullable = property.nullable ? '?' : '';
|
||||
final dartPropName = StringUtils.toDartPropertyName(propName);
|
||||
|
||||
if (property.description.isNotEmpty) {
|
||||
buffer.writeln(
|
||||
' ${StringUtils.generateComment(property.description)}',
|
||||
);
|
||||
}
|
||||
|
||||
buffer.writeln(' final $dartType$nullable $dartPropName;');
|
||||
buffer.writeln('');
|
||||
});
|
||||
|
||||
// 生成构造函数
|
||||
if (model.properties.isEmpty) {
|
||||
buffer.writeln(' const $className();');
|
||||
} else {
|
||||
buffer.writeln(' const $className({');
|
||||
model.properties.forEach((propName, property) {
|
||||
final dartPropName = StringUtils.toDartPropertyName(propName);
|
||||
final required = property.required ? 'required ' : '';
|
||||
buffer.writeln(' ${required}this.$dartPropName,');
|
||||
});
|
||||
buffer.writeln(' });');
|
||||
}
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成 fromJson 方法
|
||||
buffer.writeln(
|
||||
' factory $className.fromJson(Map<String, dynamic> json) {',
|
||||
);
|
||||
if (model.properties.isEmpty) {
|
||||
buffer.writeln(' return const $className();');
|
||||
} else {
|
||||
buffer.writeln(' return $className(');
|
||||
model.properties.forEach((propName, property) {
|
||||
final dartPropName = StringUtils.toDartPropertyName(propName);
|
||||
final dartType = getDartPropertyType(property);
|
||||
|
||||
buffer.write(' $dartPropName: ');
|
||||
|
||||
// 生成类型转换逻辑
|
||||
if (property.type == PropertyType.reference &&
|
||||
property.reference != null) {
|
||||
final refType = StringUtils.generateClassName(property.reference!);
|
||||
if (property.nullable) {
|
||||
buffer.write(
|
||||
'json[\'$propName\'] != null ? $refType.fromJson(json[\'$propName\']) : null',
|
||||
);
|
||||
} else {
|
||||
buffer.write('$refType.fromJson(json[\'$propName\'])');
|
||||
}
|
||||
} else if (property.type == PropertyType.array) {
|
||||
// 简化的数组处理
|
||||
buffer.write(
|
||||
'json[\'$propName\'] != null ? List<dynamic>.from(json[\'$propName\']) : null',
|
||||
);
|
||||
} else {
|
||||
// 基本类型
|
||||
if (property.nullable) {
|
||||
buffer.write('json[\'$propName\'] as $dartType?');
|
||||
} else {
|
||||
buffer.write('json[\'$propName\'] as $dartType');
|
||||
}
|
||||
}
|
||||
|
||||
buffer.writeln(',');
|
||||
});
|
||||
buffer.writeln(' );');
|
||||
}
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成 toJson 方法
|
||||
buffer.writeln(' Map<String, dynamic> toJson() {');
|
||||
buffer.writeln(' return {');
|
||||
model.properties.forEach((propName, property) {
|
||||
final dartPropName = StringUtils.toDartPropertyName(propName);
|
||||
|
||||
if (property.type == PropertyType.reference &&
|
||||
property.reference != null) {
|
||||
buffer.write(' \'$propName\': $dartPropName?.toJson()');
|
||||
} else if (property.type == PropertyType.array) {
|
||||
buffer.write(' \'$propName\': $dartPropName');
|
||||
} else {
|
||||
buffer.write(' \'$propName\': $dartPropName');
|
||||
}
|
||||
|
||||
buffer.writeln(',');
|
||||
});
|
||||
buffer.writeln(' };');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成 copyWith 方法
|
||||
if (model.properties.isNotEmpty) {
|
||||
buffer.writeln(' $className copyWith({');
|
||||
model.properties.forEach((propName, property) {
|
||||
final dartType = getDartPropertyType(property);
|
||||
final dartPropName = StringUtils.toDartPropertyName(propName);
|
||||
buffer.writeln(' $dartType? $dartPropName,');
|
||||
});
|
||||
buffer.writeln(' }) {');
|
||||
buffer.writeln(' return $className(');
|
||||
model.properties.forEach((propName, property) {
|
||||
final dartPropName = StringUtils.toDartPropertyName(propName);
|
||||
buffer.writeln(
|
||||
' $dartPropName: $dartPropName ?? this.$dartPropName,',
|
||||
);
|
||||
});
|
||||
buffer.writeln(' );');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
buffer.writeln('}');
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// 生成带注解的模型代码
|
||||
String generateAnnotatedModelCode(ApiModel model) {
|
||||
final className = StringUtils.generateClassName(model.name);
|
||||
final buffer = StringBuffer();
|
||||
|
||||
// 生成导入依赖
|
||||
final importedTypes = getImportedTypes(model);
|
||||
for (final importType in importedTypes) {
|
||||
final importFileName = StringUtils.generateFileName(importType);
|
||||
buffer.writeln('import \'$importFileName\';');
|
||||
}
|
||||
|
||||
if (importedTypes.isNotEmpty) {
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
// 生成 part 声明
|
||||
final partFileName = StringUtils.generateFileName(model.name);
|
||||
final generatedPart = partFileName.replaceAll('.dart', '.g.dart');
|
||||
buffer.writeln('part \'$generatedPart\';');
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成类注释
|
||||
if (model.description.isNotEmpty) {
|
||||
buffer.writeln(StringUtils.generateComment(model.description));
|
||||
}
|
||||
|
||||
buffer.writeln('@JsonSerializable()');
|
||||
buffer.writeln('class $className {');
|
||||
|
||||
// 生成属性
|
||||
model.properties.forEach((propName, property) {
|
||||
final dartType = getDartPropertyType(property);
|
||||
final nullable = property.nullable ? '?' : '';
|
||||
final dartPropName = StringUtils.toDartPropertyName(propName);
|
||||
|
||||
if (property.description.isNotEmpty) {
|
||||
buffer.writeln(
|
||||
' ${StringUtils.generateComment(property.description)}',
|
||||
);
|
||||
}
|
||||
|
||||
// 添加JsonKey注解
|
||||
final needsJsonKey =
|
||||
_needsJsonKeyAnnotation(dartPropName, propName, property);
|
||||
if (needsJsonKey.isNotEmpty) {
|
||||
buffer.writeln(' @JsonKey($needsJsonKey)');
|
||||
}
|
||||
|
||||
buffer.writeln(' final $dartType$nullable $dartPropName;');
|
||||
buffer.writeln('');
|
||||
});
|
||||
|
||||
// 生成构造函数
|
||||
if (model.properties.isEmpty) {
|
||||
buffer.writeln(' const $className();');
|
||||
} else {
|
||||
buffer.writeln(' const $className({');
|
||||
model.properties.forEach((propName, property) {
|
||||
final dartPropName = StringUtils.toDartPropertyName(propName);
|
||||
final required = property.required ? 'required ' : '';
|
||||
buffer.writeln(' ${required}this.$dartPropName,');
|
||||
});
|
||||
buffer.writeln(' });');
|
||||
}
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成 fromJson 工厂方法
|
||||
buffer.writeln(
|
||||
' factory $className.fromJson(Map<String, dynamic> json) => _\$${className}FromJson(json);',
|
||||
);
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成 toJson 方法
|
||||
buffer.writeln(
|
||||
' Map<String, dynamic> toJson() => _\$${className}ToJson(this);',
|
||||
);
|
||||
buffer.writeln('');
|
||||
|
||||
buffer.writeln('}');
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// 生成单独的模型文件
|
||||
Map<String, String> generateSeparateModelFiles() {
|
||||
final files = <String, String>{};
|
||||
|
||||
// 生成所有模型文件
|
||||
for (final model in document.models.values) {
|
||||
final fileName = StringUtils.generateFileName(model.name);
|
||||
final content = generateSingleModelFile(model);
|
||||
files[fileName] = content;
|
||||
}
|
||||
|
||||
// 生成 index.dart 文件
|
||||
final modelFileNames = document.models.keys
|
||||
.map((name) => StringUtils.generateFileName(name))
|
||||
.toList();
|
||||
final indexContent = generateIndexFile(modelFileNames);
|
||||
files['index.dart'] = indexContent;
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/// 生成单个模型文件
|
||||
String generateSingleModelFile(ApiModel model) {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
// 生成文件头
|
||||
buffer.writeln(generateFileHeader('${model.name} 模型定义'));
|
||||
buffer.writeln('');
|
||||
|
||||
// 枚举类需要导入 json_annotation 以使用 @JsonEnum 注解
|
||||
if (!useSimpleModels && model.isEnum) {
|
||||
buffer.writeln(
|
||||
'import \'package:json_annotation/json_annotation.dart\';',
|
||||
);
|
||||
buffer.writeln('');
|
||||
}
|
||||
// 普通类且非简洁模式时导入 json_annotation
|
||||
else if (!useSimpleModels && !model.isEnum) {
|
||||
buffer.writeln(
|
||||
'import \'package:json_annotation/json_annotation.dart\';',
|
||||
);
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
// 生成导入依赖 - 统一使用 index.dart
|
||||
final importedTypes = getImportedTypes(model);
|
||||
if (importedTypes.isNotEmpty) {
|
||||
buffer.writeln('import \'index.dart\';');
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
// 生成模型代码,但不包含导入语句和文件头(因为已经在上面生成了)
|
||||
buffer.writeln(_generateModelCodeWithoutImports(model));
|
||||
|
||||
return generateTypeCheckedCode(buffer.toString());
|
||||
}
|
||||
|
||||
/// 生成模型代码(不包含导入语句)
|
||||
String _generateModelCodeWithoutImports(ApiModel model) {
|
||||
if (model.isEnum) {
|
||||
return _generateEnumCodeWithoutImports(model);
|
||||
}
|
||||
|
||||
return useSimpleModels
|
||||
? _generateSimpleModelCodeWithoutImports(model)
|
||||
: _generateAnnotatedModelCodeWithoutImports(model);
|
||||
}
|
||||
|
||||
/// 生成枚举代码(不包含导入语句)
|
||||
String _generateEnumCodeWithoutImports(ApiModel model) {
|
||||
final className = StringUtils.generateClassName(model.name);
|
||||
final enumType = model.enumType?.value ?? 'string';
|
||||
final buffer = StringBuffer();
|
||||
|
||||
// 生成枚举类注释
|
||||
if (model.description.isNotEmpty) {
|
||||
buffer.writeln(StringUtils.generateComment(model.description));
|
||||
}
|
||||
|
||||
// 添加 @JsonEnum 注解
|
||||
buffer.writeln('@JsonEnum()');
|
||||
buffer.writeln('enum $className {');
|
||||
|
||||
// 生成枚举值
|
||||
for (int i = 0; i < model.enumValues.length; i++) {
|
||||
final value = model.enumValues[i];
|
||||
final enumName = StringUtils.generateEnumValueName(value, i);
|
||||
|
||||
if (enumType == 'integer' || enumType == 'number') {
|
||||
buffer.writeln(' $enumName($value),');
|
||||
} else {
|
||||
buffer.writeln(' $enumName(\'$value\'),');
|
||||
}
|
||||
}
|
||||
|
||||
// 移除最后一个逗号
|
||||
final content = buffer.toString().trimRight();
|
||||
buffer.clear();
|
||||
buffer.writeln(content.substring(0, content.lastIndexOf(',')));
|
||||
buffer.writeln(';');
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成构造函数和方法
|
||||
buffer.writeln(' const $className(this.value);');
|
||||
buffer.writeln(
|
||||
' final ${enumType == 'integer' || enumType == 'number' ? 'int' : 'String'} value;',
|
||||
);
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成 fromValue 方法
|
||||
buffer.writeln(' static $className fromValue(dynamic value) {');
|
||||
buffer.writeln(' for (final enumValue in $className.values) {');
|
||||
buffer.writeln(' if (enumValue.value == value) {');
|
||||
buffer.writeln(' return enumValue;');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln(' throw ArgumentError(\'Unknown enum value: \$value\');');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成 fromJson 方法
|
||||
buffer.writeln(' factory $className.fromJson(dynamic json) {');
|
||||
buffer.writeln(' return fromValue(json);');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成 toJson 方法
|
||||
buffer.writeln(' dynamic toJson() => value;');
|
||||
buffer.writeln('');
|
||||
|
||||
buffer.writeln('}');
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
// 已移动到 StringUtils.generateEnumValueName
|
||||
|
||||
/// 生成简洁版模型代码(不包含导入语句)
|
||||
String _generateSimpleModelCodeWithoutImports(ApiModel model) {
|
||||
final className = StringUtils.generateClassName(model.name);
|
||||
final buffer = StringBuffer();
|
||||
|
||||
// 生成类注释
|
||||
if (model.description.isNotEmpty) {
|
||||
buffer.writeln(StringUtils.generateComment(model.description));
|
||||
}
|
||||
|
||||
buffer.writeln('class $className {');
|
||||
|
||||
// 生成属性
|
||||
model.properties.forEach((propName, property) {
|
||||
final dartType = getDartPropertyType(property);
|
||||
final nullable = property.nullable ? '?' : '';
|
||||
final dartPropName = StringUtils.toDartPropertyName(propName);
|
||||
|
||||
if (property.description.isNotEmpty) {
|
||||
buffer.writeln(
|
||||
' ${StringUtils.generateComment(property.description)}',
|
||||
);
|
||||
}
|
||||
|
||||
buffer.writeln(' final $dartType$nullable $dartPropName;');
|
||||
buffer.writeln('');
|
||||
});
|
||||
|
||||
// 生成构造函数
|
||||
if (model.properties.isEmpty) {
|
||||
buffer.writeln(' const $className();');
|
||||
} else {
|
||||
buffer.writeln(' const $className({');
|
||||
model.properties.forEach((propName, property) {
|
||||
final dartPropName = StringUtils.toDartPropertyName(propName);
|
||||
final required = property.required ? 'required ' : '';
|
||||
buffer.writeln(' ${required}this.$dartPropName,');
|
||||
});
|
||||
buffer.writeln(' });');
|
||||
}
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成 fromJson 方法
|
||||
buffer.writeln(
|
||||
' factory $className.fromJson(Map<String, dynamic> json) {',
|
||||
);
|
||||
if (model.properties.isEmpty) {
|
||||
buffer.writeln(' return const $className();');
|
||||
} else {
|
||||
buffer.writeln(' return $className(');
|
||||
model.properties.forEach((propName, property) {
|
||||
final dartPropName = StringUtils.toDartPropertyName(propName);
|
||||
final dartType = getDartPropertyType(property);
|
||||
|
||||
buffer.write(' $dartPropName: ');
|
||||
|
||||
// 生成类型转换逻辑
|
||||
if (property.type == PropertyType.reference &&
|
||||
property.reference != null) {
|
||||
final refType = StringUtils.generateClassName(property.reference!);
|
||||
if (property.nullable) {
|
||||
buffer.write(
|
||||
'json[\'$propName\'] != null ? $refType.fromJson(json[\'$propName\']) : null',
|
||||
);
|
||||
} else {
|
||||
buffer.write('$refType.fromJson(json[\'$propName\'])');
|
||||
}
|
||||
} else if (property.type == PropertyType.array) {
|
||||
// 简化的数组处理
|
||||
buffer.write(
|
||||
'json[\'$propName\'] != null ? List<dynamic>.from(json[\'$propName\']) : null',
|
||||
);
|
||||
} else {
|
||||
// 基本类型
|
||||
if (property.nullable) {
|
||||
buffer.write('json[\'$propName\'] as $dartType?');
|
||||
} else {
|
||||
buffer.write('json[\'$propName\'] as $dartType');
|
||||
}
|
||||
}
|
||||
|
||||
buffer.writeln(',');
|
||||
});
|
||||
buffer.writeln(' );');
|
||||
}
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成 toJson 方法
|
||||
buffer.writeln(' Map<String, dynamic> toJson() {');
|
||||
buffer.writeln(' return {');
|
||||
model.properties.forEach((propName, property) {
|
||||
final dartPropName = StringUtils.toDartPropertyName(propName);
|
||||
|
||||
if (property.type == PropertyType.reference &&
|
||||
property.reference != null) {
|
||||
buffer.write(' \'$propName\': $dartPropName?.toJson()');
|
||||
} else if (property.type == PropertyType.array) {
|
||||
buffer.write(' \'$propName\': $dartPropName');
|
||||
} else {
|
||||
buffer.write(' \'$propName\': $dartPropName');
|
||||
}
|
||||
|
||||
buffer.writeln(',');
|
||||
});
|
||||
buffer.writeln(' };');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成 copyWith 方法
|
||||
if (model.properties.isNotEmpty) {
|
||||
buffer.writeln(' $className copyWith({');
|
||||
model.properties.forEach((propName, property) {
|
||||
final dartType = getDartPropertyType(property);
|
||||
final dartPropName = StringUtils.toDartPropertyName(propName);
|
||||
buffer.writeln(' $dartType? $dartPropName,');
|
||||
});
|
||||
buffer.writeln(' }) {');
|
||||
buffer.writeln(' return $className(');
|
||||
model.properties.forEach((propName, property) {
|
||||
final dartPropName = StringUtils.toDartPropertyName(propName);
|
||||
buffer.writeln(
|
||||
' $dartPropName: $dartPropName ?? this.$dartPropName,',
|
||||
);
|
||||
});
|
||||
buffer.writeln(' );');
|
||||
buffer.writeln(' }');
|
||||
buffer.writeln('');
|
||||
}
|
||||
|
||||
buffer.writeln('}');
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// 生成带注解的模型代码(不包含导入语句)
|
||||
String _generateAnnotatedModelCodeWithoutImports(ApiModel model) {
|
||||
final className = StringUtils.generateClassName(model.name);
|
||||
final buffer = StringBuffer();
|
||||
|
||||
// 生成 part 声明
|
||||
final partFileName = StringUtils.generateFileName(model.name);
|
||||
final generatedPart = partFileName.replaceAll('.dart', '.g.dart');
|
||||
buffer.writeln('part \'$generatedPart\';');
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成类注释
|
||||
if (model.description.isNotEmpty) {
|
||||
buffer.writeln(StringUtils.generateComment(model.description));
|
||||
}
|
||||
|
||||
buffer.writeln('@JsonSerializable()');
|
||||
buffer.writeln('class $className {');
|
||||
|
||||
// 生成属性
|
||||
model.properties.forEach((propName, property) {
|
||||
final dartType = getDartPropertyType(property);
|
||||
final nullable = property.nullable ? '?' : '';
|
||||
final dartPropName = StringUtils.toDartPropertyName(propName);
|
||||
|
||||
if (property.description.isNotEmpty) {
|
||||
buffer.writeln(
|
||||
' ${StringUtils.generateComment(property.description)}',
|
||||
);
|
||||
}
|
||||
|
||||
// 添加JsonKey注解
|
||||
final needsJsonKey =
|
||||
_needsJsonKeyAnnotation(dartPropName, propName, property);
|
||||
if (needsJsonKey.isNotEmpty) {
|
||||
buffer.writeln(' @JsonKey($needsJsonKey)');
|
||||
}
|
||||
|
||||
buffer.writeln(' final $dartType$nullable $dartPropName;');
|
||||
buffer.writeln('');
|
||||
});
|
||||
|
||||
// 生成构造函数
|
||||
if (model.properties.isEmpty) {
|
||||
buffer.writeln(' const $className();');
|
||||
} else {
|
||||
buffer.writeln(' const $className({');
|
||||
model.properties.forEach((propName, property) {
|
||||
final dartPropName = StringUtils.toDartPropertyName(propName);
|
||||
final required = property.required ? 'required ' : '';
|
||||
buffer.writeln(' ${required}this.$dartPropName,');
|
||||
});
|
||||
buffer.writeln(' });');
|
||||
}
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成 fromJson 工厂方法
|
||||
buffer.writeln(
|
||||
' factory $className.fromJson(Map<String, dynamic> json) => _\$${className}FromJson(json);',
|
||||
);
|
||||
buffer.writeln('');
|
||||
|
||||
// 生成 toJson 方法
|
||||
buffer.writeln(
|
||||
' Map<String, dynamic> toJson() => _\$${className}ToJson(this);',
|
||||
);
|
||||
buffer.writeln('');
|
||||
|
||||
buffer.writeln('}');
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// 生成模型索引文件
|
||||
String generateIndexFile(List<String> modelFileNames) {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
buffer.writeln(generateFileHeader('API 模型导出文件'));
|
||||
buffer.writeln('');
|
||||
|
||||
// 添加 library 声明
|
||||
buffer.writeln('library;');
|
||||
buffer.writeln('');
|
||||
|
||||
// 按文件名排序并导出所有模型
|
||||
final sortedFiles = List<String>.from(modelFileNames)..sort();
|
||||
|
||||
for (final fileName in sortedFiles) {
|
||||
buffer.writeln('export \'$fileName\';');
|
||||
}
|
||||
|
||||
return generateTypeCheckedCode(buffer.toString());
|
||||
}
|
||||
|
||||
/// 判断是否需要JsonKey注解以及注解的内容
|
||||
String _needsJsonKeyAnnotation(
|
||||
String dartPropName, String propName, ApiProperty property) {
|
||||
final annotations = <String>[];
|
||||
|
||||
// 属性名与JSON字段名不同时需要name参数
|
||||
if (dartPropName != propName) {
|
||||
annotations.add('name: \'$propName\'');
|
||||
}
|
||||
|
||||
// DateTime类型需要特殊处理
|
||||
if (property.type == PropertyType.string &&
|
||||
(property.format == 'date-time' || property.format == 'date')) {
|
||||
// 对于DateTime类型,通常json_annotation会自动处理,但可以显式指定
|
||||
// annotations.add('fromJson: DateTime.parse, toJson: _dateTimeToString');
|
||||
}
|
||||
|
||||
// 枚举类型的处理
|
||||
if (property.type == PropertyType.reference) {
|
||||
// 检查是否是枚举类型(这里需要更复杂的逻辑来判断)
|
||||
// 暂时不添加特殊处理
|
||||
}
|
||||
|
||||
// 如果需要忽略某些属性
|
||||
// if (shouldIgnore) {
|
||||
// annotations.add('ignore: true');
|
||||
// }
|
||||
|
||||
return annotations.join(', ');
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,412 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../core/config.dart';
|
||||
import '../core/exceptions.dart';
|
||||
import '../core/models.dart';
|
||||
import '../utils/cache_manager.dart';
|
||||
import '../utils/performance_monitor.dart';
|
||||
import '../utils/string_utils.dart';
|
||||
import '../utils/type_validator.dart';
|
||||
|
||||
/// Swagger数据解析器
|
||||
/// 负责解析Swagger JSON文档并提取相关信息
|
||||
class SwaggerDataParser {
|
||||
final CacheManager _cacheManager;
|
||||
final PerformanceMonitor _performanceMonitor;
|
||||
final TypeValidator _typeValidator;
|
||||
|
||||
// 缓存解析结果
|
||||
SwaggerDocument? _cachedDocument;
|
||||
|
||||
SwaggerDataParser()
|
||||
: _cacheManager = CacheManager(),
|
||||
_performanceMonitor = PerformanceMonitor(),
|
||||
_typeValidator = TypeValidator();
|
||||
|
||||
/// 获取并解析Swagger JSON文档
|
||||
Future<SwaggerDocument> fetchAndParseSwaggerDocument() async {
|
||||
// 如果有缓存且未过期,直接返回缓存结果
|
||||
if (_cachedDocument != null) {
|
||||
return _cachedDocument!;
|
||||
}
|
||||
|
||||
return _performanceMonitor.measure(
|
||||
'fetchAndParseSwaggerDocument',
|
||||
() async {
|
||||
try {
|
||||
print('🔄 正在获取Swagger JSON文档...');
|
||||
|
||||
Map<String, dynamic> jsonData;
|
||||
|
||||
if (SwaggerConfig.swaggerJsonUrl.startsWith('file://')) {
|
||||
// 处理本地文件
|
||||
final filePath =
|
||||
SwaggerConfig.swaggerJsonUrl.replaceFirst('file://', '');
|
||||
final file = File(filePath);
|
||||
if (await file.exists()) {
|
||||
final content = await file.readAsString();
|
||||
jsonData = json.decode(content) as Map<String, dynamic>;
|
||||
} else {
|
||||
throw SwaggerParseException(
|
||||
'本地文件不存在',
|
||||
url: SwaggerConfig.swaggerJsonUrl,
|
||||
details: '文件路径: $filePath',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 处理远程URL
|
||||
final response = await http.get(
|
||||
Uri.parse(SwaggerConfig.swaggerJsonUrl),
|
||||
headers: SwaggerConfig.httpHeaders,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
jsonData = json.decode(response.body) as Map<String, dynamic>;
|
||||
} else {
|
||||
throw SwaggerParseException(
|
||||
'HTTP请求失败',
|
||||
url: SwaggerConfig.swaggerJsonUrl,
|
||||
statusCode: response.statusCode,
|
||||
details: 'HTTP响应状态码: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_cachedDocument = await parseSwaggerDocument(jsonData);
|
||||
print('✅ Swagger文档解析完成');
|
||||
return _cachedDocument!;
|
||||
} catch (e) {
|
||||
if (e is SwaggerParseException) {
|
||||
rethrow;
|
||||
}
|
||||
throw SwaggerParseException(
|
||||
'获取Swagger文档失败',
|
||||
url: SwaggerConfig.swaggerJsonUrl,
|
||||
details: e.toString(),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 解析Swagger JSON文档
|
||||
Future<SwaggerDocument> parseSwaggerDocument(
|
||||
Map<String, dynamic> jsonData,
|
||||
) async {
|
||||
return _performanceMonitor.measure('parseSwaggerDocument', () async {
|
||||
// 尝试从缓存获取
|
||||
final cacheKey = 'swagger_doc_${jsonData.hashCode}';
|
||||
final cachedResult = _cacheManager.get<SwaggerDocument>(cacheKey);
|
||||
if (cachedResult != null) {
|
||||
_cachedDocument = cachedResult;
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
// 解析文档基本信息
|
||||
final info = jsonData['info'] as Map<String, dynamic>? ?? {};
|
||||
final title = info['title'] as String? ?? 'API Documentation';
|
||||
final version = info['version'] as String? ?? '1.0.0';
|
||||
final description = info['description'] as String? ?? '';
|
||||
|
||||
// 解析其他基本信息
|
||||
final host = jsonData['host'] as String? ?? '';
|
||||
final basePath = jsonData['basePath'] as String? ?? '/';
|
||||
final schemes = List<String>.from(jsonData['schemes'] ?? ['https']);
|
||||
final consumes = List<String>.from(jsonData['consumes'] ?? []);
|
||||
final produces = List<String>.from(jsonData['produces'] ?? []);
|
||||
|
||||
// 解析tags信息 (用于获取控制器描述)
|
||||
final tagsInfo = _parseTagsInfo(jsonData);
|
||||
|
||||
// 解析API路径
|
||||
final paths = _parseApiPaths(jsonData);
|
||||
|
||||
// 解析API模型
|
||||
final models = _parseApiModels(jsonData);
|
||||
|
||||
// 解析API控制器 (传入tags信息)
|
||||
final controllers = _parseApiControllers(paths, tagsInfo);
|
||||
|
||||
final document = SwaggerDocument(
|
||||
title: title,
|
||||
version: version,
|
||||
description: description,
|
||||
host: host,
|
||||
basePath: basePath,
|
||||
schemes: schemes,
|
||||
consumes: consumes,
|
||||
produces: produces,
|
||||
paths: paths,
|
||||
models: models,
|
||||
controllers: controllers,
|
||||
);
|
||||
|
||||
// 缓存结果
|
||||
_cacheManager.put(cacheKey, document);
|
||||
_cachedDocument = document;
|
||||
|
||||
return document;
|
||||
});
|
||||
}
|
||||
|
||||
/// 解析tags信息
|
||||
Map<String, String> _parseTagsInfo(Map<String, dynamic> jsonData) {
|
||||
final tagsInfo = <String, String>{};
|
||||
|
||||
try {
|
||||
final tags = jsonData['tags'] as List<dynamic>?;
|
||||
if (tags != null) {
|
||||
for (final tag in tags) {
|
||||
if (tag is Map<String, dynamic>) {
|
||||
final name = tag['name'] as String?;
|
||||
final description = tag['description'] as String?;
|
||||
if (name != null && description != null) {
|
||||
tagsInfo[name] = description;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('⚠️ 解析tags信息时发生错误: $e');
|
||||
}
|
||||
|
||||
return tagsInfo;
|
||||
}
|
||||
|
||||
/// 解析API路径
|
||||
Map<String, ApiPath> _parseApiPaths(Map<String, dynamic> jsonData) {
|
||||
final paths = <String, ApiPath>{};
|
||||
final pathsData = jsonData['paths'] as Map<String, dynamic>?;
|
||||
|
||||
if (pathsData == null) {
|
||||
throw SwaggerParseException('未发现API路径定义');
|
||||
}
|
||||
|
||||
try {
|
||||
pathsData.forEach((pathKey, pathValue) {
|
||||
if (pathValue is Map<String, dynamic>) {
|
||||
pathValue.forEach((methodKey, methodValue) {
|
||||
if (methodValue is Map<String, dynamic>) {
|
||||
final method = HttpMethod.fromString(methodKey);
|
||||
final apiPath = ApiPath.fromJson(
|
||||
pathKey,
|
||||
methodKey, // 传入字符串而不是HttpMethod对象
|
||||
methodValue,
|
||||
);
|
||||
final key =
|
||||
'${method.value.toUpperCase()}_${pathKey.replaceAll('/', '_')}';
|
||||
paths[key] = apiPath;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
throw SwaggerParseException('解析API路径失败', details: e.toString());
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
/// 解析API模型
|
||||
Map<String, ApiModel> _parseApiModels(Map<String, dynamic> jsonData) {
|
||||
final models = <String, ApiModel>{};
|
||||
|
||||
// 优先解析 components/schemas (Swagger 3.0)
|
||||
final schemas = jsonData['components']?['schemas'] as Map<String, dynamic>?;
|
||||
// 如果没有 components/schemas,尝试解析 definitions (Swagger 2.0)
|
||||
final definitions = jsonData['definitions'] as Map<String, dynamic>?;
|
||||
|
||||
final modelDefinitions = schemas ?? definitions;
|
||||
|
||||
if (modelDefinitions == null) {
|
||||
print('ℹ️ 未发现模型定义 (components/schemas 或 definitions)');
|
||||
return models;
|
||||
}
|
||||
|
||||
print(
|
||||
'🔍 发现模型定义位置: ${schemas != null ? 'components/schemas' : 'definitions'}',
|
||||
);
|
||||
|
||||
try {
|
||||
modelDefinitions.forEach((name, definition) {
|
||||
final model = ApiModel.fromJson(
|
||||
name,
|
||||
definition as Map<String, dynamic>,
|
||||
);
|
||||
models[name] = model;
|
||||
});
|
||||
} catch (e) {
|
||||
throw SwaggerParseException('解析API模型失败', details: e.toString());
|
||||
}
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
/// 解析API控制器
|
||||
Map<String, ApiController> _parseApiControllers(
|
||||
Map<String, ApiPath> paths,
|
||||
Map<String, String> tagsInfo,
|
||||
) {
|
||||
final controllers = <String, List<ApiPath>>{};
|
||||
|
||||
try {
|
||||
// 根据tags分组API路径
|
||||
for (final apiPath in paths.values) {
|
||||
for (final tag in apiPath.tags) {
|
||||
controllers.putIfAbsent(tag, () => []).add(apiPath);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建控制器对象
|
||||
final result = <String, ApiController>{};
|
||||
controllers.forEach((name, pathList) {
|
||||
// 从tags信息中获取描述
|
||||
final swaggerDescription = tagsInfo[name];
|
||||
|
||||
// 使用通用的描述获取方法
|
||||
final description = SwaggerConfig.getControllerDescription(
|
||||
name,
|
||||
swaggerDescription: swaggerDescription,
|
||||
);
|
||||
|
||||
result[name] = ApiController(
|
||||
name: name,
|
||||
description: description,
|
||||
paths: pathList,
|
||||
);
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
throw SwaggerParseException('解析API控制器失败', details: e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析属性类型
|
||||
String parsePropertyType(Map<String, dynamic> propData) {
|
||||
try {
|
||||
// 直接类型
|
||||
if (propData['type'] != null) {
|
||||
return propData['type'] as String;
|
||||
}
|
||||
|
||||
// 引用类型 ($ref)
|
||||
if (propData['\$ref'] != null) {
|
||||
final ref = propData['\$ref'] as String;
|
||||
// 从 #/components/schemas/ModelName 或 #/definitions/ModelName 中提取类型名
|
||||
final parts = ref.split('/');
|
||||
if (parts.isNotEmpty) {
|
||||
return parts.last;
|
||||
}
|
||||
}
|
||||
|
||||
// 数组类型
|
||||
if (propData['items'] != null) {
|
||||
final items = propData['items'] as Map<String, dynamic>;
|
||||
final itemType = parsePropertyType(items);
|
||||
return 'array<$itemType>';
|
||||
}
|
||||
|
||||
// 默认类型
|
||||
return 'string';
|
||||
} catch (e) {
|
||||
throw SwaggerParseException('解析属性类型失败', details: e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取Dart类型映射
|
||||
String getDartType(String swaggerType, String? format) {
|
||||
switch (swaggerType.toLowerCase()) {
|
||||
case 'string':
|
||||
switch (format?.toLowerCase()) {
|
||||
case 'date':
|
||||
case 'date-time':
|
||||
return 'DateTime';
|
||||
case 'byte':
|
||||
case 'binary':
|
||||
return 'String';
|
||||
default:
|
||||
return 'String';
|
||||
}
|
||||
case 'integer':
|
||||
switch (format?.toLowerCase()) {
|
||||
case 'int64':
|
||||
return 'int';
|
||||
case 'int32':
|
||||
default:
|
||||
return 'int';
|
||||
}
|
||||
case 'number':
|
||||
switch (format?.toLowerCase()) {
|
||||
case 'float':
|
||||
case 'double':
|
||||
return 'double';
|
||||
default:
|
||||
return 'double';
|
||||
}
|
||||
case 'boolean':
|
||||
return 'bool';
|
||||
case 'array':
|
||||
return 'List<dynamic>';
|
||||
case 'object':
|
||||
return 'Map<String, dynamic>';
|
||||
case 'file':
|
||||
return 'String';
|
||||
default:
|
||||
// 检查是否为数组类型
|
||||
if (swaggerType.startsWith('array<') && swaggerType.endsWith('>')) {
|
||||
final itemType = swaggerType.substring(6, swaggerType.length - 1);
|
||||
final dartItemType = getDartType(itemType, null);
|
||||
return 'List<$dartItemType>';
|
||||
}
|
||||
|
||||
// 默认为自定义类型
|
||||
return StringUtils.generateClassName(swaggerType);
|
||||
}
|
||||
}
|
||||
|
||||
/// 清除缓存
|
||||
void clearCache() {
|
||||
_cachedDocument = null;
|
||||
_cacheManager.clear();
|
||||
}
|
||||
|
||||
/// 获取文档统计信息
|
||||
Map<String, dynamic> getDocumentStats() {
|
||||
if (_cachedDocument == null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
final doc = _cachedDocument!;
|
||||
|
||||
// 统计HTTP方法
|
||||
final methodStats = <String, int>{};
|
||||
for (final path in doc.paths.values) {
|
||||
final method = path.method.value;
|
||||
methodStats[method] = (methodStats[method] ?? 0) + 1;
|
||||
}
|
||||
|
||||
// 统计模型类型
|
||||
final modelStats = <String, int>{};
|
||||
for (final model in doc.models.values) {
|
||||
if (model.isEnum) {
|
||||
modelStats['enum'] = (modelStats['enum'] ?? 0) + 1;
|
||||
} else {
|
||||
modelStats['class'] = (modelStats['class'] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
'title': doc.title,
|
||||
'version': doc.version,
|
||||
'paths': doc.paths.length,
|
||||
'models': doc.models.length,
|
||||
'controllers': doc.controllers.length,
|
||||
'methods': methodStats,
|
||||
'modelTypes': modelStats,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'commands/base_command.dart';
|
||||
import 'commands/generate_command.dart';
|
||||
import 'core/config.dart';
|
||||
import 'utils/performance_monitor.dart';
|
||||
import 'utils/string_utils.dart';
|
||||
|
||||
/// Swagger CLI 应用程序
|
||||
/// 使用命令模式架构的新版本CLI工具
|
||||
class SwaggerCLI {
|
||||
final Map<String, BaseCommand> _commands = {};
|
||||
final PerformanceMonitor _monitor = PerformanceMonitor();
|
||||
|
||||
SwaggerCLI() {
|
||||
_registerCommands();
|
||||
}
|
||||
|
||||
/// 注册所有命令
|
||||
void _registerCommands() {
|
||||
_registerCommand(GenerateCommand());
|
||||
// 未来可以添加更多命令:
|
||||
// _registerCommand(ParseCommand());
|
||||
// _registerCommand(ValidateCommand());
|
||||
// _registerCommand(InfoCommand());
|
||||
// _registerCommand(TestCommand());
|
||||
// _registerCommand(CleanCommand());
|
||||
}
|
||||
|
||||
/// 注册单个命令
|
||||
void _registerCommand(BaseCommand command) {
|
||||
_commands[command.name] = command;
|
||||
}
|
||||
|
||||
/// 运行CLI应用程序
|
||||
Future<int> run(List<String> arguments) async {
|
||||
try {
|
||||
_showBanner();
|
||||
|
||||
if (arguments.isEmpty ||
|
||||
arguments.first == 'help' ||
|
||||
arguments.first == '--help') {
|
||||
_showHelp();
|
||||
return 0;
|
||||
}
|
||||
|
||||
final commandName = arguments.first;
|
||||
final commandArgs =
|
||||
arguments.length > 1 ? arguments.sublist(1) : <String>[];
|
||||
|
||||
// 检查特殊命令
|
||||
if (commandName == 'version' || commandName == '--version') {
|
||||
_showVersion();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 查找并执行命令
|
||||
final command = _commands[commandName];
|
||||
if (command == null) {
|
||||
print('❌ 未知命令: $commandName');
|
||||
print('');
|
||||
_showAvailableCommands();
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 检查命令帮助
|
||||
if (commandArgs.contains('--help') || commandArgs.contains('-h')) {
|
||||
command.showHelp();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 执行命令
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final exitCode = await command.execute(commandArgs);
|
||||
stopwatch.stop();
|
||||
|
||||
// 显示执行时间
|
||||
if (exitCode == 0) {
|
||||
print('');
|
||||
print('⏱️ 执行时间: ${StringUtils.formatDuration(stopwatch.elapsed)}');
|
||||
}
|
||||
|
||||
return exitCode;
|
||||
} catch (error, stackTrace) {
|
||||
print('❌ 应用程序错误: $error');
|
||||
print('堆栈跟踪: $stackTrace');
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示应用程序横幅
|
||||
void _showBanner() {
|
||||
print('');
|
||||
print('🚀 Swagger API 代码生成器 v2.0');
|
||||
print('=====================================');
|
||||
print('强大的 Swagger API 代码生成工具');
|
||||
print('');
|
||||
}
|
||||
|
||||
/// 显示帮助信息
|
||||
void _showHelp() {
|
||||
print('用法: dart swagger_cli_new.dart <命令> [选项]');
|
||||
print('');
|
||||
print('全新的命令式架构,提供更好的可扩展性和用户体验。');
|
||||
print('');
|
||||
_showAvailableCommands();
|
||||
_showGlobalOptions();
|
||||
_showExamples();
|
||||
_showContact();
|
||||
}
|
||||
|
||||
/// 显示可用命令
|
||||
void _showAvailableCommands() {
|
||||
print('📋 可用命令:');
|
||||
print('');
|
||||
|
||||
for (final command in _commands.values) {
|
||||
print(' ${command.name.padRight(12)} ${command.description}');
|
||||
}
|
||||
|
||||
print(' help 显示帮助信息');
|
||||
print(' version 显示版本信息');
|
||||
print('');
|
||||
}
|
||||
|
||||
/// 显示全局选项
|
||||
void _showGlobalOptions() {
|
||||
print('🔧 全局选项:');
|
||||
print(' -h, --help 显示帮助信息');
|
||||
print(' --version 显示版本信息');
|
||||
print('');
|
||||
}
|
||||
|
||||
/// 显示使用示例
|
||||
void _showExamples() {
|
||||
print('💡 使用示例:');
|
||||
print('');
|
||||
print(' # 生成所有文件');
|
||||
print(' dart swagger_cli_new.dart generate --all');
|
||||
print('');
|
||||
print(' # 只生成模型文件(简洁版本)');
|
||||
print(' dart swagger_cli_new.dart generate --models --simple');
|
||||
print('');
|
||||
print(' # 生成到指定目录并启用性能监控');
|
||||
print(
|
||||
' dart swagger_cli_new.dart generate --all --output-dir lib/generated --performance',
|
||||
);
|
||||
print('');
|
||||
print(' # 查看具体命令的帮助');
|
||||
print(' dart swagger_cli_new.dart generate --help');
|
||||
print('');
|
||||
}
|
||||
|
||||
/// 显示联系信息
|
||||
void _showContact() {
|
||||
print('🌐 更多信息:');
|
||||
print(' API文档: ${SwaggerConfig.swaggerJsonUrl}');
|
||||
print(' 基础URL: ${SwaggerConfig.baseUrl}');
|
||||
print('');
|
||||
}
|
||||
|
||||
/// 显示版本信息
|
||||
void _showVersion() {
|
||||
print('Swagger CLI v2.0.0');
|
||||
print('构建于: ${DateTime.now().toIso8601String()}');
|
||||
print('Dart SDK: ${Platform.version}');
|
||||
print('');
|
||||
print('功能特性:');
|
||||
print('- 🏗️ 模块化命令架构');
|
||||
print('- 🚀 性能监控和优化');
|
||||
print('- 🔍 智能类型验证');
|
||||
print('- 📋 详细的错误报告');
|
||||
print('- 💾 智能缓存机制');
|
||||
print('- 📚 丰富的文档生成');
|
||||
print('');
|
||||
}
|
||||
|
||||
/// 格式化持续时间
|
||||
// 已移动到 StringUtils.formatDuration
|
||||
|
||||
/// 获取可用命令列表
|
||||
List<String> get availableCommands => _commands.keys.toList();
|
||||
|
||||
/// 获取特定命令
|
||||
BaseCommand? getCommand(String name) => _commands[name];
|
||||
}
|
||||
|
||||
/// CLI应用程序入口点
|
||||
Future<void> main(List<String> arguments) async {
|
||||
final cli = SwaggerCLI();
|
||||
final exitCode = await cli.run(arguments);
|
||||
exit(exitCode);
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
import 'dart:collection';
|
||||
|
||||
import 'string_utils.dart';
|
||||
|
||||
/// 缓存管理器
|
||||
/// 提供简单的内存缓存功能
|
||||
class CacheManager {
|
||||
static const int _maxMemoryItems = 100;
|
||||
|
||||
// 内存缓存
|
||||
final Map<String, _CacheEntry> _memoryCache = {};
|
||||
final Queue<String> _accessOrder = Queue<String>();
|
||||
|
||||
CacheManager();
|
||||
|
||||
/// 从缓存获取数据
|
||||
T? get<T>(String key) {
|
||||
final entry = _memoryCache[key];
|
||||
if (entry != null && !entry.isExpired) {
|
||||
_updateAccessOrder(key);
|
||||
return entry.value as T?;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 存储数据到缓存
|
||||
void put<T>(String key, T value, [Duration? ttl]) {
|
||||
ttl ??= const Duration(hours: 1);
|
||||
|
||||
// 检查内存缓存大小限制
|
||||
if (_memoryCache.length >= _maxMemoryItems) {
|
||||
_evictOldestItem();
|
||||
}
|
||||
|
||||
final entry = _CacheEntry(
|
||||
value: value,
|
||||
expiresAt: DateTime.now().add(ttl),
|
||||
);
|
||||
|
||||
_memoryCache[key] = entry;
|
||||
_updateAccessOrder(key);
|
||||
}
|
||||
|
||||
/// 检查是否存在缓存
|
||||
bool has(String key) {
|
||||
final entry = _memoryCache[key];
|
||||
return entry != null && !entry.isExpired;
|
||||
}
|
||||
|
||||
/// 删除缓存项
|
||||
void remove(String key) {
|
||||
_memoryCache.remove(key);
|
||||
_accessOrder.removeWhere((k) => k == key);
|
||||
}
|
||||
|
||||
/// 清空缓存
|
||||
void clear() {
|
||||
_memoryCache.clear();
|
||||
_accessOrder.clear();
|
||||
}
|
||||
|
||||
/// 获取缓存统计信息
|
||||
CacheStats getStats() {
|
||||
return CacheStats(
|
||||
memoryItems: _memoryCache.length,
|
||||
diskItems: 0,
|
||||
hitRate: 0.0,
|
||||
totalSize: 0,
|
||||
);
|
||||
}
|
||||
|
||||
/// 更新访问顺序
|
||||
void _updateAccessOrder(String key) {
|
||||
_accessOrder.removeWhere((k) => k == key);
|
||||
_accessOrder.addLast(key);
|
||||
}
|
||||
|
||||
/// 驱逐最旧的项
|
||||
void _evictOldestItem() {
|
||||
if (_accessOrder.isNotEmpty) {
|
||||
final oldestKey = _accessOrder.removeFirst();
|
||||
_memoryCache.remove(oldestKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 缓存条目
|
||||
class _CacheEntry {
|
||||
final dynamic value;
|
||||
final DateTime expiresAt;
|
||||
|
||||
_CacheEntry({
|
||||
required this.value,
|
||||
required this.expiresAt,
|
||||
});
|
||||
|
||||
bool get isExpired => DateTime.now().isAfter(expiresAt);
|
||||
}
|
||||
|
||||
/// 缓存统计信息
|
||||
class CacheStats {
|
||||
final int memoryItems;
|
||||
final int diskItems;
|
||||
final double hitRate;
|
||||
final int totalSize;
|
||||
|
||||
const CacheStats({
|
||||
required this.memoryItems,
|
||||
required this.diskItems,
|
||||
required this.hitRate,
|
||||
required this.totalSize,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'CacheStats{'
|
||||
'memoryItems: $memoryItems, '
|
||||
'diskItems: $diskItems, '
|
||||
'hitRate: ${(hitRate * 100).toStringAsFixed(1)}%, '
|
||||
'totalSize: ${StringUtils.formatBytes(totalSize)}'
|
||||
'}';
|
||||
}
|
||||
|
||||
// 已移动到 StringUtils.formatBytes
|
||||
}
|
||||
|
|
@ -0,0 +1,457 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
/// 文件工具类
|
||||
/// 提供文件操作、目录管理和代码格式化功能
|
||||
class FileUtils {
|
||||
/// 确保目录存在
|
||||
static Future<Directory> ensureDirectoryExists(String dirPath) async {
|
||||
final directory = Directory(dirPath);
|
||||
if (!await directory.exists()) {
|
||||
await directory.create(recursive: true);
|
||||
}
|
||||
return directory;
|
||||
}
|
||||
|
||||
/// 安全写入文件
|
||||
static Future<void> safeWriteFile(String filePath, String content) async {
|
||||
try {
|
||||
final file = File(filePath);
|
||||
final directory = file.parent;
|
||||
|
||||
// 确保目录存在
|
||||
if (!await directory.exists()) {
|
||||
await directory.create(recursive: true);
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
await file.writeAsString(content);
|
||||
} catch (e) {
|
||||
throw FileSystemException('写入文件失败: $filePath', filePath);
|
||||
}
|
||||
}
|
||||
|
||||
/// 安全读取文件
|
||||
static Future<String> safeReadFile(String filePath) async {
|
||||
try {
|
||||
final file = File(filePath);
|
||||
if (!await file.exists()) {
|
||||
throw FileSystemException('文件不存在: $filePath', filePath);
|
||||
}
|
||||
|
||||
return await file.readAsString();
|
||||
} catch (e) {
|
||||
throw FileSystemException('读取文件失败: $filePath', filePath);
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查文件是否存在
|
||||
static Future<bool> fileExists(String filePath) async {
|
||||
return await File(filePath).exists();
|
||||
}
|
||||
|
||||
/// 检查目录是否存在
|
||||
static Future<bool> directoryExists(String dirPath) async {
|
||||
return await Directory(dirPath).exists();
|
||||
}
|
||||
|
||||
/// 删除文件(如果存在)
|
||||
static Future<void> deleteFileIfExists(String filePath) async {
|
||||
final file = File(filePath);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除目录(如果存在)
|
||||
static Future<void> deleteDirectoryIfExists(String dirPath) async {
|
||||
final directory = Directory(dirPath);
|
||||
if (await directory.exists()) {
|
||||
await directory.delete(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// 复制文件
|
||||
static Future<void> copyFile(
|
||||
String sourcePath, String destinationPath) async {
|
||||
try {
|
||||
final sourceFile = File(sourcePath);
|
||||
final destinationFile = File(destinationPath);
|
||||
|
||||
if (!await sourceFile.exists()) {
|
||||
throw FileSystemException('源文件不存在: $sourcePath', sourcePath);
|
||||
}
|
||||
|
||||
// 确保目标目录存在
|
||||
final destinationDir = destinationFile.parent;
|
||||
if (!await destinationDir.exists()) {
|
||||
await destinationDir.create(recursive: true);
|
||||
}
|
||||
|
||||
await sourceFile.copy(destinationPath);
|
||||
} catch (e) {
|
||||
throw FileSystemException('复制文件失败: $sourcePath -> $destinationPath',
|
||||
sourcePath, e is OSError ? e : null);
|
||||
}
|
||||
}
|
||||
|
||||
/// 移动文件
|
||||
static Future<void> moveFile(
|
||||
String sourcePath, String destinationPath) async {
|
||||
try {
|
||||
final sourceFile = File(sourcePath);
|
||||
final destinationFile = File(destinationPath);
|
||||
|
||||
if (!await sourceFile.exists()) {
|
||||
throw FileSystemException('源文件不存在: $sourcePath', sourcePath);
|
||||
}
|
||||
|
||||
// 确保目标目录存在
|
||||
final destinationDir = destinationFile.parent;
|
||||
if (!await destinationDir.exists()) {
|
||||
await destinationDir.create(recursive: true);
|
||||
}
|
||||
|
||||
await sourceFile.rename(destinationPath);
|
||||
} catch (e) {
|
||||
throw FileSystemException('移动文件失败: $sourcePath -> $destinationPath',
|
||||
sourcePath, e is OSError ? e : null);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取文件大小
|
||||
static Future<int> getFileSize(String filePath) async {
|
||||
try {
|
||||
final file = File(filePath);
|
||||
if (!await file.exists()) {
|
||||
return 0;
|
||||
}
|
||||
return await file.length();
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取目录大小
|
||||
static Future<int> getDirectorySize(String dirPath) async {
|
||||
try {
|
||||
final directory = Directory(dirPath);
|
||||
if (!await directory.exists()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int totalSize = 0;
|
||||
await for (final entity in directory.list(recursive: true)) {
|
||||
if (entity is File) {
|
||||
totalSize += await entity.length();
|
||||
}
|
||||
}
|
||||
return totalSize;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// 列出目录中的文件
|
||||
static Future<List<String>> listFiles(String dirPath,
|
||||
{String? extension}) async {
|
||||
try {
|
||||
final directory = Directory(dirPath);
|
||||
if (!await directory.exists()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final files = <String>[];
|
||||
await for (final entity in directory.list()) {
|
||||
if (entity is File) {
|
||||
if (extension == null || entity.path.endsWith(extension)) {
|
||||
files.add(entity.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
return files;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// 列出目录中的子目录
|
||||
static Future<List<String>> listDirectories(String dirPath) async {
|
||||
try {
|
||||
final directory = Directory(dirPath);
|
||||
if (!await directory.exists()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final directories = <String>[];
|
||||
await for (final entity in directory.list()) {
|
||||
if (entity is Directory) {
|
||||
directories.add(entity.path);
|
||||
}
|
||||
}
|
||||
return directories;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建备份文件
|
||||
static Future<String> createBackup(String filePath) async {
|
||||
try {
|
||||
final file = File(filePath);
|
||||
if (!await file.exists()) {
|
||||
throw FileSystemException('文件不存在: $filePath', filePath);
|
||||
}
|
||||
|
||||
final timestamp = DateTime.now().toIso8601String().replaceAll(':', '-');
|
||||
final backupPath = '${filePath}.backup.$timestamp';
|
||||
|
||||
await file.copy(backupPath);
|
||||
return backupPath;
|
||||
} catch (e) {
|
||||
throw FileSystemException(
|
||||
'创建备份失败: $filePath', filePath, e is OSError ? e : null);
|
||||
}
|
||||
}
|
||||
|
||||
/// 恢复备份文件
|
||||
static Future<void> restoreBackup(
|
||||
String backupPath, String originalPath) async {
|
||||
try {
|
||||
final backupFile = File(backupPath);
|
||||
if (!await backupFile.exists()) {
|
||||
throw FileSystemException('备份文件不存在: $backupPath', backupPath);
|
||||
}
|
||||
|
||||
await backupFile.copy(originalPath);
|
||||
} catch (e) {
|
||||
throw FileSystemException('恢复备份失败: $backupPath -> $originalPath',
|
||||
backupPath, e is OSError ? e : null);
|
||||
}
|
||||
}
|
||||
|
||||
/// 格式化文件路径
|
||||
static String formatPath(String filePath) {
|
||||
return path.normalize(filePath);
|
||||
}
|
||||
|
||||
/// 获取文件名(不包括路径)
|
||||
static String getFileName(String filePath) {
|
||||
return path.basename(filePath);
|
||||
}
|
||||
|
||||
/// 获取文件名(不包括扩展名)
|
||||
static String getFileNameWithoutExtension(String filePath) {
|
||||
return path.basenameWithoutExtension(filePath);
|
||||
}
|
||||
|
||||
/// 获取文件扩展名
|
||||
static String getFileExtension(String filePath) {
|
||||
return path.extension(filePath);
|
||||
}
|
||||
|
||||
/// 获取文件所在目录
|
||||
static String getDirectoryPath(String filePath) {
|
||||
return path.dirname(filePath);
|
||||
}
|
||||
|
||||
/// 连接路径
|
||||
static String joinPath(List<String> parts) {
|
||||
return path.joinAll(parts);
|
||||
}
|
||||
|
||||
/// 获取相对路径
|
||||
static String getRelativePath(String filePath, String basePath) {
|
||||
return path.relative(filePath, from: basePath);
|
||||
}
|
||||
|
||||
/// 获取绝对路径
|
||||
static String getAbsolutePath(String filePath) {
|
||||
return path.absolute(filePath);
|
||||
}
|
||||
|
||||
/// 检查路径是否为绝对路径
|
||||
static bool isAbsolute(String filePath) {
|
||||
return path.isAbsolute(filePath);
|
||||
}
|
||||
|
||||
/// 清理文件名(移除不合法字符)
|
||||
static String sanitizeFileName(String fileName) {
|
||||
// 移除或替换不合法的文件名字符
|
||||
return fileName
|
||||
.replaceAll(RegExp(r'[<>:"/\\|?*]'), '_')
|
||||
.replaceAll(RegExp(r'\s+'), '_')
|
||||
.replaceAll(RegExp(r'_{2,}'), '_')
|
||||
.replaceAll(RegExp(r'^_|_$'), '');
|
||||
}
|
||||
|
||||
/// 生成唯一文件名
|
||||
static Future<String> generateUniqueFileName(
|
||||
String basePath, String fileName) async {
|
||||
final directory = Directory(basePath);
|
||||
final extension = getFileExtension(fileName);
|
||||
final nameWithoutExt = getFileNameWithoutExtension(fileName);
|
||||
|
||||
String uniqueName = fileName;
|
||||
int counter = 1;
|
||||
|
||||
while (await File(path.join(basePath, uniqueName)).exists()) {
|
||||
uniqueName = '${nameWithoutExt}_$counter$extension';
|
||||
counter++;
|
||||
}
|
||||
|
||||
return uniqueName;
|
||||
}
|
||||
|
||||
/// 批量操作文件
|
||||
static Future<void> batchOperation(
|
||||
List<String> filePaths,
|
||||
Future<void> Function(String filePath) operation,
|
||||
) async {
|
||||
for (final filePath in filePaths) {
|
||||
try {
|
||||
await operation(filePath);
|
||||
} catch (e) {
|
||||
print('批量操作失败: $filePath - $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 查找文件
|
||||
static Future<List<String>> findFiles(
|
||||
String searchPath,
|
||||
String pattern, {
|
||||
bool recursive = false,
|
||||
}) async {
|
||||
try {
|
||||
final directory = Directory(searchPath);
|
||||
if (!await directory.exists()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final regex = RegExp(pattern);
|
||||
final foundFiles = <String>[];
|
||||
|
||||
await for (final entity in directory.list(recursive: recursive)) {
|
||||
if (entity is File) {
|
||||
final fileName = getFileName(entity.path);
|
||||
if (regex.hasMatch(fileName)) {
|
||||
foundFiles.add(entity.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return foundFiles;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取文件修改时间
|
||||
static Future<DateTime?> getFileModifiedTime(String filePath) async {
|
||||
try {
|
||||
final file = File(filePath);
|
||||
if (!await file.exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final stat = await file.stat();
|
||||
return stat.modified;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 比较文件修改时间
|
||||
static Future<bool> isFileNewer(String filePath1, String filePath2) async {
|
||||
final time1 = await getFileModifiedTime(filePath1);
|
||||
final time2 = await getFileModifiedTime(filePath2);
|
||||
|
||||
if (time1 == null || time2 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return time1.isAfter(time2);
|
||||
}
|
||||
|
||||
/// 计算文件哈希
|
||||
static Future<String?> calculateFileHash(String filePath) async {
|
||||
try {
|
||||
final file = File(filePath);
|
||||
if (!await file.exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final bytes = await file.readAsBytes();
|
||||
return bytes.hashCode.toString();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 格式化文件大小
|
||||
static String formatFileSize(int bytes) {
|
||||
if (bytes < 1024) {
|
||||
return '${bytes}B';
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
return '${(bytes / 1024).toStringAsFixed(1)}KB';
|
||||
} else if (bytes < 1024 * 1024 * 1024) {
|
||||
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)}MB';
|
||||
} else {
|
||||
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)}GB';
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建临时文件
|
||||
static Future<File> createTempFile(String prefix, {String? suffix}) async {
|
||||
final tempDir = Directory.systemTemp;
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
final fileName = '$prefix$timestamp${suffix ?? ''}';
|
||||
|
||||
return File(path.join(tempDir.path, fileName));
|
||||
}
|
||||
|
||||
/// 清理临时文件
|
||||
static Future<void> cleanupTempFiles(String pattern) async {
|
||||
try {
|
||||
final tempDir = Directory.systemTemp;
|
||||
final regex = RegExp(pattern);
|
||||
|
||||
await for (final entity in tempDir.list()) {
|
||||
if (entity is File) {
|
||||
final fileName = getFileName(entity.path);
|
||||
if (regex.hasMatch(fileName)) {
|
||||
await entity.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('清理临时文件失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取项目根目录下的generator目录路径(兼容旧版本)
|
||||
static String getProjectRootGeneratorDir() {
|
||||
final currentDir = Directory.current.path;
|
||||
return joinPath([currentDir, 'generator']);
|
||||
}
|
||||
|
||||
/// 安全地写入文件(兼容旧版本)
|
||||
static Future<void> writeFile(String path, String content) async {
|
||||
await safeWriteFile(path, content);
|
||||
}
|
||||
|
||||
/// 获取目录中的文件列表(兼容旧版本)
|
||||
static Future<List<FileSystemEntity>> listDirectory(String path) async {
|
||||
try {
|
||||
final directory = Directory(path);
|
||||
if (!await directory.exists()) {
|
||||
return [];
|
||||
}
|
||||
return await directory.list().toList();
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,335 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
/// 性能监控器
|
||||
/// 用于监控和记录应用程序性能指标
|
||||
class PerformanceMonitor {
|
||||
static final Logger _logger = Logger('PerformanceMonitor');
|
||||
|
||||
final Map<String, PerformanceMetric> _metrics = {};
|
||||
final Map<String, List<Duration>> _measurements = {};
|
||||
final bool _enabled;
|
||||
|
||||
PerformanceMonitor({bool enabled = true}) : _enabled = enabled {
|
||||
if (_enabled) {
|
||||
_logger.info('性能监控器已启用');
|
||||
}
|
||||
}
|
||||
|
||||
/// 测量异步操作的执行时间
|
||||
Future<T> measure<T>(
|
||||
String operationName, Future<T> Function() operation) async {
|
||||
if (!_enabled) {
|
||||
return await operation();
|
||||
}
|
||||
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
try {
|
||||
final result = await operation();
|
||||
stopwatch.stop();
|
||||
|
||||
_recordMeasurement(operationName, stopwatch.elapsed);
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
stopwatch.stop();
|
||||
_recordMeasurement(operationName, stopwatch.elapsed, error: e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 测量同步操作的执行时间
|
||||
T measureSync<T>(String operationName, T Function() operation) {
|
||||
if (!_enabled) {
|
||||
return operation();
|
||||
}
|
||||
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
try {
|
||||
final result = operation();
|
||||
stopwatch.stop();
|
||||
|
||||
_recordMeasurement(operationName, stopwatch.elapsed);
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
stopwatch.stop();
|
||||
_recordMeasurement(operationName, stopwatch.elapsed, error: e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 开始计时
|
||||
PerformanceTimer startTimer(String operationName) {
|
||||
return PerformanceTimer(operationName, this);
|
||||
}
|
||||
|
||||
/// 记录测量结果
|
||||
void _recordMeasurement(String operationName, Duration duration,
|
||||
{dynamic error}) {
|
||||
if (!_enabled) return;
|
||||
|
||||
// 记录到测量历史中
|
||||
_measurements.putIfAbsent(operationName, () => []);
|
||||
_measurements[operationName]!.add(duration);
|
||||
|
||||
// 更新或创建性能指标
|
||||
if (_metrics.containsKey(operationName)) {
|
||||
_metrics[operationName]!.addMeasurement(duration, error: error);
|
||||
} else {
|
||||
_metrics[operationName] = PerformanceMetric(operationName);
|
||||
_metrics[operationName]!.addMeasurement(duration, error: error);
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
final status = error != null ? 'ERROR' : 'SUCCESS';
|
||||
_logger.info('$operationName: ${duration.inMilliseconds}ms [$status]');
|
||||
}
|
||||
|
||||
/// 获取所有性能指标
|
||||
Map<String, PerformanceMetric> getMetrics() {
|
||||
return Map.unmodifiable(_metrics);
|
||||
}
|
||||
|
||||
/// 获取特定操作的性能指标
|
||||
PerformanceMetric? getMetric(String operationName) {
|
||||
return _metrics[operationName];
|
||||
}
|
||||
|
||||
/// 获取性能报告
|
||||
PerformanceReport generateReport() {
|
||||
final totalOperations =
|
||||
_metrics.values.fold(0, (sum, metric) => sum + metric.count);
|
||||
final totalTime = _metrics.values.fold(
|
||||
Duration.zero,
|
||||
(sum, metric) => sum + metric.totalTime,
|
||||
);
|
||||
|
||||
return PerformanceReport(
|
||||
totalOperations: totalOperations,
|
||||
totalTime: totalTime,
|
||||
metrics: Map.from(_metrics),
|
||||
generatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 清空所有指标
|
||||
void clear() {
|
||||
_metrics.clear();
|
||||
_measurements.clear();
|
||||
}
|
||||
|
||||
/// 获取慢操作(执行时间超过阈值)
|
||||
List<SlowOperation> getSlowOperations(
|
||||
[Duration threshold = const Duration(milliseconds: 500)]) {
|
||||
final slowOps = <SlowOperation>[];
|
||||
|
||||
for (final metric in _metrics.values) {
|
||||
if (metric.maxTime >= threshold) {
|
||||
slowOps.add(SlowOperation(
|
||||
name: metric.operationName,
|
||||
maxTime: metric.maxTime,
|
||||
avgTime: metric.averageTime,
|
||||
count: metric.count,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// 按最大执行时间排序
|
||||
slowOps.sort((a, b) => b.maxTime.compareTo(a.maxTime));
|
||||
|
||||
return slowOps;
|
||||
}
|
||||
|
||||
/// 导出性能数据到文件
|
||||
Future<void> exportToFile(String filePath) async {
|
||||
if (!_enabled) return;
|
||||
|
||||
try {
|
||||
final report = generateReport();
|
||||
final data = {
|
||||
'generatedAt': report.generatedAt.toIso8601String(),
|
||||
'summary': {
|
||||
'totalOperations': report.totalOperations,
|
||||
'totalTime': report.totalTime.inMilliseconds,
|
||||
},
|
||||
'metrics': report.metrics.map((key, value) => MapEntry(key, {
|
||||
'operationName': value.operationName,
|
||||
'count': value.count,
|
||||
'totalTime': value.totalTime.inMilliseconds,
|
||||
'averageTime': value.averageTime.inMilliseconds,
|
||||
'minTime': value.minTime.inMilliseconds,
|
||||
'maxTime': value.maxTime.inMilliseconds,
|
||||
'errorCount': value.errorCount,
|
||||
'lastExecuted': value.lastExecuted?.toIso8601String(),
|
||||
})),
|
||||
};
|
||||
|
||||
final file = File(filePath);
|
||||
await file.writeAsString(json.encode(data));
|
||||
|
||||
_logger.info('性能数据已导出到: $filePath');
|
||||
} catch (e) {
|
||||
_logger.severe('导出性能数据失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 打印性能摘要
|
||||
void printSummary() {
|
||||
if (!_enabled) {
|
||||
print('性能监控器已禁用');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_metrics.isEmpty) {
|
||||
print('没有性能数据');
|
||||
return;
|
||||
}
|
||||
|
||||
print('\n🔍 性能监控摘要:');
|
||||
print('=' * 50);
|
||||
|
||||
final sortedMetrics = _metrics.values.toList()
|
||||
..sort((a, b) => b.totalTime.compareTo(a.totalTime));
|
||||
|
||||
for (final metric in sortedMetrics) {
|
||||
print('${metric.operationName}:');
|
||||
print(' 执行次数: ${metric.count}');
|
||||
print(' 总时间: ${metric.totalTime.inMilliseconds}ms');
|
||||
print(' 平均时间: ${metric.averageTime.inMilliseconds}ms');
|
||||
print(' 最小时间: ${metric.minTime.inMilliseconds}ms');
|
||||
print(' 最大时间: ${metric.maxTime.inMilliseconds}ms');
|
||||
if (metric.errorCount > 0) {
|
||||
print(' 错误次数: ${metric.errorCount}');
|
||||
}
|
||||
print('');
|
||||
}
|
||||
|
||||
final slowOps = getSlowOperations();
|
||||
if (slowOps.isNotEmpty) {
|
||||
print('🐌 慢操作 (>500ms):');
|
||||
for (final op in slowOps.take(5)) {
|
||||
print(' ${op.name}: ${op.maxTime.inMilliseconds}ms (最大)');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 性能计时器
|
||||
class PerformanceTimer {
|
||||
final String operationName;
|
||||
final PerformanceMonitor monitor;
|
||||
final Stopwatch _stopwatch;
|
||||
|
||||
PerformanceTimer(this.operationName, this.monitor)
|
||||
: _stopwatch = Stopwatch()..start();
|
||||
|
||||
/// 停止计时并记录结果
|
||||
void stop({dynamic error}) {
|
||||
if (!_stopwatch.isRunning) return;
|
||||
|
||||
_stopwatch.stop();
|
||||
monitor._recordMeasurement(operationName, _stopwatch.elapsed, error: error);
|
||||
}
|
||||
|
||||
/// 获取当前经过的时间
|
||||
Duration get elapsed => _stopwatch.elapsed;
|
||||
}
|
||||
|
||||
/// 性能指标
|
||||
class PerformanceMetric {
|
||||
final String operationName;
|
||||
int count = 0;
|
||||
Duration totalTime = Duration.zero;
|
||||
Duration minTime = Duration.zero;
|
||||
Duration maxTime = Duration.zero;
|
||||
int errorCount = 0;
|
||||
DateTime? lastExecuted;
|
||||
|
||||
PerformanceMetric(this.operationName);
|
||||
|
||||
/// 添加测量结果
|
||||
void addMeasurement(Duration duration, {dynamic error}) {
|
||||
count++;
|
||||
totalTime += duration;
|
||||
lastExecuted = DateTime.now();
|
||||
|
||||
if (error != null) {
|
||||
errorCount++;
|
||||
}
|
||||
|
||||
if (count == 1) {
|
||||
minTime = duration;
|
||||
maxTime = duration;
|
||||
} else {
|
||||
if (duration < minTime) minTime = duration;
|
||||
if (duration > maxTime) maxTime = duration;
|
||||
}
|
||||
}
|
||||
|
||||
/// 平均执行时间
|
||||
Duration get averageTime {
|
||||
if (count == 0) return Duration.zero;
|
||||
return Duration(microseconds: totalTime.inMicroseconds ~/ count);
|
||||
}
|
||||
|
||||
/// 成功率
|
||||
double get successRate {
|
||||
if (count == 0) return 0.0;
|
||||
return (count - errorCount) / count;
|
||||
}
|
||||
}
|
||||
|
||||
/// 性能报告
|
||||
class PerformanceReport {
|
||||
final int totalOperations;
|
||||
final Duration totalTime;
|
||||
final Map<String, PerformanceMetric> metrics;
|
||||
final DateTime generatedAt;
|
||||
|
||||
const PerformanceReport({
|
||||
required this.totalOperations,
|
||||
required this.totalTime,
|
||||
required this.metrics,
|
||||
required this.generatedAt,
|
||||
});
|
||||
|
||||
/// 获取最慢的操作
|
||||
PerformanceMetric? get slowestOperation {
|
||||
if (metrics.isEmpty) return null;
|
||||
|
||||
return metrics.values.reduce((a, b) => a.maxTime > b.maxTime ? a : b);
|
||||
}
|
||||
|
||||
/// 获取最频繁的操作
|
||||
PerformanceMetric? get mostFrequentOperation {
|
||||
if (metrics.isEmpty) return null;
|
||||
|
||||
return metrics.values.reduce((a, b) => a.count > b.count ? a : b);
|
||||
}
|
||||
|
||||
/// 获取平均执行时间
|
||||
Duration get averageExecutionTime {
|
||||
if (totalOperations == 0) return Duration.zero;
|
||||
return Duration(microseconds: totalTime.inMicroseconds ~/ totalOperations);
|
||||
}
|
||||
}
|
||||
|
||||
/// 慢操作信息
|
||||
class SlowOperation {
|
||||
final String name;
|
||||
final Duration maxTime;
|
||||
final Duration avgTime;
|
||||
final int count;
|
||||
|
||||
const SlowOperation({
|
||||
required this.name,
|
||||
required this.maxTime,
|
||||
required this.avgTime,
|
||||
required this.count,
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,343 @@
|
|||
/// 字符串工具类
|
||||
///
|
||||
/// 提供常用的字符串处理功能,包括命名风格转换、注释生成等。
|
||||
///
|
||||
/// # 典型用法示例
|
||||
/// ```dart
|
||||
/// // snake_case 转 camelCase
|
||||
/// StringUtils.toDartPropertyName('user_id'); // userId
|
||||
/// // 含特殊字符
|
||||
/// StringUtils.toDartPropertyName('user-id'); // userId
|
||||
/// // 数字开头
|
||||
/// StringUtils.toDartPropertyName('1st_field'); // n1stField
|
||||
/// // 空字符串
|
||||
/// StringUtils.toDartPropertyName(''); // property
|
||||
/// ```
|
||||
///
|
||||
/// This utility provides string conversion helpers for code generation, such as
|
||||
/// converting snake_case to camelCase, generating Dart class names, and cleaning descriptions.
|
||||
///
|
||||
library;
|
||||
|
||||
import '../core/models.dart';
|
||||
|
||||
class StringUtils {
|
||||
/// 转换为camelCase
|
||||
static String toCamelCase(String input) {
|
||||
if (input.isEmpty) return input;
|
||||
|
||||
final parts = input.split('_').where((p) => p.isNotEmpty).toList();
|
||||
if (parts.isEmpty) return input;
|
||||
|
||||
String result = parts.first.toLowerCase();
|
||||
for (int i = 1; i < parts.length; i++) {
|
||||
final part = parts[i];
|
||||
if (part.isNotEmpty) {
|
||||
result += part[0].toUpperCase() + part.substring(1).toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
return result.isEmpty ? input : result;
|
||||
}
|
||||
|
||||
/// 转换为PascalCase
|
||||
static String toPascalCase(String input) {
|
||||
if (input.isEmpty) return input;
|
||||
|
||||
// 如果输入包含下划线,先按下划线分割并转换每个部分
|
||||
if (input.contains('_')) {
|
||||
final parts = input.split('_');
|
||||
String result = '';
|
||||
for (final part in parts) {
|
||||
if (part.isNotEmpty) {
|
||||
// 保持每个部分的原始大小写,只确保首字母大写
|
||||
if (part[0].toUpperCase() == part[0]) {
|
||||
// 如果首字母已经是大写,保持整个部分不变
|
||||
result += part;
|
||||
} else {
|
||||
// 如果首字母是小写,只转换首字母为大写
|
||||
result += part[0].toUpperCase() + part.substring(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.isEmpty ? input : result;
|
||||
}
|
||||
|
||||
// 如果输入已经是PascalCase格式(没有下划线且首字母大写),直接返回
|
||||
if (input[0].toUpperCase() == input[0]) {
|
||||
return input;
|
||||
}
|
||||
|
||||
// 如果输入是camelCase格式(没有下划线),转换首字母为大写
|
||||
if (RegExp(r'^[a-zA-Z][a-zA-Z0-9]*$').hasMatch(input)) {
|
||||
return input[0].toUpperCase() + input.substring(1);
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
/// 转换为snake_case
|
||||
static String toSnakeCase(String input) {
|
||||
if (input.isEmpty) return input;
|
||||
|
||||
// 处理PascalCase和camelCase
|
||||
final result =
|
||||
input.replaceAllMapped(RegExp(r'([A-Z]+)([A-Z][a-z])'), (match) {
|
||||
return '${match[1]!.substring(0, match[1]!.length - 1)}_${match[2]}';
|
||||
}).replaceAllMapped(RegExp(r'([a-z\d])([A-Z])'), (match) {
|
||||
return '${match[1]}_${match[2]}';
|
||||
}).toLowerCase();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 转换为符合Dart命名规范的属性名
|
||||
///
|
||||
/// - 支持 snake_case、kebab-case、空格、特殊字符自动转为 camelCase
|
||||
/// - 已经是驼峰命名的字符串保持不变
|
||||
/// - 数字开头自动加前缀 n
|
||||
/// - 空字符串返回 'property'
|
||||
///
|
||||
/// # 示例
|
||||
/// ```dart
|
||||
/// StringUtils.toDartPropertyName('user_id'); // userId
|
||||
/// StringUtils.toDartPropertyName('user-id'); // userId
|
||||
/// StringUtils.toDartPropertyName('1st_field'); // n1stField
|
||||
/// StringUtils.toDartPropertyName('classCadreId'); // classCadreId
|
||||
/// StringUtils.toDartPropertyName(''); // property
|
||||
/// ```
|
||||
static String toDartPropertyName(String propName) {
|
||||
// 如果已经是驼峰命名(没有下划线且首字母小写),直接返回
|
||||
if (RegExp(r'^[a-z][a-zA-Z0-9]*$').hasMatch(propName)) {
|
||||
return propName;
|
||||
}
|
||||
|
||||
// 处理特殊字符和数字开头的情况
|
||||
String result = propName;
|
||||
|
||||
// 如果以数字开头,添加前缀
|
||||
if (RegExp(r'^[0-9]').hasMatch(result)) {
|
||||
result = 'n$result';
|
||||
}
|
||||
|
||||
// 替换特殊字符为下划线
|
||||
result = result.replaceAll(RegExp(r'[^a-zA-Z0-9_]'), '_');
|
||||
|
||||
// 转换为camelCase
|
||||
result = toCamelCase(result);
|
||||
|
||||
// 确保不为空
|
||||
if (result.isEmpty) {
|
||||
result = 'property';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 清理描述文本,移除特殊字符和格式化多行注释
|
||||
static String cleanDescription(String description) {
|
||||
if (description.isEmpty) return description;
|
||||
|
||||
// 移除多余的空白字符和换行符
|
||||
String cleaned = description
|
||||
.replaceAll(RegExp(r'\s+'), ' ')
|
||||
.replaceAll(RegExp(r'[\r\n]+'), ' ')
|
||||
.trim();
|
||||
|
||||
// 移除可能引起语法错误的特殊字符
|
||||
cleaned = cleaned
|
||||
.replaceAll(RegExp(r'[^\w\s\u4e00-\u9fa5()(),,.。::;;!!??_/\\-]'), '')
|
||||
.replaceAll(RegExp(r'\s+'), ' ')
|
||||
.trim();
|
||||
|
||||
// 如果描述过长,截取前200个字符
|
||||
if (cleaned.length > 200) {
|
||||
cleaned = '${cleaned.substring(0, 200)}...';
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/// 生成端点名称
|
||||
static String generateEndpointName(String path, String? operationId) {
|
||||
// 如果有operationId,优先使用
|
||||
if (operationId != null && operationId.isNotEmpty) {
|
||||
return toCamelCase(operationId);
|
||||
}
|
||||
|
||||
// 移除路径中的版本前缀
|
||||
String cleanPath = path.replaceFirst('/api/v1', '');
|
||||
|
||||
// 移除开头的斜杠
|
||||
if (cleanPath.startsWith('/')) {
|
||||
cleanPath = cleanPath.substring(1);
|
||||
}
|
||||
|
||||
// 将路径转换为camelCase
|
||||
final parts = cleanPath.split('/');
|
||||
if (parts.length >= 2) {
|
||||
final controller = parts[0];
|
||||
final action = parts[1];
|
||||
|
||||
// 特殊情况处理
|
||||
if (controller.toLowerCase() == 'login' &&
|
||||
action.toLowerCase() == 'userlogin') {
|
||||
return 'login';
|
||||
}
|
||||
|
||||
// 转换为camelCase
|
||||
return toCamelCase('${controller}_$action');
|
||||
}
|
||||
|
||||
return toCamelCase(cleanPath.replaceAll('/', '_'));
|
||||
}
|
||||
|
||||
/// 生成Dart类名
|
||||
static String generateClassName(String name) {
|
||||
// 确保类名以大写字母开头
|
||||
final cleanName = name.replaceAll(RegExp(r'[^a-zA-Z0-9_]'), '_');
|
||||
return toPascalCase(cleanName);
|
||||
}
|
||||
|
||||
/// 生成文件名
|
||||
static String generateFileName(String name) {
|
||||
// 转换为snake_case并添加.dart扩展名
|
||||
return '${toSnakeCase(name)}.dart';
|
||||
}
|
||||
|
||||
/// 验证是否为有效的Dart标识符
|
||||
static bool isValidDartIdentifier(String identifier) {
|
||||
if (identifier.isEmpty) return false;
|
||||
|
||||
// Dart标识符规则:以字母或下划线开头,后面可以是字母、数字或下划线
|
||||
final regex = RegExp(r'^[a-zA-Z_][a-zA-Z0-9_]*$');
|
||||
return regex.hasMatch(identifier);
|
||||
}
|
||||
|
||||
/// 生成枚举值名称
|
||||
static String generateEnumValueName(dynamic value, int index) {
|
||||
if (value is String) {
|
||||
// 尝试从字符串生成合法的枚举名
|
||||
final cleanValue = value.replaceAll(RegExp(r'[^a-zA-Z0-9_]'), '');
|
||||
if (cleanValue.isNotEmpty && isValidDartIdentifier(cleanValue)) {
|
||||
return toCamelCase(cleanValue);
|
||||
}
|
||||
}
|
||||
|
||||
// 默认使用 value + 索引
|
||||
return 'value${index + 1}';
|
||||
}
|
||||
|
||||
/// 提取控制器名称
|
||||
static String extractControllerName(ApiPath path) {
|
||||
// 从tags中提取控制器名称
|
||||
if (path.tags.isNotEmpty) {
|
||||
return path.tags.first;
|
||||
}
|
||||
|
||||
// 从路径中提取控制器名称
|
||||
final pathParts = path.path.split('/');
|
||||
if (pathParts.length > 1) {
|
||||
return toPascalCase(pathParts[1]);
|
||||
}
|
||||
|
||||
return 'General';
|
||||
}
|
||||
|
||||
/// 清理路径,移除版本前缀
|
||||
static String cleanPath(String path) {
|
||||
// 移除版本前缀
|
||||
return path.replaceFirst(RegExp(r'^/api/v\d+'), '');
|
||||
}
|
||||
|
||||
/// 格式化字节大小
|
||||
static String formatBytes(int bytes) {
|
||||
if (bytes < 1024) return '${bytes}B';
|
||||
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)}KB';
|
||||
if (bytes < 1024 * 1024 * 1024)
|
||||
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)}MB';
|
||||
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)}GB';
|
||||
}
|
||||
|
||||
/// 格式化持续时间
|
||||
static String formatDuration(Duration duration) {
|
||||
if (duration.inMilliseconds >= 1000) {
|
||||
return '${(duration.inMilliseconds / 1000).toStringAsFixed(2)}秒';
|
||||
} else {
|
||||
return '${duration.inMilliseconds}毫秒';
|
||||
}
|
||||
}
|
||||
|
||||
/// 转义字符串中的特殊字符
|
||||
String escapeString(String input) {
|
||||
return input
|
||||
.replaceAll('\\', '\\\\')
|
||||
.replaceAll('"', '\\"')
|
||||
.replaceAll('\n', '\\n')
|
||||
.replaceAll('\r', '\\r')
|
||||
.replaceAll('\t', '\\t');
|
||||
}
|
||||
|
||||
/// 缩进文本
|
||||
String indent(String text, int spaces) {
|
||||
final indentation = ' ' * spaces;
|
||||
return text.split('\n').map((line) => '$indentation$line').join('\n');
|
||||
}
|
||||
|
||||
/// 生成注释块
|
||||
static String generateComment(String text, {bool isBlock = false}) {
|
||||
if (text.isEmpty) return '';
|
||||
|
||||
final cleanText = cleanDescription(text);
|
||||
|
||||
if (isBlock) {
|
||||
return '/**\n * $cleanText\n */';
|
||||
} else {
|
||||
return '/// $cleanText';
|
||||
}
|
||||
}
|
||||
|
||||
/// pluralize 单词
|
||||
String pluralize(String word) {
|
||||
if (word.isEmpty) return word;
|
||||
|
||||
final lowerWord = word.toLowerCase();
|
||||
|
||||
// 特殊复数形式
|
||||
const irregularPlurals = {
|
||||
'child': 'children',
|
||||
'person': 'people',
|
||||
'man': 'men',
|
||||
'woman': 'women',
|
||||
'mouse': 'mice',
|
||||
'goose': 'geese',
|
||||
};
|
||||
|
||||
if (irregularPlurals.containsKey(lowerWord)) {
|
||||
return irregularPlurals[lowerWord]!;
|
||||
}
|
||||
|
||||
// 规则复数形式
|
||||
if (lowerWord.endsWith('y')) {
|
||||
return '${word.substring(0, word.length - 1)}ies';
|
||||
} else if (lowerWord.endsWith('s') ||
|
||||
lowerWord.endsWith('sh') ||
|
||||
lowerWord.endsWith('ch') ||
|
||||
lowerWord.endsWith('x') ||
|
||||
lowerWord.endsWith('z')) {
|
||||
return '${word}es';
|
||||
} else {
|
||||
return '${word}s';
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成文件头注释
|
||||
static String generateFileHeader(String description, String source) {
|
||||
final timestamp = DateTime.now().toIso8601String();
|
||||
return '''// $description
|
||||
// 基于 Swagger API 文档:
|
||||
// 自动生成于: $timestamp by Max
|
||||
// Copyright (C) 2025 YuanXuan. All rights reserved.
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,632 @@
|
|||
import '../core/models.dart';
|
||||
|
||||
/// 类型验证器
|
||||
/// 提供严格的类型检查和验证功能
|
||||
class TypeValidator {
|
||||
/// 验证API模型
|
||||
static ValidationResult validateApiModel(ApiModel model) {
|
||||
final errors = <ValidationError>[];
|
||||
final warnings = <String>[];
|
||||
|
||||
// 验证模型名称
|
||||
if (!_isValidDartIdentifier(model.name)) {
|
||||
errors.add(
|
||||
ValidationError(
|
||||
field: 'name',
|
||||
message: '模型名称不符合Dart命名规范: ${model.name}',
|
||||
severity: ErrorSeverity.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 验证枚举类型
|
||||
if (model.isEnum) {
|
||||
final enumValidation = _validateEnum(model);
|
||||
errors.addAll(enumValidation.errors);
|
||||
warnings.addAll(enumValidation.warnings);
|
||||
} else {
|
||||
// 验证类属性
|
||||
final propertiesValidation = _validateProperties(model.properties);
|
||||
errors.addAll(propertiesValidation.errors);
|
||||
warnings.addAll(propertiesValidation.warnings);
|
||||
}
|
||||
|
||||
return ValidationResult(
|
||||
isValid: errors.isEmpty,
|
||||
errors: errors,
|
||||
warnings: warnings,
|
||||
);
|
||||
}
|
||||
|
||||
/// 验证API属性
|
||||
static ValidationResult validateApiProperty(
|
||||
String propertyName,
|
||||
ApiProperty property,
|
||||
) {
|
||||
final errors = <ValidationError>[];
|
||||
final warnings = <String>[];
|
||||
|
||||
// 验证属性名称
|
||||
if (!_isValidDartIdentifier(propertyName)) {
|
||||
errors.add(
|
||||
ValidationError(
|
||||
field: 'name',
|
||||
message: '属性名称不符合Dart命名规范: $propertyName',
|
||||
severity: ErrorSeverity.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 验证类型
|
||||
if (!_isValidPropertyType(property.type)) {
|
||||
errors.add(
|
||||
ValidationError(
|
||||
field: 'type',
|
||||
message: '不支持的属性类型: ${property.type}',
|
||||
severity: ErrorSeverity.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 验证引用类型
|
||||
if (property.type == PropertyType.reference) {
|
||||
if (property.reference == null || property.reference!.isEmpty) {
|
||||
errors.add(
|
||||
ValidationError(
|
||||
field: 'reference',
|
||||
message: '引用类型缺少引用目标',
|
||||
severity: ErrorSeverity.error,
|
||||
),
|
||||
);
|
||||
} else if (!_isValidDartIdentifier(property.reference!)) {
|
||||
errors.add(
|
||||
ValidationError(
|
||||
field: 'reference',
|
||||
message: '引用目标不符合Dart命名规范: ${property.reference}',
|
||||
severity: ErrorSeverity.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 验证可空性和必填性的逻辑
|
||||
if (property.required && property.nullable) {
|
||||
warnings.add('属性 $propertyName 同时标记为必填和可空,这可能导致逻辑冲突');
|
||||
}
|
||||
|
||||
// 验证日期格式
|
||||
if (property.type == PropertyType.string &&
|
||||
(property.format == 'date' || property.format == 'date-time')) {
|
||||
// 日期类型的特殊验证
|
||||
if (property.example != null &&
|
||||
!_isValidDateFormat(property.example.toString())) {
|
||||
warnings.add('属性 $propertyName 的示例值不符合日期格式');
|
||||
}
|
||||
}
|
||||
|
||||
return ValidationResult(
|
||||
isValid: errors.isEmpty,
|
||||
errors: errors,
|
||||
warnings: warnings,
|
||||
);
|
||||
}
|
||||
|
||||
/// 验证API路径
|
||||
static ValidationResult validateApiPath(ApiPath path) {
|
||||
final errors = <ValidationError>[];
|
||||
final warnings = <String>[];
|
||||
|
||||
// 验证路径格式
|
||||
if (!path.path.startsWith('/')) {
|
||||
errors.add(
|
||||
ValidationError(
|
||||
field: 'path',
|
||||
message: 'API路径必须以/开头: ${path.path}',
|
||||
severity: ErrorSeverity.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 验证操作ID
|
||||
if (path.operationId.isNotEmpty &&
|
||||
!_isValidDartIdentifier(path.operationId)) {
|
||||
warnings.add('操作ID不符合Dart命名规范: ${path.operationId}');
|
||||
}
|
||||
|
||||
// 验证参数
|
||||
for (final param in path.parameters) {
|
||||
final paramValidation = _validateParameter(param);
|
||||
errors.addAll(paramValidation.errors);
|
||||
warnings.addAll(paramValidation.warnings);
|
||||
}
|
||||
|
||||
// 验证路径参数一致性
|
||||
final pathParams = _extractPathParameters(path.path);
|
||||
final definedPathParams = path.parameters
|
||||
.where((p) => p.location == ParameterLocation.path)
|
||||
.map((p) => p.name)
|
||||
.toSet();
|
||||
|
||||
for (final pathParam in pathParams) {
|
||||
if (!definedPathParams.contains(pathParam)) {
|
||||
errors.add(
|
||||
ValidationError(
|
||||
field: 'parameters',
|
||||
message: '路径参数 $pathParam 在路径中定义但未在参数列表中声明',
|
||||
severity: ErrorSeverity.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return ValidationResult(
|
||||
isValid: errors.isEmpty,
|
||||
errors: errors,
|
||||
warnings: warnings,
|
||||
);
|
||||
}
|
||||
|
||||
/// 验证Swagger文档
|
||||
static ValidationResult validateSwaggerDocument(SwaggerDocument document) {
|
||||
final errors = <ValidationError>[];
|
||||
final warnings = <String>[];
|
||||
|
||||
// 验证基本信息
|
||||
if (document.title.isEmpty) {
|
||||
warnings.add('文档标题为空');
|
||||
}
|
||||
|
||||
if (document.version.isEmpty) {
|
||||
warnings.add('文档版本为空');
|
||||
}
|
||||
|
||||
// 验证模型
|
||||
for (final model in document.models.values) {
|
||||
final modelValidation = validateApiModel(model);
|
||||
errors.addAll(modelValidation.errors);
|
||||
warnings.addAll(modelValidation.warnings);
|
||||
}
|
||||
|
||||
// 验证路径
|
||||
for (final path in document.paths.values) {
|
||||
final pathValidation = validateApiPath(path);
|
||||
errors.addAll(pathValidation.errors);
|
||||
warnings.addAll(pathValidation.warnings);
|
||||
}
|
||||
|
||||
// 验证引用完整性
|
||||
final referenceValidation = _validateReferences(document);
|
||||
errors.addAll(referenceValidation.errors);
|
||||
warnings.addAll(referenceValidation.warnings);
|
||||
|
||||
return ValidationResult(
|
||||
isValid: errors.isEmpty,
|
||||
errors: errors,
|
||||
warnings: warnings,
|
||||
);
|
||||
}
|
||||
|
||||
/// 验证生成的代码
|
||||
static ValidationResult validateGeneratedCode(
|
||||
String code,
|
||||
CodeType codeType,
|
||||
) {
|
||||
final errors = <ValidationError>[];
|
||||
final warnings = <String>[];
|
||||
|
||||
// 基础语法检查
|
||||
if (code.trim().isEmpty) {
|
||||
errors.add(
|
||||
ValidationError(
|
||||
field: 'code',
|
||||
message: '生成的代码为空',
|
||||
severity: ErrorSeverity.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 检查括号匹配
|
||||
if (!_isBalancedBrackets(code)) {
|
||||
errors.add(
|
||||
ValidationError(
|
||||
field: 'syntax',
|
||||
message: '代码中存在不匹配的括号',
|
||||
severity: ErrorSeverity.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 检查基本Dart语法要素
|
||||
switch (codeType) {
|
||||
case CodeType.model:
|
||||
if (!code.contains('class ') && !code.contains('enum ')) {
|
||||
errors.add(
|
||||
ValidationError(
|
||||
field: 'content',
|
||||
message: '模型代码必须包含class或enum声明',
|
||||
severity: ErrorSeverity.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case CodeType.endpoints:
|
||||
if (!code.contains('class ')) {
|
||||
errors.add(
|
||||
ValidationError(
|
||||
field: 'content',
|
||||
message: '端点代码必须包含class声明',
|
||||
severity: ErrorSeverity.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case CodeType.documentation:
|
||||
if (!code.contains('#')) {
|
||||
warnings.add('文档代码似乎不包含Markdown标题');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 检查潜在的命名冲突
|
||||
final identifiers = _extractIdentifiers(code);
|
||||
final duplicates = _findDuplicates(identifiers);
|
||||
if (duplicates.isNotEmpty) {
|
||||
warnings.add('检测到可能的命名冲突: ${duplicates.join(', ')}');
|
||||
}
|
||||
|
||||
return ValidationResult(
|
||||
isValid: errors.isEmpty,
|
||||
errors: errors,
|
||||
warnings: warnings,
|
||||
);
|
||||
}
|
||||
|
||||
/// 验证枚举
|
||||
static ValidationResult _validateEnum(ApiModel model) {
|
||||
final errors = <ValidationError>[];
|
||||
final warnings = <String>[];
|
||||
|
||||
if (model.enumValues.isEmpty) {
|
||||
errors.add(
|
||||
ValidationError(
|
||||
field: 'enumValues',
|
||||
message: '枚举类型必须包含至少一个值',
|
||||
severity: ErrorSeverity.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 验证枚举值的类型一致性
|
||||
if (model.enumValues.length > 1) {
|
||||
final firstType = model.enumValues.first.runtimeType;
|
||||
for (final value in model.enumValues) {
|
||||
if (value.runtimeType != firstType) {
|
||||
warnings.add('枚举值类型不一致');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 验证枚举值的唯一性
|
||||
final uniqueValues = model.enumValues.toSet();
|
||||
if (uniqueValues.length != model.enumValues.length) {
|
||||
errors.add(
|
||||
ValidationError(
|
||||
field: 'enumValues',
|
||||
message: '枚举值存在重复',
|
||||
severity: ErrorSeverity.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ValidationResult(
|
||||
isValid: errors.isEmpty,
|
||||
errors: errors,
|
||||
warnings: warnings,
|
||||
);
|
||||
}
|
||||
|
||||
/// 验证属性集合
|
||||
static ValidationResult _validateProperties(
|
||||
Map<String, ApiProperty> properties,
|
||||
) {
|
||||
final errors = <ValidationError>[];
|
||||
final warnings = <String>[];
|
||||
|
||||
for (final entry in properties.entries) {
|
||||
final propertyValidation = validateApiProperty(entry.key, entry.value);
|
||||
errors.addAll(propertyValidation.errors);
|
||||
warnings.addAll(propertyValidation.warnings);
|
||||
}
|
||||
|
||||
// 检查是否存在保留字冲突
|
||||
final reservedWords = _getDartReservedWords();
|
||||
for (final propertyName in properties.keys) {
|
||||
if (reservedWords.contains(propertyName.toLowerCase())) {
|
||||
warnings.add('属性名 $propertyName 是Dart保留字,可能导致编译错误');
|
||||
}
|
||||
}
|
||||
|
||||
return ValidationResult(
|
||||
isValid: errors.isEmpty,
|
||||
errors: errors,
|
||||
warnings: warnings,
|
||||
);
|
||||
}
|
||||
|
||||
/// 验证参数
|
||||
static ValidationResult _validateParameter(ApiParameter parameter) {
|
||||
final errors = <ValidationError>[];
|
||||
final warnings = <String>[];
|
||||
|
||||
// 验证参数名称
|
||||
if (!_isValidDartIdentifier(parameter.name)) {
|
||||
warnings.add('参数名称不符合Dart命名规范: ${parameter.name}');
|
||||
}
|
||||
|
||||
// 验证路径参数必须为required
|
||||
if (parameter.location == ParameterLocation.path && !parameter.required) {
|
||||
errors.add(
|
||||
ValidationError(
|
||||
field: 'required',
|
||||
message: '路径参数 ${parameter.name} 必须标记为required',
|
||||
severity: ErrorSeverity.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ValidationResult(
|
||||
isValid: errors.isEmpty,
|
||||
errors: errors,
|
||||
warnings: warnings,
|
||||
);
|
||||
}
|
||||
|
||||
/// 验证引用完整性
|
||||
static ValidationResult _validateReferences(SwaggerDocument document) {
|
||||
final errors = <ValidationError>[];
|
||||
final warnings = <String>[];
|
||||
|
||||
final modelNames = document.models.keys.toSet();
|
||||
|
||||
// 检查模型中的引用
|
||||
for (final model in document.models.values) {
|
||||
for (final property in model.properties.values) {
|
||||
if (property.type == PropertyType.reference &&
|
||||
property.reference != null) {
|
||||
if (!modelNames.contains(property.reference)) {
|
||||
errors.add(
|
||||
ValidationError(
|
||||
field: 'reference',
|
||||
message:
|
||||
'模型 ${model.name} 中的属性 ${property.name} 引用了不存在的类型: ${property.reference}',
|
||||
severity: ErrorSeverity.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ValidationResult(
|
||||
isValid: errors.isEmpty,
|
||||
errors: errors,
|
||||
warnings: warnings,
|
||||
);
|
||||
}
|
||||
|
||||
/// 检查是否为有效的Dart标识符
|
||||
static bool _isValidDartIdentifier(String identifier) {
|
||||
if (identifier.isEmpty) return false;
|
||||
|
||||
// Dart标识符规则:以字母或下划线开头,后面可以是字母、数字或下划线
|
||||
final regex = RegExp(r'^[a-zA-Z_][a-zA-Z0-9_]*$');
|
||||
return regex.hasMatch(identifier);
|
||||
}
|
||||
|
||||
/// 检查是否为有效的属性类型
|
||||
static bool _isValidPropertyType(PropertyType type) {
|
||||
// 所有枚举值都是有效的
|
||||
return true;
|
||||
}
|
||||
|
||||
/// 检查是否为有效的日期格式
|
||||
static bool _isValidDateFormat(String dateString) {
|
||||
try {
|
||||
DateTime.parse(dateString);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 从路径中提取路径参数
|
||||
static Set<String> _extractPathParameters(String path) {
|
||||
final regex = RegExp(r'\{(\w+)\}');
|
||||
final matches = regex.allMatches(path);
|
||||
return matches.map((match) => match.group(1)!).toSet();
|
||||
}
|
||||
|
||||
/// 检查括号是否平衡
|
||||
static bool _isBalancedBrackets(String code) {
|
||||
final brackets = {'{': '}', '(': ')', '[': ']'};
|
||||
final stack = <String>[];
|
||||
|
||||
for (final char in code.split('')) {
|
||||
if (brackets.containsKey(char)) {
|
||||
stack.add(char);
|
||||
} else if (brackets.containsValue(char)) {
|
||||
if (stack.isEmpty) return false;
|
||||
final last = stack.removeLast();
|
||||
if (brackets[last] != char) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return stack.isEmpty;
|
||||
}
|
||||
|
||||
/// 提取代码中的标识符
|
||||
static List<String> _extractIdentifiers(String code) {
|
||||
final regex = RegExp(r'\b[a-zA-Z_][a-zA-Z0-9_]*\b');
|
||||
final matches = regex.allMatches(code);
|
||||
return matches.map((match) => match.group(0)!).toList();
|
||||
}
|
||||
|
||||
/// 查找重复项
|
||||
static List<String> _findDuplicates(List<String> items) {
|
||||
final seen = <String>{};
|
||||
final duplicates = <String>{};
|
||||
|
||||
for (final item in items) {
|
||||
if (seen.contains(item)) {
|
||||
duplicates.add(item);
|
||||
} else {
|
||||
seen.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
return duplicates.toList();
|
||||
}
|
||||
|
||||
/// 获取Dart保留字列表
|
||||
static Set<String> _getDartReservedWords() {
|
||||
return {
|
||||
'abstract',
|
||||
'as',
|
||||
'assert',
|
||||
'async',
|
||||
'await',
|
||||
'break',
|
||||
'case',
|
||||
'catch',
|
||||
'class',
|
||||
'const',
|
||||
'continue',
|
||||
'default',
|
||||
'deferred',
|
||||
'do',
|
||||
'dynamic',
|
||||
'else',
|
||||
'enum',
|
||||
'export',
|
||||
'extends',
|
||||
'external',
|
||||
'factory',
|
||||
'false',
|
||||
'final',
|
||||
'finally',
|
||||
'for',
|
||||
'function',
|
||||
'get',
|
||||
'hide',
|
||||
'if',
|
||||
'implements',
|
||||
'import',
|
||||
'in',
|
||||
'interface',
|
||||
'is',
|
||||
'library',
|
||||
'mixin',
|
||||
'new',
|
||||
'null',
|
||||
'on',
|
||||
'operator',
|
||||
'part',
|
||||
'required',
|
||||
'rethrow',
|
||||
'return',
|
||||
'set',
|
||||
'show',
|
||||
'static',
|
||||
'super',
|
||||
'switch',
|
||||
'sync',
|
||||
'this',
|
||||
'throw',
|
||||
'true',
|
||||
'try',
|
||||
'typedef',
|
||||
'var',
|
||||
'void',
|
||||
'while',
|
||||
'with',
|
||||
'yield',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证结果
|
||||
class ValidationResult {
|
||||
final bool isValid;
|
||||
final List<ValidationError> errors;
|
||||
final List<String> warnings;
|
||||
|
||||
const ValidationResult({
|
||||
required this.isValid,
|
||||
required this.errors,
|
||||
required this.warnings,
|
||||
});
|
||||
|
||||
/// 是否有错误
|
||||
bool get hasErrors => errors.isNotEmpty;
|
||||
|
||||
/// 是否有警告
|
||||
bool get hasWarnings => warnings.isNotEmpty;
|
||||
|
||||
/// 获取关键错误
|
||||
List<ValidationError> get criticalErrors =>
|
||||
errors.where((e) => e.severity == ErrorSeverity.error).toList();
|
||||
|
||||
/// 生成验证报告
|
||||
String generateReport() {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
if (isValid) {
|
||||
buffer.writeln('✅ 验证通过');
|
||||
} else {
|
||||
buffer.writeln('❌ 验证失败');
|
||||
}
|
||||
|
||||
if (errors.isNotEmpty) {
|
||||
buffer.writeln('\n🚨 错误:');
|
||||
for (final error in errors) {
|
||||
buffer.writeln(
|
||||
'- [${error.severity.name.toUpperCase()}] ${error.field}: ${error.message}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (warnings.isNotEmpty) {
|
||||
buffer.writeln('\n⚠️ 警告:');
|
||||
for (final warning in warnings) {
|
||||
buffer.writeln('- $warning');
|
||||
}
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证错误
|
||||
class ValidationError {
|
||||
final String field;
|
||||
final String message;
|
||||
final ErrorSeverity severity;
|
||||
|
||||
const ValidationError({
|
||||
required this.field,
|
||||
required this.message,
|
||||
required this.severity,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ValidationError(field: $field, message: $message, severity: $severity)';
|
||||
}
|
||||
}
|
||||
|
||||
/// 错误严重程度
|
||||
enum ErrorSeverity { warning, error, critical }
|
||||
|
||||
/// 代码类型
|
||||
enum CodeType { model, endpoints, documentation }
|
||||
|
|
@ -0,0 +1,637 @@
|
|||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_fe_analyzer_shared:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "85.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: de617bfdc64f3d8b00835ec2957441ceca0a29cdf7881f7ab231bc14f71159c0
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.5.6"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
build:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.5.4"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_config
|
||||
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
build_daemon:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.0.4"
|
||||
build_resolvers:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_resolvers
|
||||
sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.5.4"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.5.4"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.1.2"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_collection
|
||||
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
built_value:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "8.10.1"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: checked_yaml
|
||||
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
cli_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_config
|
||||
sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
code_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_builder
|
||||
sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.10.1"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: convert
|
||||
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
coverage:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: coverage
|
||||
sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: frontend_server_client
|
||||
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: graphs
|
||||
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
json_annotation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
json_serializable:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: json_serializable
|
||||
sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.9.5"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "10.0.9"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.9"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.12.17"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.11.1"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
node_preamble:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: node_preamble
|
||||
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
pubspec_parse:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pubspec_parse
|
||||
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.2"
|
||||
shelf_packages_handler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_packages_handler
|
||||
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
shelf_static:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_static
|
||||
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_gen
|
||||
sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
source_helper:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_helper
|
||||
sha256: "4f81479fe5194a622cdd1713fe1ecb683a6e6c85cd8cec8e2e35ee5ab3fdf2a1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.6"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_map_stack_trace
|
||||
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
source_maps:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_maps
|
||||
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.10.13"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.10.1"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_transform
|
||||
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
test:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: test
|
||||
sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.25.15"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.7.4"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.6.8"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timing
|
||||
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "15.0.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket
|
||||
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
webkit_inspection_protocol:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webkit_inspection_protocol
|
||||
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.8.0 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
name: swagger_generator_flutter
|
||||
description: A Flutter project using generated API models
|
||||
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
# 推荐版本必需的依赖
|
||||
json_annotation: ^4.8.1
|
||||
http: ^1.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
# 代码生成必需的依赖
|
||||
build_runner: ^2.4.7
|
||||
json_serializable: ^6.7.1
|
||||
test: ^1.24.0
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
#!/bin/bash
|
||||
|
||||
# 简化版 Swagger CLI 运行脚本
|
||||
# 提供便捷的命令行界面
|
||||
|
||||
# 颜色定义
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 脚本路径
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CLI_DART_FILE="$SCRIPT_DIR/bin/main.dart"
|
||||
|
||||
# 显示帮助
|
||||
show_help() {
|
||||
echo -e "${CYAN}🚀 Swagger CLI 工具${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}用法: $0 [命令] [选项]${NC}"
|
||||
echo ""
|
||||
echo -e "${GREEN}快速命令:${NC}"
|
||||
echo -e " $0 all # 生成所有文件"
|
||||
echo -e " $0 models # 生成数据模型"
|
||||
echo -e " $0 endpoints # 生成API端点"
|
||||
echo -e " $0 docs # 生成API文档"
|
||||
echo -e " $0 api # 生成Retrofit API"
|
||||
echo ""
|
||||
echo -e "${GREEN}直接使用:${NC}"
|
||||
echo -e " dart run bin/main.dart generate --help"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
if [ $# -eq 0 ] || [ "$1" = "help" ] || [ "$1" = "--help" ]; then
|
||||
show_help
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case "$1" in
|
||||
all)
|
||||
dart run "$CLI_DART_FILE" generate --models --api --split-by-tags
|
||||
;;
|
||||
models)
|
||||
dart run "$CLI_DART_FILE" generate --models
|
||||
;;
|
||||
endpoints)
|
||||
dart run "$CLI_DART_FILE" generate --endpoints
|
||||
;;
|
||||
docs)
|
||||
dart run "$CLI_DART_FILE" generate --docs
|
||||
;;
|
||||
api)
|
||||
dart run "$CLI_DART_FILE" generate --api
|
||||
;;
|
||||
*)
|
||||
echo -e "${YELLOW}未知命令: $1${NC}"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import 'lib/utils/string_utils.dart';
|
||||
|
||||
void main() {
|
||||
// 测试属性名转换
|
||||
print('Testing property name conversion:');
|
||||
print('classCadreId -> ${StringUtils.toDartPropertyName('classCadreId')}');
|
||||
print('meetingTitle -> ${StringUtils.toDartPropertyName('meetingTitle')}');
|
||||
print('taskInfo -> ${StringUtils.toDartPropertyName('taskInfo')}');
|
||||
print(
|
||||
'sunTaskUserResults -> ${StringUtils.toDartPropertyName('sunTaskUserResults')}');
|
||||
print(
|
||||
'sunTaskFileResults -> ${StringUtils.toDartPropertyName('sunTaskFileResults')}');
|
||||
|
||||
// 测试下划线格式
|
||||
print('\nTesting snake_case conversion:');
|
||||
print(
|
||||
'class_cadre_id -> ${StringUtils.toDartPropertyName('class_cadre_id')}');
|
||||
print('meeting_title -> ${StringUtils.toDartPropertyName('meeting_title')}');
|
||||
print('task_info -> ${StringUtils.toDartPropertyName('task_info')}');
|
||||
}
|
||||
|
|
@ -0,0 +1,923 @@
|
|||
import 'package:test/test.dart';
|
||||
|
||||
import '../lib/core/models.dart';
|
||||
|
||||
void main() {
|
||||
group('ApiPath', () {
|
||||
test('creates ApiPath with required fields', () {
|
||||
final path = ApiPath(
|
||||
path: '/api/users',
|
||||
method: HttpMethod.get,
|
||||
summary: 'Get users',
|
||||
description: 'Retrieve all users',
|
||||
operationId: 'getUsers',
|
||||
tags: ['User'],
|
||||
parameters: [],
|
||||
responses: {},
|
||||
requestBody: null,
|
||||
);
|
||||
|
||||
expect(path.path, '/api/users');
|
||||
expect(path.method, HttpMethod.get);
|
||||
expect(path.summary, 'Get users');
|
||||
expect(path.description, 'Retrieve all users');
|
||||
expect(path.tags, ['User']);
|
||||
expect(path.parameters, isEmpty);
|
||||
expect(path.responses, isEmpty);
|
||||
expect(path.requestBody, isNull);
|
||||
});
|
||||
|
||||
test('creates ApiPath with all fields', () {
|
||||
final parameters = [
|
||||
ApiParameter(
|
||||
name: 'id',
|
||||
location: ParameterLocation.path,
|
||||
required: true,
|
||||
type: PropertyType.integer,
|
||||
description: 'User ID',
|
||||
),
|
||||
];
|
||||
|
||||
final responses = {
|
||||
'200': ApiResponse(
|
||||
code: '200',
|
||||
description: 'Success',
|
||||
schema: {'type': 'object'},
|
||||
content: null,
|
||||
),
|
||||
};
|
||||
|
||||
final requestBody = ApiRequestBody(
|
||||
description: 'User data',
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
'schema': {'type': 'object'}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
final path = ApiPath(
|
||||
path: '/api/users/{id}',
|
||||
method: HttpMethod.put,
|
||||
summary: 'Update user',
|
||||
description: 'Update user by ID',
|
||||
operationId: 'updateUser',
|
||||
tags: ['User'],
|
||||
parameters: parameters,
|
||||
responses: responses,
|
||||
requestBody: requestBody,
|
||||
);
|
||||
|
||||
expect(path.parameters, hasLength(1));
|
||||
expect(path.responses, hasLength(1));
|
||||
expect(path.requestBody, isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('ApiParameter', () {
|
||||
test('creates ApiParameter with required fields', () {
|
||||
final param = ApiParameter(
|
||||
name: 'id',
|
||||
location: ParameterLocation.path,
|
||||
required: true,
|
||||
type: PropertyType.integer,
|
||||
description: 'User ID',
|
||||
);
|
||||
|
||||
expect(param.name, 'id');
|
||||
expect(param.location, ParameterLocation.path);
|
||||
expect(param.required, true);
|
||||
expect(param.type, PropertyType.integer);
|
||||
expect(param.description, 'User ID');
|
||||
});
|
||||
|
||||
test('creates ApiParameter with optional fields', () {
|
||||
final param = ApiParameter(
|
||||
name: 'page',
|
||||
location: ParameterLocation.query,
|
||||
required: false,
|
||||
type: PropertyType.integer,
|
||||
description: 'Page number',
|
||||
format: 'int32',
|
||||
example: 1,
|
||||
defaultValue: 1,
|
||||
);
|
||||
|
||||
expect(param.format, 'int32');
|
||||
expect(param.example, 1);
|
||||
expect(param.defaultValue, 1);
|
||||
});
|
||||
|
||||
test('creates ApiParameter from JSON', () {
|
||||
final json = {
|
||||
'name': 'id',
|
||||
'in': 'path',
|
||||
'required': true,
|
||||
'type': 'integer',
|
||||
'description': 'User ID',
|
||||
'format': 'int64',
|
||||
'example': 123,
|
||||
'default': 1,
|
||||
};
|
||||
|
||||
final param = ApiParameter.fromJson(json);
|
||||
|
||||
expect(param.name, 'id');
|
||||
expect(param.location, ParameterLocation.path);
|
||||
expect(param.required, true);
|
||||
expect(param.type, PropertyType.integer);
|
||||
expect(param.description, 'User ID');
|
||||
expect(param.format, 'int64');
|
||||
expect(param.example, 123);
|
||||
expect(param.defaultValue, 1);
|
||||
});
|
||||
});
|
||||
|
||||
group('ApiResponse', () {
|
||||
test('creates ApiResponse with required fields', () {
|
||||
final response = ApiResponse(
|
||||
code: '200',
|
||||
description: 'Success',
|
||||
schema: null,
|
||||
content: null,
|
||||
);
|
||||
|
||||
expect(response.code, '200');
|
||||
expect(response.description, 'Success');
|
||||
expect(response.schema, isNull);
|
||||
expect(response.content, isNull);
|
||||
});
|
||||
|
||||
test('creates ApiResponse with schema', () {
|
||||
final schema = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'id': {'type': 'integer'},
|
||||
'name': {'type': 'string'},
|
||||
},
|
||||
};
|
||||
|
||||
final response = ApiResponse(
|
||||
code: '200',
|
||||
description: 'Success',
|
||||
schema: schema,
|
||||
content: null,
|
||||
);
|
||||
|
||||
expect(response.schema, equals(schema));
|
||||
});
|
||||
|
||||
test('creates ApiResponse from JSON', () {
|
||||
final json = {
|
||||
'description': 'Success',
|
||||
'schema': {'type': 'object'},
|
||||
'content': {
|
||||
'application/json': {
|
||||
'schema': {'type': 'object'},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
final response = ApiResponse.fromJson('200', json);
|
||||
|
||||
expect(response.code, '200');
|
||||
expect(response.description, 'Success');
|
||||
expect(response.schema, isNotNull);
|
||||
expect(response.content, isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('ApiRequestBody', () {
|
||||
test('creates ApiRequestBody with required fields', () {
|
||||
final requestBody = ApiRequestBody(
|
||||
description: 'User data',
|
||||
required: true,
|
||||
content: null,
|
||||
);
|
||||
|
||||
expect(requestBody.description, 'User data');
|
||||
expect(requestBody.required, true);
|
||||
expect(requestBody.content, isNull);
|
||||
});
|
||||
|
||||
test('creates ApiRequestBody with content', () {
|
||||
final content = {
|
||||
'application/json': {
|
||||
'schema': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'name': {'type': 'string'},
|
||||
'email': {'type': 'string'},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
final requestBody = ApiRequestBody(
|
||||
description: 'User data',
|
||||
required: true,
|
||||
content: content,
|
||||
);
|
||||
|
||||
expect(requestBody.content, equals(content));
|
||||
});
|
||||
|
||||
test('creates ApiRequestBody from JSON', () {
|
||||
final json = {
|
||||
'description': 'User data',
|
||||
'required': true,
|
||||
'content': {
|
||||
'application/json': {
|
||||
'schema': {'type': 'object'},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
final requestBody = ApiRequestBody.fromJson(json);
|
||||
|
||||
expect(requestBody.description, 'User data');
|
||||
expect(requestBody.required, true);
|
||||
expect(requestBody.content, isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('ApiModel', () {
|
||||
test('creates ApiModel with required fields', () {
|
||||
final model = ApiModel(
|
||||
name: 'User',
|
||||
description: 'User model',
|
||||
properties: {},
|
||||
required: [],
|
||||
);
|
||||
|
||||
expect(model.name, 'User');
|
||||
expect(model.description, 'User model');
|
||||
expect(model.properties, isEmpty);
|
||||
expect(model.required, isEmpty);
|
||||
expect(model.isEnum, false);
|
||||
expect(model.enumValues, isEmpty);
|
||||
expect(model.enumType, isNull);
|
||||
});
|
||||
|
||||
test('creates ApiModel with properties', () {
|
||||
final properties = {
|
||||
'id': ApiProperty(
|
||||
name: 'id',
|
||||
type: PropertyType.integer,
|
||||
description: 'User ID',
|
||||
required: true,
|
||||
),
|
||||
'name': ApiProperty(
|
||||
name: 'name',
|
||||
type: PropertyType.string,
|
||||
description: 'User name',
|
||||
required: true,
|
||||
),
|
||||
};
|
||||
|
||||
final model = ApiModel(
|
||||
name: 'User',
|
||||
description: 'User model',
|
||||
properties: properties,
|
||||
required: ['id', 'name'],
|
||||
);
|
||||
|
||||
expect(model.properties, hasLength(2));
|
||||
expect(model.required, ['id', 'name']);
|
||||
});
|
||||
|
||||
test('creates enum ApiModel', () {
|
||||
final model = ApiModel(
|
||||
name: 'UserStatus',
|
||||
description: 'User status enum',
|
||||
properties: {},
|
||||
required: [],
|
||||
isEnum: true,
|
||||
enumValues: ['active', 'inactive', 'pending'],
|
||||
enumType: PropertyType.string,
|
||||
);
|
||||
|
||||
expect(model.isEnum, true);
|
||||
expect(model.enumValues, ['active', 'inactive', 'pending']);
|
||||
expect(model.enumType, PropertyType.string);
|
||||
});
|
||||
|
||||
test('creates ApiModel from JSON', () {
|
||||
final json = {
|
||||
'description': 'User model',
|
||||
'properties': {
|
||||
'id': {
|
||||
'type': 'integer',
|
||||
'description': 'User ID',
|
||||
},
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'description': 'User name',
|
||||
},
|
||||
},
|
||||
'required': ['id', 'name'],
|
||||
};
|
||||
|
||||
final model = ApiModel.fromJson('User', json);
|
||||
|
||||
expect(model.name, 'User');
|
||||
expect(model.description, 'User model');
|
||||
expect(model.properties, hasLength(2));
|
||||
expect(model.required, ['id', 'name']);
|
||||
});
|
||||
|
||||
test('creates enum ApiModel from JSON', () {
|
||||
final json = {
|
||||
'description': 'User status enum',
|
||||
'enum': ['active', 'inactive', 'pending'],
|
||||
'type': 'string',
|
||||
};
|
||||
|
||||
final model = ApiModel.fromJson('UserStatus', json);
|
||||
|
||||
expect(model.isEnum, true);
|
||||
expect(model.enumValues, ['active', 'inactive', 'pending']);
|
||||
expect(model.enumType, PropertyType.string);
|
||||
});
|
||||
});
|
||||
|
||||
group('ApiProperty', () {
|
||||
test('creates ApiProperty with required fields', () {
|
||||
final property = ApiProperty(
|
||||
name: 'id',
|
||||
type: PropertyType.integer,
|
||||
description: 'User ID',
|
||||
required: true,
|
||||
);
|
||||
|
||||
expect(property.name, 'id');
|
||||
expect(property.type, PropertyType.integer);
|
||||
expect(property.description, 'User ID');
|
||||
expect(property.required, true);
|
||||
expect(property.nullable, false);
|
||||
});
|
||||
|
||||
test('creates ApiProperty with optional fields', () {
|
||||
final property = ApiProperty(
|
||||
name: 'name',
|
||||
type: PropertyType.string,
|
||||
description: 'User name',
|
||||
required: false,
|
||||
nullable: true,
|
||||
format: 'string',
|
||||
example: 'John Doe',
|
||||
defaultValue: 'Unknown',
|
||||
reference: 'User',
|
||||
);
|
||||
|
||||
expect(property.nullable, true);
|
||||
expect(property.format, 'string');
|
||||
expect(property.example, 'John Doe');
|
||||
expect(property.defaultValue, 'Unknown');
|
||||
expect(property.reference, 'User');
|
||||
});
|
||||
|
||||
test('creates ApiProperty from JSON', () {
|
||||
final json = {
|
||||
'type': 'string',
|
||||
'description': 'User name',
|
||||
'nullable': true,
|
||||
'format': 'string',
|
||||
'example': 'John Doe',
|
||||
'default': 'Unknown',
|
||||
};
|
||||
|
||||
final property = ApiProperty.fromJson('name', json, []);
|
||||
|
||||
expect(property.name, 'name');
|
||||
expect(property.type, PropertyType.string);
|
||||
expect(property.description, 'User name');
|
||||
expect(property.nullable, true);
|
||||
expect(property.format, 'string');
|
||||
expect(property.example, 'John Doe');
|
||||
expect(property.defaultValue, 'Unknown');
|
||||
});
|
||||
|
||||
test('creates ApiProperty with reference', () {
|
||||
final json = {
|
||||
'type': 'object',
|
||||
'description': 'User object',
|
||||
'\$ref': '#/components/schemas/User',
|
||||
};
|
||||
|
||||
final property = ApiProperty.fromJson('user', json, []);
|
||||
|
||||
expect(property.type, PropertyType.reference);
|
||||
expect(property.reference, 'User');
|
||||
});
|
||||
|
||||
test('creates ApiProperty with array items', () {
|
||||
final json = {
|
||||
'type': 'array',
|
||||
'description': 'User list',
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'\$ref': '#/components/schemas/User',
|
||||
},
|
||||
};
|
||||
|
||||
final property = ApiProperty.fromJson('users', json, []);
|
||||
|
||||
expect(property.type, PropertyType.array);
|
||||
expect(property.items, isNotNull);
|
||||
expect(property.items!.name, 'User');
|
||||
});
|
||||
});
|
||||
|
||||
group('PropertyType', () {
|
||||
test('converts string to PropertyType', () {
|
||||
expect(PropertyType.fromString('string'), PropertyType.string);
|
||||
expect(PropertyType.fromString('integer'), PropertyType.integer);
|
||||
expect(PropertyType.fromString('number'), PropertyType.number);
|
||||
expect(PropertyType.fromString('boolean'), PropertyType.boolean);
|
||||
expect(PropertyType.fromString('array'), PropertyType.array);
|
||||
expect(PropertyType.fromString('object'), PropertyType.object);
|
||||
expect(PropertyType.fromString('unknown'), PropertyType.string);
|
||||
});
|
||||
|
||||
test('gets PropertyType value', () {
|
||||
expect(PropertyType.string.value, 'string');
|
||||
expect(PropertyType.integer.value, 'integer');
|
||||
expect(PropertyType.number.value, 'number');
|
||||
expect(PropertyType.boolean.value, 'boolean');
|
||||
expect(PropertyType.array.value, 'array');
|
||||
expect(PropertyType.object.value, 'object');
|
||||
expect(PropertyType.reference.value, 'reference');
|
||||
});
|
||||
});
|
||||
|
||||
group('ParameterLocation', () {
|
||||
test('converts string to ParameterLocation', () {
|
||||
expect(ParameterLocation.fromString('query'), ParameterLocation.query);
|
||||
expect(ParameterLocation.fromString('path'), ParameterLocation.path);
|
||||
expect(ParameterLocation.fromString('header'), ParameterLocation.header);
|
||||
expect(ParameterLocation.fromString('cookie'), ParameterLocation.cookie);
|
||||
expect(ParameterLocation.fromString('unknown'), ParameterLocation.query);
|
||||
});
|
||||
});
|
||||
|
||||
group('HttpMethod', () {
|
||||
test('converts string to HttpMethod', () {
|
||||
expect(HttpMethod.fromString('GET'), HttpMethod.get);
|
||||
expect(HttpMethod.fromString('POST'), HttpMethod.post);
|
||||
expect(HttpMethod.fromString('PUT'), HttpMethod.put);
|
||||
expect(HttpMethod.fromString('DELETE'), HttpMethod.delete);
|
||||
expect(HttpMethod.fromString('PATCH'), HttpMethod.patch);
|
||||
expect(HttpMethod.fromString('HEAD'), HttpMethod.head);
|
||||
expect(HttpMethod.fromString('OPTIONS'), HttpMethod.options);
|
||||
});
|
||||
|
||||
test('handles case insensitive input', () {
|
||||
expect(HttpMethod.fromString('get'), HttpMethod.get);
|
||||
expect(HttpMethod.fromString('Post'), HttpMethod.post);
|
||||
expect(HttpMethod.fromString('put'), HttpMethod.put);
|
||||
});
|
||||
|
||||
test('returns default for unknown method', () {
|
||||
expect(HttpMethod.fromString('UNKNOWN'), HttpMethod.get);
|
||||
expect(HttpMethod.fromString(''), HttpMethod.get);
|
||||
});
|
||||
|
||||
test('gets HttpMethod value', () {
|
||||
expect(HttpMethod.get.value, 'GET');
|
||||
expect(HttpMethod.post.value, 'POST');
|
||||
expect(HttpMethod.put.value, 'PUT');
|
||||
expect(HttpMethod.delete.value, 'DELETE');
|
||||
expect(HttpMethod.patch.value, 'PATCH');
|
||||
expect(HttpMethod.head.value, 'HEAD');
|
||||
expect(HttpMethod.options.value, 'OPTIONS');
|
||||
});
|
||||
});
|
||||
|
||||
group('SwaggerDocument', () {
|
||||
test('creates SwaggerDocument with required fields', () {
|
||||
final document = SwaggerDocument(
|
||||
title: 'Test API',
|
||||
version: '1.0.0',
|
||||
description: 'Test API description',
|
||||
host: 'api.example.com',
|
||||
basePath: '/api',
|
||||
schemes: ['https'],
|
||||
consumes: ['application/json'],
|
||||
produces: ['application/json'],
|
||||
paths: {},
|
||||
models: {},
|
||||
controllers: {},
|
||||
);
|
||||
|
||||
expect(document.title, 'Test API');
|
||||
expect(document.version, '1.0.0');
|
||||
expect(document.description, 'Test API description');
|
||||
expect(document.host, 'api.example.com');
|
||||
expect(document.basePath, '/api');
|
||||
expect(document.schemes, ['https']);
|
||||
expect(document.consumes, ['application/json']);
|
||||
expect(document.produces, ['application/json']);
|
||||
expect(document.paths, isEmpty);
|
||||
expect(document.models, isEmpty);
|
||||
expect(document.controllers, isEmpty);
|
||||
});
|
||||
|
||||
test('creates SwaggerDocument from JSON with all fields', () {
|
||||
final json = {
|
||||
'info': {
|
||||
'title': 'Test API',
|
||||
'version': '1.0.0',
|
||||
'description': 'Test API description',
|
||||
},
|
||||
'host': 'api.example.com',
|
||||
'basePath': '/api',
|
||||
'schemes': ['https', 'http'],
|
||||
'consumes': ['application/json', 'application/xml'],
|
||||
'produces': ['application/json', 'text/plain'],
|
||||
};
|
||||
|
||||
final document = SwaggerDocument.fromJson(json);
|
||||
|
||||
expect(document.title, 'Test API');
|
||||
expect(document.version, '1.0.0');
|
||||
expect(document.description, 'Test API description');
|
||||
expect(document.host, 'api.example.com');
|
||||
expect(document.basePath, '/api');
|
||||
expect(document.schemes, ['https', 'http']);
|
||||
expect(document.consumes, ['application/json', 'application/xml']);
|
||||
expect(document.produces, ['application/json', 'text/plain']);
|
||||
});
|
||||
|
||||
test('creates SwaggerDocument from JSON with minimal fields', () {
|
||||
final json = <String, dynamic>{};
|
||||
|
||||
final document = SwaggerDocument.fromJson(json);
|
||||
|
||||
expect(document.title, 'API');
|
||||
expect(document.version, '1.0.0');
|
||||
expect(document.description, '');
|
||||
expect(document.host, '');
|
||||
expect(document.basePath, '');
|
||||
expect(document.schemes, ['https']);
|
||||
expect(document.consumes, ['application/json']);
|
||||
expect(document.produces, ['application/json']);
|
||||
});
|
||||
|
||||
test('creates SwaggerDocument from JSON with null info', () {
|
||||
final json = {'info': null};
|
||||
|
||||
final document = SwaggerDocument.fromJson(json);
|
||||
|
||||
expect(document.title, 'API');
|
||||
expect(document.version, '1.0.0');
|
||||
expect(document.description, '');
|
||||
});
|
||||
});
|
||||
|
||||
group('ApiController', () {
|
||||
test('creates ApiController with required fields', () {
|
||||
final controller = ApiController(
|
||||
name: 'UserController',
|
||||
description: 'User management controller',
|
||||
paths: [],
|
||||
);
|
||||
|
||||
expect(controller.name, 'UserController');
|
||||
expect(controller.description, 'User management controller');
|
||||
expect(controller.paths, isEmpty);
|
||||
});
|
||||
|
||||
test('creates ApiController with paths', () {
|
||||
final paths = [
|
||||
ApiPath(
|
||||
path: '/api/users',
|
||||
method: HttpMethod.get,
|
||||
summary: 'Get users',
|
||||
description: 'Retrieve all users',
|
||||
operationId: 'getUsers',
|
||||
tags: ['User'],
|
||||
parameters: [],
|
||||
responses: {},
|
||||
requestBody: null,
|
||||
),
|
||||
ApiPath(
|
||||
path: '/api/users/{id}',
|
||||
method: HttpMethod.post,
|
||||
summary: 'Create user',
|
||||
description: 'Create a new user',
|
||||
operationId: 'createUser',
|
||||
tags: ['User'],
|
||||
parameters: [],
|
||||
responses: {},
|
||||
requestBody: null,
|
||||
),
|
||||
];
|
||||
|
||||
final controller = ApiController(
|
||||
name: 'UserController',
|
||||
description: 'User management controller',
|
||||
paths: paths,
|
||||
);
|
||||
|
||||
expect(controller.paths, hasLength(2));
|
||||
expect(controller.paths[0].path, '/api/users');
|
||||
expect(controller.paths[1].path, '/api/users/{id}');
|
||||
});
|
||||
|
||||
test('creates ApiController from paths', () {
|
||||
final paths = [
|
||||
ApiPath(
|
||||
path: '/api/users',
|
||||
method: HttpMethod.get,
|
||||
summary: 'Get users',
|
||||
description: 'Retrieve all users',
|
||||
operationId: 'getUsers',
|
||||
tags: ['User'],
|
||||
parameters: [],
|
||||
responses: {},
|
||||
requestBody: null,
|
||||
),
|
||||
];
|
||||
|
||||
final controller = ApiController.fromPaths('UserController', paths);
|
||||
|
||||
expect(controller.name, 'UserController');
|
||||
expect(controller.description, 'UserController');
|
||||
expect(controller.paths, hasLength(1));
|
||||
});
|
||||
});
|
||||
|
||||
group('ApiPath fromJson', () {
|
||||
test('creates ApiPath from JSON with all fields', () {
|
||||
final json = {
|
||||
'summary': 'Get users',
|
||||
'description': 'Retrieve all users',
|
||||
'operationId': 'getUsers',
|
||||
'tags': ['User'],
|
||||
'parameters': [
|
||||
<String, dynamic>{
|
||||
'name': 'id',
|
||||
'in': 'path',
|
||||
'required': true,
|
||||
'type': 'integer',
|
||||
'description': 'User ID',
|
||||
}
|
||||
],
|
||||
'responses': <String, dynamic>{
|
||||
'200': <String, dynamic>{
|
||||
'description': 'Success',
|
||||
'schema': <String, dynamic>{'type': 'object'},
|
||||
}
|
||||
},
|
||||
'requestBody': <String, dynamic>{
|
||||
'description': 'User data',
|
||||
'required': true,
|
||||
'content': <String, dynamic>{
|
||||
'application/json': <String, dynamic>{
|
||||
'schema': <String, dynamic>{'type': 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
'deprecated': true,
|
||||
};
|
||||
|
||||
final path = ApiPath.fromJson('/api/users/{id}', 'PUT', json);
|
||||
|
||||
expect(path.path, '/api/users/{id}');
|
||||
expect(path.method, HttpMethod.put);
|
||||
expect(path.summary, 'Get users');
|
||||
expect(path.description, 'Retrieve all users');
|
||||
expect(path.operationId, 'getUsers');
|
||||
expect(path.tags, ['User']);
|
||||
expect(path.parameters, hasLength(1));
|
||||
expect(path.responses, hasLength(1));
|
||||
expect(path.requestBody, isNotNull);
|
||||
expect(path.deprecated, true);
|
||||
});
|
||||
|
||||
test('creates ApiPath from JSON with minimal fields', () {
|
||||
final json = <String, dynamic>{};
|
||||
|
||||
final path = ApiPath.fromJson('/api/users', 'GET', json);
|
||||
|
||||
expect(path.path, '/api/users');
|
||||
expect(path.method, HttpMethod.get);
|
||||
expect(path.summary, '');
|
||||
expect(path.description, '');
|
||||
expect(path.operationId, '');
|
||||
expect(path.tags, isEmpty);
|
||||
expect(path.parameters, isEmpty);
|
||||
expect(path.responses, isEmpty);
|
||||
expect(path.requestBody, isNull);
|
||||
expect(path.deprecated, false);
|
||||
});
|
||||
});
|
||||
|
||||
group('ApiModel fromJson edge cases', () {
|
||||
test('creates ApiModel with nullable properties', () {
|
||||
final json = {
|
||||
'description': 'User model',
|
||||
'properties': {
|
||||
'id': {
|
||||
'type': 'integer',
|
||||
'description': 'User ID',
|
||||
'nullable': true,
|
||||
},
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'description': 'User name',
|
||||
'nullable': false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
final model = ApiModel.fromJson('User', json);
|
||||
|
||||
expect(model.properties['id']!.nullable, true);
|
||||
expect(model.properties['id']!.required, false);
|
||||
expect(model.properties['name']!.nullable, false);
|
||||
expect(model.properties['name']!.required, true);
|
||||
expect(model.required, ['name']);
|
||||
});
|
||||
|
||||
test('creates ApiModel with explicit required fields', () {
|
||||
final json = {
|
||||
'description': 'User model',
|
||||
'properties': {
|
||||
'id': {
|
||||
'type': 'integer',
|
||||
'description': 'User ID',
|
||||
'nullable': true,
|
||||
},
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'description': 'User name',
|
||||
'nullable': false,
|
||||
},
|
||||
},
|
||||
'required': ['id', 'name'],
|
||||
};
|
||||
|
||||
final model = ApiModel.fromJson('User', json);
|
||||
|
||||
expect(model.properties['id']!.required, true);
|
||||
expect(model.properties['name']!.required, true);
|
||||
expect(model.required, ['id', 'name']);
|
||||
});
|
||||
});
|
||||
|
||||
group('ApiProperty complex types', () {
|
||||
test('creates ApiProperty with nested object', () {
|
||||
final json = {
|
||||
'type': 'object',
|
||||
'description': 'User address',
|
||||
'properties': {
|
||||
'street': {'type': 'string'},
|
||||
'city': {'type': 'string'},
|
||||
},
|
||||
};
|
||||
|
||||
final property = ApiProperty.fromJson('address', json, []);
|
||||
|
||||
expect(property.type, PropertyType.object);
|
||||
expect(property.description, 'User address');
|
||||
expect(property.required, false);
|
||||
});
|
||||
|
||||
test('creates ApiProperty with array of primitives', () {
|
||||
final json = {
|
||||
'type': 'array',
|
||||
'description': 'User tags',
|
||||
'items': {
|
||||
'type': 'string',
|
||||
},
|
||||
};
|
||||
|
||||
final property = ApiProperty.fromJson('tags', json, []);
|
||||
|
||||
expect(property.type, PropertyType.array);
|
||||
expect(property.description, 'User tags');
|
||||
expect(property.items, isNotNull);
|
||||
expect(property.items!.name, 'string');
|
||||
});
|
||||
|
||||
test('creates ApiProperty with array of objects', () {
|
||||
final json = {
|
||||
'type': 'array',
|
||||
'description': 'User list',
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'\$ref': '#/components/schemas/User',
|
||||
},
|
||||
};
|
||||
|
||||
final property = ApiProperty.fromJson('users', json, []);
|
||||
|
||||
expect(property.type, PropertyType.array);
|
||||
expect(property.description, 'User list');
|
||||
expect(property.items, isNotNull);
|
||||
expect(property.items!.name, 'User');
|
||||
});
|
||||
|
||||
test('creates ApiProperty with file type', () {
|
||||
final json = {
|
||||
'type': 'file',
|
||||
'description': 'User avatar',
|
||||
'format': 'binary',
|
||||
};
|
||||
|
||||
final property = ApiProperty.fromJson('avatar', json, []);
|
||||
|
||||
expect(property.type, PropertyType.file);
|
||||
expect(property.description, 'User avatar');
|
||||
expect(property.format, 'binary');
|
||||
});
|
||||
|
||||
test('creates ApiProperty with date type', () {
|
||||
final json = {
|
||||
'type': 'string',
|
||||
'format': 'date',
|
||||
'description': 'User birth date',
|
||||
};
|
||||
|
||||
final property = ApiProperty.fromJson('birthDate', json, []);
|
||||
|
||||
expect(property.type, PropertyType.string);
|
||||
expect(property.format, 'date');
|
||||
expect(property.description, 'User birth date');
|
||||
});
|
||||
|
||||
test('creates ApiProperty with date-time type', () {
|
||||
final json = {
|
||||
'type': 'string',
|
||||
'format': 'date-time',
|
||||
'description': 'User created at',
|
||||
};
|
||||
|
||||
final property = ApiProperty.fromJson('createdAt', json, []);
|
||||
|
||||
expect(property.type, PropertyType.string);
|
||||
expect(property.format, 'date-time');
|
||||
expect(property.description, 'User created at');
|
||||
});
|
||||
});
|
||||
|
||||
group('Error handling and edge cases', () {
|
||||
test('ApiParameter fromJson handles missing fields gracefully', () {
|
||||
final json = <String, dynamic>{};
|
||||
|
||||
final param = ApiParameter.fromJson(json);
|
||||
|
||||
expect(param.name, '');
|
||||
expect(param.location, ParameterLocation.query);
|
||||
expect(param.required, false);
|
||||
expect(param.type, PropertyType.string);
|
||||
expect(param.description, '');
|
||||
expect(param.format, isNull);
|
||||
expect(param.example, isNull);
|
||||
expect(param.defaultValue, isNull);
|
||||
});
|
||||
|
||||
test('ApiResponse fromJson handles missing fields gracefully', () {
|
||||
final json = <String, dynamic>{};
|
||||
|
||||
final response = ApiResponse.fromJson('200', json);
|
||||
|
||||
expect(response.code, '200');
|
||||
expect(response.description, '');
|
||||
expect(response.schema, isNull);
|
||||
expect(response.content, isNull);
|
||||
});
|
||||
|
||||
test('ApiRequestBody fromJson handles missing fields gracefully', () {
|
||||
final json = <String, dynamic>{};
|
||||
|
||||
final requestBody = ApiRequestBody.fromJson(json);
|
||||
|
||||
expect(requestBody.description, '');
|
||||
expect(requestBody.required, false);
|
||||
expect(requestBody.content, isNull);
|
||||
});
|
||||
|
||||
test('PropertyType fromString handles unknown types', () {
|
||||
expect(PropertyType.fromString('unknown'), PropertyType.string);
|
||||
expect(PropertyType.fromString(''), PropertyType.string);
|
||||
expect(PropertyType.fromString('CUSTOM_TYPE'), PropertyType.string);
|
||||
});
|
||||
|
||||
test('ParameterLocation fromString handles unknown locations', () {
|
||||
expect(ParameterLocation.fromString('unknown'), ParameterLocation.query);
|
||||
expect(ParameterLocation.fromString(''), ParameterLocation.query);
|
||||
expect(ParameterLocation.fromString('CUSTOM_LOCATION'),
|
||||
ParameterLocation.query);
|
||||
});
|
||||
|
||||
test('HttpMethod fromString handles unknown methods', () {
|
||||
expect(HttpMethod.fromString('unknown'), HttpMethod.get);
|
||||
expect(HttpMethod.fromString(''), HttpMethod.get);
|
||||
expect(HttpMethod.fromString('CUSTOM_METHOD'), HttpMethod.get);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
import 'package:test/test.dart';
|
||||
|
||||
import '../lib/utils/string_utils.dart';
|
||||
|
||||
void main() {
|
||||
group('StringUtils', () {
|
||||
group('toDartPropertyName', () {
|
||||
test('converts snake_case to camelCase', () {
|
||||
expect(StringUtils.toDartPropertyName('user_id'), 'userId');
|
||||
expect(StringUtils.toDartPropertyName('user-id'), 'userId');
|
||||
expect(StringUtils.toDartPropertyName('1st_field'), 'n1stField');
|
||||
expect(StringUtils.toDartPropertyName(''), 'property');
|
||||
});
|
||||
|
||||
test('handles special characters', () {
|
||||
expect(StringUtils.toDartPropertyName('field@name'), 'fieldName');
|
||||
expect(StringUtils.toDartPropertyName('with space'), 'withSpace');
|
||||
expect(StringUtils.toDartPropertyName('with.dot'), 'withDot');
|
||||
expect(StringUtils.toDartPropertyName('with/slash'), 'withSlash');
|
||||
});
|
||||
|
||||
test('handles numbers at start', () {
|
||||
expect(StringUtils.toDartPropertyName('1field'), 'n1field');
|
||||
expect(StringUtils.toDartPropertyName('123test'), 'n123test');
|
||||
});
|
||||
});
|
||||
|
||||
group('toCamelCase', () {
|
||||
test('converts snake_case to camelCase', () {
|
||||
expect(StringUtils.toCamelCase('user_id'), 'userId');
|
||||
expect(StringUtils.toCamelCase('first_name'), 'firstName');
|
||||
expect(StringUtils.toCamelCase('api_version'), 'apiVersion');
|
||||
});
|
||||
|
||||
test('handles single word', () {
|
||||
expect(StringUtils.toCamelCase('user'), 'user');
|
||||
expect(StringUtils.toCamelCase(''), '');
|
||||
});
|
||||
|
||||
test('handles multiple underscores', () {
|
||||
expect(StringUtils.toCamelCase('user__id'), 'userId');
|
||||
expect(StringUtils.toCamelCase('_user_id_'), 'userId');
|
||||
});
|
||||
});
|
||||
|
||||
group('toPascalCase', () {
|
||||
test('converts snake_case to PascalCase', () {
|
||||
expect(StringUtils.toPascalCase('user_id'), 'UserId');
|
||||
expect(StringUtils.toPascalCase('first_name'), 'FirstName');
|
||||
expect(StringUtils.toPascalCase('api_version'), 'ApiVersion');
|
||||
});
|
||||
|
||||
test('handles already PascalCase', () {
|
||||
expect(StringUtils.toPascalCase('User'), 'User');
|
||||
expect(StringUtils.toPascalCase('UserID'), 'UserID');
|
||||
});
|
||||
|
||||
test('handles camelCase input', () {
|
||||
expect(StringUtils.toPascalCase('userId'), 'UserId');
|
||||
expect(StringUtils.toPascalCase('firstName'), 'FirstName');
|
||||
});
|
||||
|
||||
test('handles empty and single character', () {
|
||||
expect(StringUtils.toPascalCase(''), '');
|
||||
expect(StringUtils.toPascalCase('a'), 'A');
|
||||
});
|
||||
});
|
||||
|
||||
group('toSnakeCase', () {
|
||||
test('converts camelCase to snake_case', () {
|
||||
expect(StringUtils.toSnakeCase('userId'), 'user_id');
|
||||
expect(StringUtils.toSnakeCase('firstName'), 'first_name');
|
||||
expect(StringUtils.toSnakeCase('apiVersion'), 'api_version');
|
||||
});
|
||||
|
||||
test('converts PascalCase to snake_case', () {
|
||||
expect(StringUtils.toSnakeCase('UserID'), 'user_id');
|
||||
expect(StringUtils.toSnakeCase('FirstName'), 'first_name');
|
||||
});
|
||||
|
||||
test('handles acronyms', () {
|
||||
expect(StringUtils.toSnakeCase('API'), 'api');
|
||||
expect(StringUtils.toSnakeCase('URL'), 'url');
|
||||
});
|
||||
|
||||
test('handles empty and single character', () {
|
||||
expect(StringUtils.toSnakeCase(''), '');
|
||||
expect(StringUtils.toSnakeCase('a'), 'a');
|
||||
});
|
||||
});
|
||||
|
||||
group('generateClassName', () {
|
||||
test('generates valid class names', () {
|
||||
expect(StringUtils.generateClassName('user'), 'User');
|
||||
expect(StringUtils.generateClassName('user_id'), 'UserId');
|
||||
expect(StringUtils.generateClassName('api-version'), 'ApiVersion');
|
||||
});
|
||||
|
||||
test('handles special characters', () {
|
||||
expect(StringUtils.generateClassName('user@name'), 'UserName');
|
||||
expect(StringUtils.generateClassName('user.name'), 'UserName');
|
||||
});
|
||||
});
|
||||
|
||||
group('generateFileName', () {
|
||||
test('generates valid file names', () {
|
||||
expect(StringUtils.generateFileName('User'), 'user.dart');
|
||||
expect(StringUtils.generateFileName('UserID'), 'user_id.dart');
|
||||
expect(StringUtils.generateFileName('ApiVersion'), 'api_version.dart');
|
||||
});
|
||||
});
|
||||
|
||||
group('isValidDartIdentifier', () {
|
||||
test('valid identifiers', () {
|
||||
expect(StringUtils.isValidDartIdentifier('user'), true);
|
||||
expect(StringUtils.isValidDartIdentifier('user_id'), true);
|
||||
expect(StringUtils.isValidDartIdentifier('_private'), true);
|
||||
expect(StringUtils.isValidDartIdentifier('user123'), true);
|
||||
});
|
||||
|
||||
test('invalid identifiers', () {
|
||||
expect(StringUtils.isValidDartIdentifier(''), false);
|
||||
expect(StringUtils.isValidDartIdentifier('123user'), false);
|
||||
expect(StringUtils.isValidDartIdentifier('user-name'), false);
|
||||
expect(StringUtils.isValidDartIdentifier('user@name'), false);
|
||||
});
|
||||
});
|
||||
|
||||
group('generateEnumValueName', () {
|
||||
test('generates valid enum names from strings', () {
|
||||
expect(StringUtils.generateEnumValueName('active', 0), 'active');
|
||||
expect(
|
||||
StringUtils.generateEnumValueName('user_status', 1), 'userStatus');
|
||||
});
|
||||
|
||||
test('handles invalid strings', () {
|
||||
expect(StringUtils.generateEnumValueName('', 0), 'value1');
|
||||
expect(StringUtils.generateEnumValueName('123', 1), 'value2');
|
||||
});
|
||||
|
||||
test('handles non-string values', () {
|
||||
expect(StringUtils.generateEnumValueName(123, 0), 'value1');
|
||||
expect(StringUtils.generateEnumValueName('', 1), 'value2');
|
||||
});
|
||||
});
|
||||
|
||||
group('cleanDescription', () {
|
||||
test('cleans basic descriptions', () {
|
||||
expect(StringUtils.cleanDescription(' test description '),
|
||||
'test description');
|
||||
expect(StringUtils.cleanDescription('line1\nline2'), 'line1 line2');
|
||||
});
|
||||
|
||||
test('removes special characters', () {
|
||||
expect(StringUtils.cleanDescription('test@#\$%'), 'test');
|
||||
expect(StringUtils.cleanDescription('test[description]'),
|
||||
'testdescription');
|
||||
});
|
||||
|
||||
test('truncates long descriptions', () {
|
||||
final longDesc = 'a' * 300;
|
||||
final result = StringUtils.cleanDescription(longDesc);
|
||||
expect(result.length, lessThanOrEqualTo(203)); // 200 + '...'
|
||||
expect(result.endsWith('...'), true);
|
||||
});
|
||||
|
||||
test('handles empty and null', () {
|
||||
expect(StringUtils.cleanDescription(''), '');
|
||||
});
|
||||
});
|
||||
|
||||
group('generateComment', () {
|
||||
test('generates single line comments', () {
|
||||
expect(StringUtils.generateComment('test'), '/// test');
|
||||
expect(StringUtils.generateComment(''), '');
|
||||
});
|
||||
|
||||
test('generates block comments', () {
|
||||
final result = StringUtils.generateComment('test', isBlock: true);
|
||||
expect(result, contains('/**'));
|
||||
expect(result, contains('test'));
|
||||
expect(result, contains('*/'));
|
||||
});
|
||||
});
|
||||
|
||||
group('formatBytes', () {
|
||||
test('formats bytes correctly', () {
|
||||
expect(StringUtils.formatBytes(1023), '1023B');
|
||||
expect(StringUtils.formatBytes(1024), '1.0KB');
|
||||
expect(StringUtils.formatBytes(1024 * 1024), '1.0MB');
|
||||
expect(StringUtils.formatBytes(1024 * 1024 * 1024), '1.0GB');
|
||||
});
|
||||
});
|
||||
|
||||
group('formatDuration', () {
|
||||
test('formats duration correctly', () {
|
||||
expect(
|
||||
StringUtils.formatDuration(Duration(milliseconds: 500)), '500毫秒');
|
||||
expect(StringUtils.formatDuration(Duration(seconds: 1)), '1.00秒');
|
||||
expect(StringUtils.formatDuration(Duration(seconds: 2)), '2.00秒');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue