From 364aeddc4c99db567b78cc1381a39bf26cb249e1 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 24 Jul 2025 10:55:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=81=A2=E5=A4=8D=E4=B8=A2=E5=A4=B1?= =?UTF-8?q?=E7=9A=84=E9=87=8D=E8=A6=81=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 恢复 swagger.json (12,902 行) - 恢复 lib/swagger_cli_new.dart (193 行) - 恢复 tests/models_test.dart (1,589 行) - 恢复 tests/schema_validator_test.dart (475 行) 这些文件在之前的 revert 操作中被误删除 --- lib/swagger_cli_new.dart | 192 ++++ tests/models_test.dart | 1587 ++++++++++++++++++++++++++++++ tests/schema_validator_test.dart | 0 3 files changed, 1779 insertions(+) create mode 100644 lib/swagger_cli_new.dart create mode 100644 tests/models_test.dart create mode 100644 tests/schema_validator_test.dart diff --git a/lib/swagger_cli_new.dart b/lib/swagger_cli_new.dart new file mode 100644 index 0000000..14aff84 --- /dev/null +++ b/lib/swagger_cli_new.dart @@ -0,0 +1,192 @@ +import 'dart:io'; + +import 'commands/base_command.dart'; +import 'commands/generate_command.dart'; +import 'core/config.dart'; + +import 'utils/string_utils.dart'; + +/// Swagger CLI 应用程序 +/// 使用命令模式架构的新版本CLI工具 +class SwaggerCLI { + final Map _commands = {}; + + 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 run(List 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) : []; + + // 检查特殊命令 + 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 get availableCommands => _commands.keys.toList(); + + /// 获取特定命令 + BaseCommand? getCommand(String name) => _commands[name]; +} + +/// CLI应用程序入口点 +Future main(List arguments) async { + final cli = SwaggerCLI(); + final exitCode = await cli.run(arguments); + exit(exitCode); +} diff --git a/tests/models_test.dart b/tests/models_test.dart new file mode 100644 index 0000000..64bb797 --- /dev/null +++ b/tests/models_test.dart @@ -0,0 +1,1587 @@ +import 'package:swagger_generator_flutter/core/models.dart'; +import 'package:test/test.dart'; + +void main() { + group('ApiPath', () { + test('creates ApiPath with required fields', () { + const 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 = [ + const ApiParameter( + name: 'id', + location: ParameterLocation.path, + required: true, + type: PropertyType.integer, + description: 'User ID', + ), + ]; + + final responses = { + '200': const ApiResponse( + code: '200', + description: 'Success', + content: { + 'application/json': ApiMediaType( + schema: {'type': 'object'}, + ), + }, + ), + }; + + const requestBody = ApiRequestBody( + description: 'User data', + required: true, + content: { + 'application/json': ApiMediaType( + 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', () { + const 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', () { + const 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', () { + const response = ApiResponse( + code: '200', + description: 'Success', + ); + + expect(response.code, '200'); + expect(response.description, 'Success'); + expect(response.content, isEmpty); + expect(response.headers, isEmpty); + expect(response.links, isEmpty); + }); + + test('creates ApiResponse with content', () { + final content = { + 'application/json': const ApiMediaType( + schema: { + 'type': 'object', + 'properties': { + 'id': {'type': 'integer'}, + 'name': {'type': 'string'}, + }, + }, + ), + }; + + final response = ApiResponse( + code: '200', + description: 'Success', + content: content, + ); + + expect(response.content.length, 1); + expect(response.content['application/json']?.schema?['type'], 'object'); + expect(response.supportedMediaTypes, contains('application/json')); + expect(response.supportsMediaType('application/json'), true); + expect(response.hasHeaders, false); + expect(response.hasLinks, false); + }); + + test('creates ApiResponse from JSON', () { + final json = { + 'description': 'Success', + 'headers': { + 'X-Rate-Limit': { + 'description': 'Rate limit', + 'schema': {'type': 'integer'}, + }, + }, + 'content': { + 'application/json': { + 'schema': {'type': 'object'}, + 'example': {'id': 1, 'name': 'test'}, + }, + }, + 'links': { + 'GetUserByName': { + 'operationId': 'getUserByName', + 'parameters': {'username': '\$response.body#/username'}, + }, + }, + }; + + final response = ApiResponse.fromJson('200', json); + + expect(response.code, '200'); + expect(response.description, 'Success'); + expect(response.content.length, 1); + expect(response.content['application/json']?.schema?['type'], 'object'); + expect(response.headers.length, 1); + expect(response.headers['X-Rate-Limit']?.description, 'Rate limit'); + expect(response.links.length, 1); + expect(response.links['GetUserByName']?.operationId, 'getUserByName'); + expect(response.hasHeaders, true); + expect(response.hasLinks, true); + }); + }); + + group('ApiRequestBody', () { + test('creates ApiRequestBody with required fields', () { + const requestBody = ApiRequestBody( + description: 'User data', + required: true, + ); + + expect(requestBody.description, 'User data'); + expect(requestBody.required, true); + expect(requestBody.content, isEmpty); + }); + + test('creates ApiRequestBody with content', () { + final content = { + 'application/json': const ApiMediaType( + schema: { + 'type': 'object', + 'properties': { + 'name': {'type': 'string'}, + 'email': {'type': 'string'}, + }, + }, + ), + }; + + final requestBody = ApiRequestBody( + description: 'User data', + required: true, + content: content, + ); + + expect(requestBody.content.length, 1); + expect( + requestBody.content['application/json']?.schema?['type'], 'object'); + expect(requestBody.supportedMediaTypes, contains('application/json')); + expect(requestBody.supportsMediaType('application/json'), true); + }); + + 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', () { + const 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': const ApiProperty( + name: 'id', + type: PropertyType.integer, + description: 'User ID', + required: true, + ), + 'name': const 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', () { + const 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', () { + const 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', () { + const 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.unknown); + }); + + 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', () { + const document = SwaggerDocument( + title: 'Test API', + version: '1.0.0', + description: 'Test API description', + servers: [ + ApiServer( + url: 'https://api.example.com/api', + description: 'Production server', + ), + ], + components: ApiComponents( + schemas: { + 'User': ApiModel( + name: 'User', + description: 'User model', + properties: {}, + required: [], + ), + }, + ), + paths: {}, + models: {}, + controllers: {}, + ); + + expect(document.title, 'Test API'); + expect(document.version, '1.0.0'); + expect(document.description, 'Test API description'); + expect(document.servers.length, 1); + expect(document.servers.first.url, 'https://api.example.com/api'); + expect(document.servers.first.description, 'Production server'); + expect(document.components.schemas.length, 1); + expect(document.components.schemas['User']?.name, 'User'); + 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', + }, + 'servers': [ + { + 'url': 'https://api.example.com/api', + 'description': 'Production server', + 'variables': { + 'version': { + 'default': 'v1', + 'enum': ['v1', 'v2'], + 'description': 'API version', + }, + }, + }, + { + 'url': 'http://localhost:3000', + 'description': 'Development server', + }, + ], + 'components': { + 'schemas': { + 'User': { + 'type': 'object', + 'description': 'User model', + 'properties': { + 'id': {'type': 'integer'}, + 'name': {'type': 'string'}, + }, + 'required': ['id', 'name'], + }, + }, + 'responses': { + 'NotFound': { + 'description': 'Resource not found', + 'content': { + 'application/json': { + 'schema': { + 'type': 'object', + 'properties': { + 'error': {'type': 'string'}, + }, + }, + }, + }, + }, + }, + }, + }; + + final document = SwaggerDocument.fromJson(json); + + expect(document.title, 'Test API'); + expect(document.version, '1.0.0'); + expect(document.description, 'Test API description'); + expect(document.servers.length, 2); + expect(document.servers.first.url, 'https://api.example.com/api'); + expect(document.servers.first.description, 'Production server'); + expect(document.servers.first.variables.length, 1); + expect(document.servers.first.variables['version']?.defaultValue, 'v1'); + expect(document.components.schemas.length, 1); + expect(document.components.schemas['User']?.name, 'User'); + expect(document.components.responses.length, 1); + expect(document.components.responses['NotFound']?.description, + 'Resource not found'); + }); + + test('creates SwaggerDocument with composition schemas', () { + final json = { + 'info': { + 'title': 'Test API', + 'version': '1.0.0', + }, + 'components': { + 'schemas': { + 'Pet': { + 'type': 'object', + 'properties': { + 'name': {'type': 'string'}, + 'type': {'type': 'string'}, + }, + 'required': ['name'], + }, + 'Dog': { + 'allOf': [ + {'\$ref': '#/components/schemas/Pet'}, + { + 'type': 'object', + 'properties': { + 'breed': {'type': 'string'}, + }, + }, + ], + }, + 'Animal': { + 'oneOf': [ + {'\$ref': '#/components/schemas/Pet'}, + { + 'type': 'object', + 'properties': { + 'species': {'type': 'string'}, + }, + }, + ], + }, + }, + }, + }; + + final document = SwaggerDocument.fromJson(json); + + expect(document.components.schemas.length, 3); + + // 测试 allOf 组合 + final dogModel = document.components.schemas['Dog']; + expect(dogModel?.isAllOf, true); + expect(dogModel?.allOf.length, 2); + expect(dogModel?.allOf.first.reference, 'Pet'); + + // 测试 oneOf 组合 + final animalModel = document.components.schemas['Animal']; + expect(animalModel?.isOneOf, true); + expect(animalModel?.oneOf.length, 2); + expect(animalModel?.oneOf.first.reference, 'Pet'); + }); + + test('creates SwaggerDocument from JSON with minimal fields', () { + final json = {}; + + final document = SwaggerDocument.fromJson(json); + + expect(document.title, 'API'); + expect(document.version, '1.0.0'); + expect(document.description, ''); + expect(document.servers.length, 1); + expect(document.servers.first.url, '/'); + expect(document.servers.first.description, ''); + }); + + 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', () { + const 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 = [ + const ApiPath( + path: '/api/users', + method: HttpMethod.get, + summary: 'Get users', + description: 'Retrieve all users', + operationId: 'getUsers', + tags: ['User'], + parameters: [], + responses: {}, + requestBody: null, + ), + const 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 = [ + const 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': [ + { + 'name': 'id', + 'in': 'path', + 'required': true, + 'type': 'integer', + 'description': 'User ID', + } + ], + 'responses': { + '200': { + 'description': 'Success', + 'schema': {'type': 'object'}, + } + }, + 'requestBody': { + 'description': 'User data', + 'required': true, + 'content': { + 'application/json': { + 'schema': {'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 = {}; + + 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 = {}; + + 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 = {}; + + final response = ApiResponse.fromJson('200', json); + + expect(response.code, '200'); + expect(response.description, ''); + expect(response.content, isEmpty); + expect(response.headers, isEmpty); + expect(response.links, isEmpty); + expect(response.hasHeaders, false); + expect(response.hasLinks, false); + }); + + test('ApiRequestBody fromJson handles missing fields gracefully', () { + final json = {}; + + final requestBody = ApiRequestBody.fromJson(json); + + expect(requestBody.description, ''); + expect(requestBody.required, false); + expect(requestBody.content, isEmpty); + }); + + test('PropertyType fromString handles unknown types', () { + expect(PropertyType.fromString('unknown'), PropertyType.unknown); + expect(PropertyType.fromString(''), PropertyType.unknown); + expect(PropertyType.fromString('CUSTOM_TYPE'), PropertyType.unknown); + }); + + 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); + }); + }); + + group('ApiSchema', () { + test('creates ApiSchema with basic properties', () { + const schema = ApiSchema( + type: 'object', + description: 'Test schema', + properties: { + 'name': ApiProperty( + name: 'name', + type: PropertyType.string, + description: 'Name property', + required: true, + ), + }, + required: ['name'], + ); + + expect(schema.type, 'object'); + expect(schema.description, 'Test schema'); + expect(schema.properties.length, 1); + expect(schema.required, ['name']); + expect(schema.isObject, true); + expect(schema.isComposition, false); + }); + + test('creates ApiSchema with composition patterns', () { + const schema = ApiSchema( + allOf: [ + ApiSchema( + reference: 'BaseModel', + ), + ApiSchema( + type: 'object', + properties: { + 'extra': ApiProperty( + name: 'extra', + type: PropertyType.string, + description: 'Extra property', + required: false, + ), + }, + ), + ], + ); + + expect(schema.isComposition, true); + expect(schema.isAllOf, true); + expect(schema.allOf.length, 2); + expect(schema.allOf.first.isReference, true); + expect(schema.allOf.first.reference, 'BaseModel'); + }); + + test('creates ApiSchema from JSON with allOf', () { + final json = { + 'allOf': [ + {'\$ref': '#/components/schemas/Pet'}, + { + 'type': 'object', + 'properties': { + 'breed': {'type': 'string'}, + }, + 'required': ['breed'], + }, + ], + }; + + final schema = ApiSchema.fromJson(json); + + expect(schema.isComposition, true); + expect(schema.isAllOf, true); + expect(schema.allOf.length, 2); + expect(schema.allOf.first.reference, 'Pet'); + expect(schema.allOf.last.type, 'object'); + expect(schema.allOf.last.properties.length, 1); + expect(schema.allOf.last.required, ['breed']); + }); + + test('creates ApiSchema from JSON with oneOf', () { + final json = { + 'oneOf': [ + {'\$ref': '#/components/schemas/Cat'}, + {'\$ref': '#/components/schemas/Dog'}, + ], + }; + + final schema = ApiSchema.fromJson(json); + + expect(schema.isComposition, true); + expect(schema.isOneOf, true); + expect(schema.oneOf.length, 2); + expect(schema.oneOf.first.reference, 'Cat'); + expect(schema.oneOf.last.reference, 'Dog'); + }); + + test('creates ApiSchema from JSON with validation constraints', () { + final json = { + 'type': 'string', + 'minLength': 1, + 'maxLength': 100, + 'pattern': '^[a-zA-Z]+\$', + 'example': 'example', + 'nullable': true, + }; + + final schema = ApiSchema.fromJson(json); + + expect(schema.type, 'string'); + expect(schema.minLength, 1); + expect(schema.maxLength, 100); + expect(schema.pattern, '^[a-zA-Z]+\$'); + expect(schema.example, 'example'); + expect(schema.nullable, true); + }); + + test('creates ApiSchema from JSON with discriminator', () { + final json = { + 'oneOf': [ + {'\$ref': '#/components/schemas/Cat'}, + {'\$ref': '#/components/schemas/Dog'}, + ], + 'discriminator': { + 'propertyName': 'petType', + 'mapping': { + 'cat': '#/components/schemas/Cat', + 'dog': '#/components/schemas/Dog', + }, + }, + }; + + final schema = ApiSchema.fromJson(json); + + expect(schema.isComposition, true); + expect(schema.isOneOf, true); + expect(schema.hasDiscriminator, true); + expect(schema.discriminator?.propertyName, 'petType'); + expect(schema.discriminator?.hasMapping, true); + expect(schema.discriminator?.mapping.length, 2); + expect(schema.discriminator?.getSchemaForValue('cat'), + '#/components/schemas/Cat'); + expect(schema.discriminator?.getSchemaForValue('dog'), + '#/components/schemas/Dog'); + }); + }); + + group('ApiDiscriminator', () { + test('creates ApiDiscriminator with required fields', () { + const discriminator = ApiDiscriminator( + propertyName: 'type', + ); + + expect(discriminator.propertyName, 'type'); + expect(discriminator.mapping, isEmpty); + expect(discriminator.hasMapping, false); + }); + + test('creates ApiDiscriminator with mapping', () { + const discriminator = ApiDiscriminator( + propertyName: 'petType', + mapping: { + 'cat': '#/components/schemas/Cat', + 'dog': '#/components/schemas/Dog', + }, + ); + + expect(discriminator.propertyName, 'petType'); + expect(discriminator.mapping.length, 2); + expect(discriminator.hasMapping, true); + expect( + discriminator.getSchemaForValue('cat'), '#/components/schemas/Cat'); + expect( + discriminator.getSchemaForValue('dog'), '#/components/schemas/Dog'); + expect(discriminator.getSchemaForValue('bird'), isNull); + }); + + test('creates ApiDiscriminator from JSON', () { + final json = { + 'propertyName': 'objectType', + 'mapping': { + 'user': '#/components/schemas/User', + 'admin': '#/components/schemas/Admin', + }, + }; + + final discriminator = ApiDiscriminator.fromJson(json); + + expect(discriminator.propertyName, 'objectType'); + expect(discriminator.mapping.length, 2); + expect(discriminator.hasMapping, true); + expect( + discriminator.getSchemaForValue('user'), '#/components/schemas/User'); + expect(discriminator.getSchemaForValue('admin'), + '#/components/schemas/Admin'); + }); + + test('creates ApiDiscriminator from JSON with minimal fields', () { + final json = { + 'propertyName': 'type', + }; + + final discriminator = ApiDiscriminator.fromJson(json); + + expect(discriminator.propertyName, 'type'); + expect(discriminator.mapping, isEmpty); + expect(discriminator.hasMapping, false); + }); + }); + + group('Complex Nested Types', () { + test('creates ApiProperty with nested object properties', () { + final json = { + 'type': 'object', + 'description': 'User profile', + 'properties': { + 'name': {'type': 'string'}, + 'address': { + 'type': 'object', + 'properties': { + 'street': {'type': 'string'}, + 'city': {'type': 'string'}, + 'coordinates': { + 'type': 'object', + 'properties': { + 'lat': {'type': 'number'}, + 'lng': {'type': 'number'}, + }, + 'required': ['lat', 'lng'], + }, + }, + 'required': ['street', 'city'], + }, + }, + 'required': ['name'], + }; + + final property = ApiProperty.fromJson('profile', json, []); + + expect(property.type, PropertyType.object); + expect(property.hasNestedProperties, true); + expect(property.nestedProperties.length, 2); + expect(property.nestedProperties['name']?.type, PropertyType.string); + expect(property.nestedProperties['address']?.type, PropertyType.object); + expect(property.nestedProperties['address']?.hasNestedProperties, true); + + // 检查深层嵌套 + final addressProperty = property.nestedProperties['address']!; + expect(addressProperty.nestedProperties.length, 3); + expect(addressProperty.nestedProperties['coordinates']?.type, + PropertyType.object); + + final coordinatesProperty = + addressProperty.nestedProperties['coordinates']!; + expect(coordinatesProperty.nestedProperties.length, 2); + expect(coordinatesProperty.nestedProperties['lat']?.type, + PropertyType.number); + expect(coordinatesProperty.nestedProperties['lng']?.type, + PropertyType.number); + }); + + test('creates ApiProperty with array of nested objects', () { + final json = { + 'type': 'array', + 'description': 'List of users', + 'items': { + 'type': 'object', + 'properties': { + 'id': {'type': 'integer'}, + 'name': {'type': 'string'}, + 'tags': { + 'type': 'array', + 'items': {'type': 'string'}, + }, + }, + 'required': ['id', 'name'], + }, + }; + + final property = ApiProperty.fromJson('users', json, []); + + expect(property.type, PropertyType.array); + expect(property.items, isNotNull); + expect(property.items!.name, 'usersItem'); + expect(property.items!.properties.length, 3); + expect(property.items!.properties['id']?.type, PropertyType.integer); + expect(property.items!.properties['tags']?.type, PropertyType.array); + }); + + test('handles maximum depth limit', () { + final json = { + 'type': 'object', + 'properties': { + 'level1': { + 'type': 'object', + 'properties': { + 'level2': { + 'type': 'object', + 'properties': { + 'level3': {'type': 'string'}, + }, + }, + }, + }, + }, + }; + + final property = ApiProperty.fromJson('deep', json, [], maxDepth: 2); + + expect(property.type, PropertyType.object); + expect(property.hasNestedProperties, true); + + // 第一层应该正常解析 + final level1 = property.nestedProperties['level1']!; + expect(level1.type, PropertyType.object); + expect(level1.hasNestedProperties, true); + + // 第二层应该达到深度限制 + final level2 = level1.nestedProperties['level2']!; + expect(level2.type, PropertyType.object); + expect(level2.description, '达到最大嵌套深度的对象'); + }); + + test('creates ApiProperty with complex schema composition', () { + final json = { + 'allOf': [ + { + 'type': 'object', + 'properties': { + 'id': {'type': 'integer'}, + }, + }, + { + 'type': 'object', + 'properties': { + 'name': {'type': 'string'}, + }, + }, + ], + }; + + final property = ApiProperty.fromJson('composed', json, []); + + expect(property.hasComplexSchema, true); + expect(property.schema?.isAllOf, true); + expect(property.schema?.allOf.length, 2); + }); + }); + + group('Advanced Schema Features', () { + test('creates ApiSchema with additionalProperties as boolean', () { + final json = { + 'type': 'object', + 'properties': { + 'name': {'type': 'string'}, + }, + 'additionalProperties': false, + }; + + final schema = ApiSchema.fromJson(json); + + expect(schema.type, 'object'); + expect(schema.additionalProperties, false); + expect(schema.allowsAdditionalProperties, false); + expect(schema.additionalPropertiesSchema, isNull); + }); + + test('creates ApiSchema with additionalProperties as schema', () { + final json = { + 'type': 'object', + 'properties': { + 'name': {'type': 'string'}, + }, + 'additionalProperties': { + 'type': 'string', + 'pattern': '^[a-zA-Z]+\$', + }, + }; + + final schema = ApiSchema.fromJson(json); + + expect(schema.type, 'object'); + expect(schema.allowsAdditionalProperties, true); + expect(schema.additionalPropertiesSchema, isNotNull); + expect(schema.additionalPropertiesSchema?.type, 'string'); + expect(schema.additionalPropertiesSchema?.pattern, '^[a-zA-Z]+\$'); + }); + + test('creates ApiSchema with patternProperties', () { + final json = { + 'type': 'object', + 'patternProperties': { + '^S_': {'type': 'string'}, + '^I_': {'type': 'integer'}, + }, + }; + + final schema = ApiSchema.fromJson(json); + + expect(schema.hasPatternProperties, true); + expect(schema.patternProperties.length, 2); + expect(schema.patternProperties['^S_']?.type, 'string'); + expect(schema.patternProperties['^I_']?.type, 'integer'); + }); + + test('creates ApiSchema with propertyNames constraint', () { + final json = { + 'type': 'object', + 'propertyNames': { + 'pattern': '^[A-Za-z_][A-Za-z0-9_]*\$', + }, + }; + + final schema = ApiSchema.fromJson(json); + + expect(schema.hasPropertyNames, true); + expect(schema.propertyNames?.pattern, '^[A-Za-z_][A-Za-z0-9_]*\$'); + }); + + test('creates ApiSchema with dependencies', () { + final json = { + 'type': 'object', + 'properties': { + 'name': {'type': 'string'}, + 'credit_card': {'type': 'string'}, + 'billing_address': {'type': 'string'}, + }, + 'dependencies': { + 'credit_card': ['billing_address'], + }, + }; + + final schema = ApiSchema.fromJson(json); + + expect(schema.hasDependencies, true); + expect(schema.dependencies.length, 1); + expect(schema.dependencies['credit_card'], ['billing_address']); + }); + + test('creates ApiSchema with const value', () { + final json = { + 'const': 'United States of America', + }; + + final schema = ApiSchema.fromJson(json); + + expect(schema.hasConstValue, true); + expect(schema.constValue, 'United States of America'); + }); + + test('creates ApiSchema with conditional schema (if/then/else)', () { + final json = { + 'type': 'object', + 'properties': { + 'street_address': {'type': 'string'}, + 'country': {'type': 'string'}, + 'postal_code': {'type': 'string'}, + }, + 'if': { + 'properties': { + 'country': {'const': 'United States of America'}, + }, + }, + 'then': { + 'properties': { + 'postal_code': {'pattern': '[0-9]{5}(-[0-9]{4})?'}, + }, + }, + 'else': { + 'properties': { + 'postal_code': {'pattern': '[A-Z][0-9][A-Z] [0-9][A-Z][0-9]'}, + }, + }, + }; + + final schema = ApiSchema.fromJson(json); + + expect(schema.hasConditionalSchema, true); + expect(schema.ifSchema, isNotNull); + expect(schema.thenSchema, isNotNull); + expect(schema.elseSchema, isNotNull); + + // 检查基本结构 + expect(schema.ifSchema?.properties.length, 1); + expect(schema.thenSchema?.properties.length, 1); + expect(schema.elseSchema?.properties.length, 1); + + // 检查属性存在性 + expect(schema.ifSchema?.properties.containsKey('country'), true); + expect(schema.thenSchema?.properties.containsKey('postal_code'), true); + expect(schema.elseSchema?.properties.containsKey('postal_code'), true); + }); + + test('creates ApiSchema with multiple advanced features', () { + final json = { + 'type': 'object', + 'properties': { + 'name': {'type': 'string'}, + }, + 'additionalProperties': {'type': 'string'}, + 'patternProperties': { + '^prefix_': {'type': 'integer'}, + }, + 'propertyNames': { + 'pattern': '^[a-z]+\$', + }, + 'dependencies': { + 'name': ['id'], + }, + }; + + final schema = ApiSchema.fromJson(json); + + expect(schema.allowsAdditionalProperties, true); + expect(schema.hasPatternProperties, true); + expect(schema.hasPropertyNames, true); + expect(schema.hasDependencies, true); + expect(schema.patternProperties.length, 1); + expect(schema.dependencies.length, 1); + }); + }); +} diff --git a/tests/schema_validator_test.dart b/tests/schema_validator_test.dart new file mode 100644 index 0000000..e69de29