Complete OpenAPI 3.0/3.1 generation, diagnostics, lifecycle safety, enum and naming mappings, bilingual documentation, examples, and pub.dev release validation.
454 lines
12 KiB
Dart
454 lines
12 KiB
Dart
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<KoiExternalResponseModel> 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<KoiDiagnosticException>().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<KoiDiagnosticException>().having(
|
|
(error) => error.diagnostics.map((diagnostic) => diagnostic.code),
|
|
'diagnostic codes',
|
|
contains('openapi.multisource_operation_conflict'),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
Future<File> _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"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
''';
|
|
}
|