Release 0.2.0
Complete OpenAPI 3.0/3.1 generation, diagnostics, lifecycle safety, enum and naming mappings, bilingual documentation, examples, and pub.dev release validation.
This commit is contained in:
@@ -0,0 +1,453 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
''';
|
||||
}
|
||||
Reference in New Issue
Block a user