swagger_generator_flutter/test/generate_command_test.dart

311 lines
8.4 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:swagger_generator_flutter/commands/generate_command.dart';
import 'package:swagger_generator_flutter/utils/path_resolver.dart';
import 'package:test/test.dart';
void main() {
late Directory originalCwd;
late Directory tempDir;
setUp(() async {
originalCwd = Directory.current;
tempDir = await Directory(
p.join(
originalCwd.path,
'test',
'tmp',
'generate_command_${DateTime.now().microsecondsSinceEpoch}',
),
).create(recursive: true);
Directory.current = tempDir;
PathResolver.clearCache();
});
tearDown(() async {
PathResolver.clearCache();
Directory.current = originalCwd;
if (tempDir.existsSync()) {
await tempDir.delete(recursive: true);
}
});
test('按版本生成 API 与模型文件', () async {
await _writeJson(
'swagger_v1.json',
_buildSwaggerDoc(
title: 'Test API V1',
version: '1.0.0',
specs: [
const _PathSpec(
path: '/api/v1/users',
tag: 'Users',
schemaName: 'User',
operationId: 'getUsers',
),
],
),
);
await _writeJson(
'swagger_v2.json',
_buildSwaggerDoc(
title: 'Test API V2',
version: '2.0.0',
specs: [
const _PathSpec(
path: '/api/v2/admins',
tag: 'Admins',
schemaName: 'Admin',
operationId: 'getAdmins',
),
],
),
);
await _writeConfig(
swaggerFiles: ['swagger_v1.json', 'swagger_v2.json'],
apiClientClassName: 'MultiVersionApiClient',
apiClientFileName: 'multi_version_client',
);
final exitCode = await GenerateCommand().execute([]);
expect(exitCode, equals(0));
final outputDir = Directory(p.join(tempDir.path, 'out'));
expect(outputDir.existsSync(), isTrue);
final apiV1Dir = Directory(p.join(outputDir.path, 'api', 'v1'));
final apiV2Dir = Directory(p.join(outputDir.path, 'api', 'v2'));
expect(apiV1Dir.existsSync(), isTrue);
expect(apiV2Dir.existsSync(), isTrue);
expect(
_dartFiles(apiV1Dir).isNotEmpty,
isTrue,
reason: 'v1 版本应至少生成一个 API 文件',
);
expect(
_dartFiles(apiV2Dir).isNotEmpty,
isTrue,
reason: 'v2 版本应至少生成一个 API 文件',
);
final apiClient =
File(p.join(outputDir.path, 'api', 'multi_version_client.dart'));
expect(apiClient.existsSync(), isTrue);
final summary = File(p.join(outputDir.path, 'SUMMARY.md'));
expect(summary.existsSync(), isTrue);
final summaryContent = summary.readAsStringSync();
expect(summaryContent, contains('API标题'));
expect(summaryContent, contains('API版本'));
final modelsDir = Directory(p.join(outputDir.path, 'models', 'result'));
expect(modelsDir.existsSync(), isTrue);
final modelFiles = _dartFiles(modelsDir);
expect(
modelFiles.length,
greaterThanOrEqualTo(2),
reason: '示例应生成 User 与 Admin 模型文件',
);
});
test('支持命令行 tag 过滤', () async {
await _writeJson(
'swagger_filtered.json',
_buildSwaggerDoc(
title: 'Filtered API',
version: '1.0.0',
specs: const [
_PathSpec(
path: '/api/v1/users',
tag: 'Users',
schemaName: 'User',
operationId: 'getUsers',
),
_PathSpec(
path: '/api/v1/internal',
tag: 'Internal',
schemaName: 'InternalModel',
operationId: 'getInternal',
),
],
),
);
await _writeConfig(
swaggerFiles: ['swagger_filtered.json'],
apiClientClassName: 'FilteredApiClient',
apiClientFileName: 'filtered_api_client',
);
final exitCode = await GenerateCommand().execute([
'--api',
'--models',
'--included-tags=Users',
'--excluded-tags=Internal',
]);
expect(exitCode, equals(0));
final apiDir = Directory(p.join(tempDir.path, 'out', 'api', 'v1'));
expect(apiDir.existsSync(), isTrue);
final apiFiles = _dartFiles(apiDir);
expect(apiFiles.any((file) => file.contains('internal')), isFalse);
expect(apiFiles.any((file) => file.contains('user')), isTrue);
final resultDir =
Directory(p.join(tempDir.path, 'out', 'models', 'result'));
expect(resultDir.existsSync(), isTrue);
final modelNames = _dartFiles(resultDir).map(p.basename).toList();
expect(modelNames.any((name) => name.contains('internal')), isFalse);
expect(modelNames.any((name) => name.contains('user')), isTrue);
});
}
Future<void> _writeJson(String name, Map<String, dynamic> data) async {
final file = File(p.join(Directory.current.path, name));
await file.writeAsString(jsonEncode(data));
}
Future<void> _writeConfig({
required List<String> swaggerFiles,
required String apiClientClassName,
required String apiClientFileName,
}) async {
final buffer = StringBuffer()
..writeln('generator:')
..writeln(' name: "test-generator"')
..writeln(' version: "1.0.0"')
..writeln(' author: "codex"')
..writeln(
' copyright: "Copyright (C) 2025 '
'Swagger Generator Flutter. All rights reserved."',
)
..writeln('input:')
..writeln(' swagger_urls:');
for (final file in swaggerFiles) {
buffer.writeln(' - "./$file"');
}
buffer
..writeln('output:')
..writeln(' base_dir: "./out"')
..writeln(' api_dir: "./out/api"')
..writeln(' models_dir: "./out/models"')
..writeln(' split_by_tags: true')
..writeln('generation:')
..writeln(' api:')
..writeln(' client:')
..writeln(' class_name: "$apiClientClassName"')
..writeln(' file_name: "$apiClientFileName"')
..writeln(' version_extraction:')
..writeln(r' pattern: "/api/v(\\d+)/"')
..writeln(' default_version: "v1"')
..writeln(' models:')
..writeln(' enabled: true');
await File(p.join(Directory.current.path, 'generator_config.yaml'))
.writeAsString(buffer.toString());
}
Map<String, dynamic> _buildSwaggerDoc({
required String title,
required String version,
required List<_PathSpec> specs,
}) {
final tagDescriptions = <String, String>{};
final schemas = <String, Map<String, dynamic>>{};
final paths = <String, Map<String, dynamic>>{};
for (final spec in specs) {
tagDescriptions.putIfAbsent(spec.tag, () => '${spec.tag} APIs');
schemas[spec.schemaName] = _schemaDefinition(spec.schemaName);
final operations = paths.putIfAbsent(spec.path, () => <String, dynamic>{});
operations['get'] = _operationFor(spec);
}
return {
'openapi': '3.0.0',
'info': {
'title': title,
'version': version,
'description': '$title description',
},
'tags': tagDescriptions.entries
.map(
(entry) => {
'name': entry.key,
'description': entry.value,
},
)
.toList(),
'paths': paths.map(MapEntry.new),
'components': {
'schemas': schemas,
},
};
}
Map<String, dynamic> _operationFor(_PathSpec spec) {
return {
'summary': '${spec.tag} summary',
'description': '${spec.tag} description',
'operationId': spec.operationId,
'tags': [spec.tag],
'responses': {
'200': {
'description': 'OK',
'content': {
'application/json': {
'schema': {
'type': 'array',
'items': {r'$ref': '#/components/schemas/${spec.schemaName}'},
},
},
},
},
},
};
}
Map<String, dynamic> _schemaDefinition(String name) {
return {
'type': 'object',
'properties': {
'id': {'type': 'integer', 'format': 'int64'},
'name': {'type': 'string'},
},
'required': ['id'],
'description': '$name definition',
};
}
Iterable<String> _dartFiles(Directory directory) {
if (!directory.existsSync()) {
return const Iterable<String>.empty();
}
return directory
.listSync()
.whereType<File>()
.map((file) => file.path)
.where((path) => path.endsWith('.dart'));
}
class _PathSpec {
const _PathSpec({
required this.path,
required this.tag,
required this.schemaName,
required this.operationId,
});
final String path;
final String tag;
final String schemaName;
final String operationId;
}