import 'dart:io'; import 'package:koi_swagger_parser/koi_swagger_parser.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; void main() { group(r'$ref resolver', () { test('解析相对文件、跨组件引用、循环引用与名称冲突', () async { final tempDir = await Directory.systemTemp.createTemp( 'koi_swagger_parser_relative_refs_', ); addTearDown(() => tempDir.delete(recursive: true)); final commonFile = File(p.join(tempDir.path, 'common.yaml')); await commonFile.writeAsString(r''' components: parameters: Limit: name: limit in: query required: false schema: type: integer requestBodies: PayloadBody: required: true content: application/json: schema: $ref: '#/components/schemas/Payload' responses: ExternalResponse: description: ok content: application/json: schema: $ref: '#/components/schemas/ExternalResponseModel' schemas: Payload: type: object required: [name] properties: name: type: string ExternalResponseModel: type: object required: [item] properties: item: $ref: '#/components/schemas/ExternalItem' ExternalItem: type: object required: [id] properties: id: type: integer parent: $ref: '#/components/schemas/ExternalResponseModel' '''); final rootFile = File(p.join(tempDir.path, 'openapi.yaml')); await rootFile.writeAsString(r''' openapi: 3.1.0 info: title: Relative refs version: 1.0.0 paths: /api/v1/external: post: tags: [external] parameters: - $ref: './common.yaml#/components/parameters/Limit' requestBody: $ref: './common.yaml#/components/requestBodies/PayloadBody' responses: '200': $ref: './common.yaml#/components/responses/ExternalResponse' components: schemas: ExternalItem: type: object properties: localOnly: type: boolean '''); final outputDir = p.join(tempDir.path, 'lib', 'src'); final configFile = await _writeConfig( tempDir.path, source: rootFile.path, outputDir: outputDir, ); final result = await const KoiSwaggerParser().generate( configPath: configFile.path, ); expect(result.dependencyDocuments, contains(commonFile.path)); expect( result.diagnostics.map((diagnostic) => diagnostic.code), contains('openapi.external_component_renamed'), ); final apiFile = File( p.join(outputDir, 'api', 'v1', 'external_api.dart'), ).readAsStringSync(); expect(apiFile, contains("@Query('limit') int? limit")); expect(apiFile, contains('@Body() required KoiPayload body')); expect( apiFile, contains('Future postExternal'), ); final responseFile = File( p.join( outputDir, 'api_models', 'result', 'external_response_model.dart', ), ).readAsStringSync(); expect(responseFile, contains('required KoiCommonExternalItem item')); final itemFile = File( p.join(outputDir, 'api_models', 'result', 'common_external_item.dart'), ).readAsStringSync(); expect(itemFile, contains('KoiExternalResponseModel? parent')); }); test('解析远程根文档中的相对远程引用', () async { final tempDir = await Directory.systemTemp.createTemp( 'koi_swagger_parser_remote_refs_', ); addTearDown(() => tempDir.delete(recursive: true)); final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); addTearDown(() => server.close(force: true)); server.listen((request) async { request.response.headers.contentType = ContentType.json; if (request.uri.path == '/root.json') { request.response.write(r''' { "openapi": "3.1.0", "info": { "title": "Remote refs", "version": "1.0.0" }, "paths": { "/api/v1/remote": { "get": { "responses": { "200": { "description": "ok", "content": { "application/json": { "schema": { "$ref": "./models.json#/components/schemas/RemoteModel" } } } } } } } } } '''); } else if (request.uri.path == '/models.json') { request.response.write(r''' { "components": { "schemas": { "RemoteModel": { "type": "object", "required": ["id"], "properties": { "id": { "type": "integer" } } } } } } '''); } else { request.response.statusCode = HttpStatus.notFound; } await request.response.close(); }); final rootUrl = 'http://${server.address.host}:${server.port}/root.json'; final modelsUrl = 'http://${server.address.host}:${server.port}/models.json'; final outputDir = p.join(tempDir.path, 'lib', 'src'); final configFile = await _writeConfig( tempDir.path, source: rootUrl, outputDir: outputDir, ); final result = await const KoiSwaggerParser().generate( configPath: configFile.path, ); expect(result.dependencyDocuments, contains(modelsUrl)); expect( File( p.join(outputDir, 'api_models', 'result', 'remote_model.dart'), ).existsSync(), isTrue, ); }); test('外部文档中不存在的 pointer 产生结构化错误', () async { final tempDir = await Directory.systemTemp.createTemp( 'koi_swagger_parser_missing_external_ref_', ); addTearDown(() => tempDir.delete(recursive: true)); final commonFile = File(p.join(tempDir.path, 'common.json')); await commonFile.writeAsString('{"components":{"schemas":{}}}'); final rootFile = File(p.join(tempDir.path, 'openapi.json')); await rootFile.writeAsString(r''' { "openapi": "3.1.0", "info": { "title": "Missing external pointer", "version": "1.0.0" }, "paths": { "/api/v1/items": { "get": { "responses": { "200": { "description": "ok", "content": { "application/json": { "schema": { "$ref": "./common.json#/components/schemas/Missing" } } } } } } } } } '''); final configFile = await _writeConfig( tempDir.path, source: rootFile.path, outputDir: p.join(tempDir.path, 'lib', 'src'), ); await expectLater( const KoiSwaggerParser().generate(configPath: configFile.path), throwsA( isA().having( (error) => error.diagnostics.map((diagnostic) => diagnostic.code), 'diagnostic codes', contains('openapi.unresolved_external_ref'), ), ), ); }); test('多 source component 冲突会确定性重命名并改写引用', () async { final tempDir = await Directory.systemTemp.createTemp( 'koi_swagger_parser_multisource_components_', ); addTearDown(() => tempDir.delete(recursive: true)); final firstFile = File(p.join(tempDir.path, 'one.json')); await firstFile.writeAsString( _singleModelDocument( title: 'First', path: '/api/v1/first', propertyName: 'id', propertyType: 'integer', ), ); final secondFile = File(p.join(tempDir.path, 'two.json')); await secondFile.writeAsString( _singleModelDocument( title: 'Second', path: '/api/v1/second', propertyName: 'name', propertyType: 'string', ), ); final outputDir = p.join(tempDir.path, 'lib', 'src'); final configFile = File(p.join(tempDir.path, 'koi_swagger_parser.yaml')); await configFile.writeAsString(''' input: swagger_urls: - url: ${firstFile.path} - url: ${secondFile.path} output: base_dir: $outputDir api_dir: ${p.join(outputDir, 'api')} models_dir: ${p.join(outputDir, 'api_models')} generation: api: response_wrapper: mode: passthrough formatting: run_dart_format: false '''); final result = await const KoiSwaggerParser().generate( configPath: configFile.path, ); expect( result.diagnostics.map((diagnostic) => diagnostic.code), contains('openapi.multisource_component_renamed'), ); expect( File( p.join(outputDir, 'api_models', 'result', 'thing.dart'), ).readAsStringSync(), contains('required int id'), ); expect( File( p.join(outputDir, 'api_models', 'result', 'two_thing.dart'), ).readAsStringSync(), contains('required String name'), ); }); test('多 source 重复 operation 会阻止生成', () async { final tempDir = await Directory.systemTemp.createTemp( 'koi_swagger_parser_multisource_operations_', ); addTearDown(() => tempDir.delete(recursive: true)); final firstFile = File(p.join(tempDir.path, 'one.json')); final secondFile = File(p.join(tempDir.path, 'two.json')); final document = _singleModelDocument( title: 'Duplicate', path: '/api/v1/duplicate', propertyName: 'id', propertyType: 'integer', ); await firstFile.writeAsString(document); await secondFile.writeAsString(document); final outputDir = p.join(tempDir.path, 'lib', 'src'); final configFile = File(p.join(tempDir.path, 'koi_swagger_parser.yaml')); await configFile.writeAsString(''' input: swagger_urls: - url: ${firstFile.path} - url: ${secondFile.path} output: base_dir: $outputDir generation: api: response_wrapper: mode: passthrough '''); await expectLater( const KoiSwaggerParser().generate(configPath: configFile.path), throwsA( isA().having( (error) => error.diagnostics.map((diagnostic) => diagnostic.code), 'diagnostic codes', contains('openapi.multisource_operation_conflict'), ), ), ); }); }); } Future _writeConfig( String directory, { required String source, required String outputDir, }) async { final configFile = File(p.join(directory, 'koi_swagger_parser.yaml')); await configFile.writeAsString(''' input: swagger_urls: - url: $source output: base_dir: $outputDir api_dir: ${p.join(outputDir, 'api')} models_dir: ${p.join(outputDir, 'api_models')} generation: api: response_wrapper: mode: passthrough formatting: run_dart_format: false '''); return configFile; } String _singleModelDocument({ required String title, required String path, required String propertyName, required String propertyType, }) { return ''' { "openapi": "3.1.0", "info": { "title": "$title", "version": "1.0.0" }, "paths": { "$path": { "get": { "responses": { "200": { "description": "ok", "content": { "application/json": { "schema": { "\$ref": "#/components/schemas/Thing" } } } } } } } }, "components": { "schemas": { "Thing": { "type": "object", "required": ["$propertyName"], "properties": { "$propertyName": { "type": "$propertyType" } } } } } } '''; }