Files
koi_swagger_parser/test/http_semantics_test.dart
T
mr.koi 454e6c51d7 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.
2026-07-31 16:47:39 +08:00

268 lines
8.4 KiB
Dart

import 'dart:io';
import 'package:koi_swagger_parser/koi_swagger_parser.dart';
import 'package:koi_swagger_parser/src/openapi/openapi.dart';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
void main() {
group('HTTP wire semantics', () {
test(
'生成 style/explode/cookie/allowReserved transport adapter 与表单',
() async {
final tempDir = await Directory.systemTemp.createTemp(
'koi_swagger_parser_http_semantics_',
);
addTearDown(() => tempDir.delete(recursive: true));
final specFile = File(p.join(tempDir.path, 'openapi.json'));
await specFile.writeAsString(r'''
{
"openapi": "3.1.0",
"info": {"title": "HTTP Semantics", "version": "1.0.0"},
"paths": {
"/api/v1/items/{coordinates}": {
"post": {
"tags": ["items"],
"operationId": "search_items",
"parameters": [
{
"name": "coordinates",
"in": "path",
"required": true,
"style": "matrix",
"explode": false,
"schema": {"type": "array", "items": {"type": "integer"}}
},
{
"name": "filter",
"in": "query",
"style": "deepObject",
"explode": true,
"schema": {
"type": "object",
"properties": {
"role": {"type": "string"},
"active": {"type": "boolean"}
}
}
},
{
"name": "sort",
"in": "query",
"style": "form",
"explode": false,
"schema": {"type": "array", "items": {"type": "string"}}
},
{
"name": "fields",
"in": "query",
"style": "pipeDelimited",
"schema": {"type": "array", "items": {"type": "string"}}
},
{
"name": "redirect",
"in": "query",
"allowReserved": true,
"schema": {"type": "string"}
},
{
"name": "X-Flags",
"in": "header",
"schema": {"type": "array", "items": {"type": "string"}}
},
{
"name": "session",
"in": "cookie",
"required": true,
"schema": {"type": "string"}
},
{
"name": "features",
"in": "cookie",
"explode": true,
"schema": {"type": "array", "items": {"type": "string"}}
}
],
"requestBody": {
"required": true,
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"type": "object",
"required": ["name", "state"],
"properties": {
"name": {"type": "string"},
"state": {
"type": "string",
"enum": ["draft", "published"]
},
"labels": {
"type": "array",
"items": {"type": "string"}
}
}
}
}
}
},
"responses": {"204": {"description": "updated"}}
}
}
}
}
''');
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: ${specFile.path}
output:
base_dir: $outputDir
api_dir: ${p.join(outputDir, 'api')}
models_dir: ${p.join(outputDir, 'api_models')}
generation:
api:
response_wrapper:
mode: passthrough
models:
class_prefix: Koi
formatting:
run_dart_format: false
''');
await const KoiSwaggerParser().generate(configPath: configFile.path);
final apiFile = File(p.join(outputDir, 'api', 'v1', 'items_api.dart'));
final api = await apiFile.readAsString();
expect(api, contains('class ItemsApi {'));
expect(api, contains('final _ItemsApiRetrofit _delegate;'));
expect(api, contains('required List<int> coordinates'));
expect(api, contains('@RestApi(parser: Parser.JsonSerializable)'));
expect(api, contains('abstract class _ItemsApiRetrofit'));
expect(
api,
contains("@POST('/api/v1/items/{coordinates}{reservedQuery}')"),
);
expect(
api,
contains("@Path('coordinates') required String coordinates"),
);
expect(api, contains("@Query('filter') required"));
expect(api, contains("@Query('sort') required String? sort"));
expect(api, contains("@Header('X-Flags') required String? xFlags"));
expect(
api,
contains("@Header('Cookie') required String? cookieHeader"),
);
expect(
api,
contains("@Path('reservedQuery') required String reservedQuery"),
);
expect(api, contains('@FormUrlEncoded()'));
expect(api, contains("@Field('state') required Object? state"));
expect(api, contains('_openApiCookieHeader('));
expect(api, contains('_openApiReservedQuery('));
expect(api, contains("case 'matrix':"));
expect(api, contains("'pipeDelimited' => '|'"));
final format = await Process.run(Platform.resolvedExecutable, <String>[
'format',
'--output=none',
apiFile.path,
]);
expect(
format.exitCode,
0,
reason: '${format.stdout}\n${format.stderr}',
);
},
);
test('可选 JSON body 生成 nullable 类型与 nullToAbsent', () async {
final config = KoiSwaggerConfig(
configPath: null,
workingDirectory: Directory.current.path,
input: const InputConfig(swaggerUrls: <String>[]),
output: OutputConfig(
baseDir: Directory.current.path,
apiDir: p.join(Directory.current.path, 'api'),
modelsDir: p.join(Directory.current.path, 'models'),
splitByTags: true,
extractVersionFromUrl: false,
),
generation: GenerationConfig(
preset: GeneratorPresetRegistry.koiFlutter,
api: const ApiGenerationConfig(
enabled: true,
useRetrofit: true,
cancelTokenSupport: false,
client: ClientConfig(
className: 'ApiClient',
fileName: 'api_client',
),
responseWrapper: ResponseWrapperConfig(
mode: ResponseWrapperMode.passthrough,
successDataField: 'data',
detectStandardEnvelope: false,
),
),
models: const ModelGenerationConfig(
enabled: true,
classPrefix: 'Koi',
defaultNonNullableListToEmpty: true,
jsonSerializable: JsonSerializableConfig(
checked: true,
explicitToJson: true,
includeIfNull: false,
),
),
formatting: const FormattingConfig(
runDartFormat: false,
runDartFix: false,
),
),
);
final parsed = OpenApiDocumentParser(config).parse(
LoadedOpenApiDocument(
document: <String, dynamic>{
'openapi': '3.1.0',
'info': <String, dynamic>{'title': 'optional body', 'version': '1'},
'paths': <String, dynamic>{
'/api/v1/items': <String, dynamic>{
'post': <String, dynamic>{
'tags': <String>['items'],
'requestBody': <String, dynamic>{
'required': false,
'content': <String, dynamic>{
'application/json': <String, dynamic>{
'schema': <String, dynamic>{
'type': 'object',
'properties': <String, dynamic>{
'name': <String, dynamic>{'type': 'string'},
},
},
},
},
},
'responses': <String, dynamic>{
'204': <String, dynamic>{'description': 'ok'},
},
},
},
},
},
sources: const <String>['memory://optional-body'],
),
);
expect(parsed.operations.single.requestBody!.type!.nullable, isTrue);
});
});
}