feat: 增加 excluded_tags 支持
This commit is contained in:
parent
03406d3fbb
commit
a3f9cb78e6
|
|
@ -163,6 +163,7 @@ void main() async {
|
|||
|
||||
#### 高级选项
|
||||
- `--included-tags` / `-i`: 只生成指定 tags 的 API 和模型
|
||||
- `--excluded-tags` / `-e`: 从生成中排除指定的 tags
|
||||
|
||||
**示例:**
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -46,6 +46,14 @@ output:
|
|||
# - "Pet"
|
||||
# - "Store"
|
||||
|
||||
# 从代码生成中排除指定的 tags
|
||||
# 适用于内部、废弃或不需要的 API
|
||||
# 如果一个 endpoint 的所有 tags 都被排除,则该 endpoint 不会生成
|
||||
excluded_tags:
|
||||
# - "Internal"
|
||||
# - "Deprecated"
|
||||
# - "Legacy"
|
||||
|
||||
# 跳过的目录列表(这些目录下的文件将不会被生成)
|
||||
# 支持相对路径和绝对路径,支持目录名或完整路径
|
||||
ignored_directories:
|
||||
|
|
@ -94,10 +102,15 @@ generation:
|
|||
default_version: "v1"
|
||||
|
||||
# 基础类型配置(根据您的项目调整)
|
||||
base_result_type: "BaseResult"
|
||||
base_page_result_type: "BasePageResult"
|
||||
base_result_import: "package:your_project/common/models/base_result.dart"
|
||||
base_page_result_import: "package:your_project/common/models/base_page_result.dart"
|
||||
# 如果您的项目有统一的响应模型,请在此处配置
|
||||
base_result_type: "BaseResult" # 基础响应模型名称
|
||||
base_page_result_type: "BasePageResult" # 分页响应模型名称
|
||||
|
||||
# 基础模型的导入路径(可选)
|
||||
# 如果提供了路径,将在 api_models/index.dart 中自动导出
|
||||
# 如果留空,则不会生成导出语句
|
||||
base_result_import: "" # 例如: "package:your_project/common/models/base_result.dart"
|
||||
base_page_result_import: "" # 例如: "package:your_project/common/models/base_page_result.dart"
|
||||
|
||||
# 方法命名
|
||||
method_naming: "camelCase" # camelCase, snake_case
|
||||
|
|
|
|||
|
|
@ -75,6 +75,12 @@ class GenerateCommand extends BaseCommand {
|
|||
description: '只生成指定tags的API和模型(逗号分隔,如:User,Pet,Store)',
|
||||
type: OptionType.string,
|
||||
),
|
||||
const CommandOption(
|
||||
name: 'excluded-tags',
|
||||
shortName: 'e',
|
||||
description: '从生成中排除指定的tags(逗号分隔)',
|
||||
type: OptionType.string,
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
|
|
@ -161,9 +167,12 @@ class GenerateCommand extends BaseCommand {
|
|||
// 解析生成选项
|
||||
final options = _parseGenerateOptions(parsedArgs);
|
||||
|
||||
// 根据 includedTags 过滤文档
|
||||
final document =
|
||||
_filterDocumentByTags(mergedDocument, options.includedTags);
|
||||
// 根据 includedTags 和 excludedTags 过滤文档
|
||||
final document = _filterDocumentByTags(
|
||||
mergedDocument,
|
||||
options.includedTags,
|
||||
options.excludedTags,
|
||||
);
|
||||
|
||||
// 使用配置的输出目录
|
||||
final baseDir = SwaggerConfig.generatorDir;
|
||||
|
|
@ -233,6 +242,14 @@ class GenerateCommand extends BaseCommand {
|
|||
|
||||
progress(' 正在生成 $version 版本 API(${versionPaths.length} 个接口)...');
|
||||
|
||||
// 筛选出当前版本实际使用的 controllers
|
||||
final versionTags = versionPaths.expand((p) => p.tags).toSet();
|
||||
final versionControllers = {
|
||||
for (var tag in versionTags)
|
||||
if (document.controllers.containsKey(tag))
|
||||
tag: document.controllers[tag]!
|
||||
};
|
||||
|
||||
// 创建该版本的临时文档
|
||||
final versionDocument = SwaggerDocument(
|
||||
title: document.title,
|
||||
|
|
@ -240,7 +257,7 @@ class GenerateCommand extends BaseCommand {
|
|||
version: document.version,
|
||||
paths: {for (var p in versionPaths) p.path: p},
|
||||
models: document.models,
|
||||
controllers: document.controllers,
|
||||
controllers: versionControllers, // 使用过滤后的 controllers
|
||||
);
|
||||
|
||||
// 创建生成器(使用配置的类名)
|
||||
|
|
@ -439,6 +456,29 @@ class GenerateCommand extends BaseCommand {
|
|||
progress('📂 [配置文件] 按 tags 分组: ${splitByTags ? "是" : "否"}');
|
||||
}
|
||||
|
||||
// 解析 excluded-tags 参数
|
||||
// 优先级:命令行参数 > 配置文件
|
||||
List<String>? excludedTags;
|
||||
final excludedTagsStr = args.getOption<String>('excluded-tags');
|
||||
if (excludedTagsStr != null && excludedTagsStr.isNotEmpty) {
|
||||
// 从命令行参数读取
|
||||
excludedTags = excludedTagsStr
|
||||
.split(',')
|
||||
.map((tag) => tag.trim())
|
||||
.where((tag) => tag.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
if (excludedTags.isNotEmpty) {
|
||||
progress('🚫 [命令行] 排除以下 tags: ${excludedTags.join(", ")}');
|
||||
}
|
||||
} else {
|
||||
// 从配置文件读取
|
||||
excludedTags = ConfigLoader.getExcludedTags();
|
||||
if (excludedTags != null && excludedTags.isNotEmpty) {
|
||||
progress('🚫 [配置文件] 排除以下 tags: ${excludedTags.join(", ")}');
|
||||
}
|
||||
}
|
||||
|
||||
return GenerateOptions(
|
||||
generateModels: hasAnyFlag
|
||||
? (args.getOption<bool>('models') ?? false)
|
||||
|
|
@ -452,6 +492,7 @@ class GenerateCommand extends BaseCommand {
|
|||
useSimpleModels: args.getOption<bool>('simple') ?? false,
|
||||
splitByTags: splitByTags,
|
||||
includedTags: includedTags,
|
||||
excludedTags: excludedTags,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -832,46 +873,60 @@ class GenerateCommand extends BaseCommand {
|
|||
await FileUtils.writeFile(indexPath, buffer.toString());
|
||||
}
|
||||
|
||||
/// 根据 includedTags 过滤文档
|
||||
/// 如果 includedTags 为 null 或空,返回原文档
|
||||
/// 否则只保留包含指定 tags 的 paths 和相关的 models
|
||||
/// 根据 includedTags 和 excludedTags 过滤文档
|
||||
SwaggerDocument _filterDocumentByTags(
|
||||
SwaggerDocument document,
|
||||
List<String>? includedTags,
|
||||
List<String>? excludedTags,
|
||||
) {
|
||||
// 如果没有指定 tags,返回原文档
|
||||
if (includedTags == null || includedTags.isEmpty) {
|
||||
final hasIncludes = includedTags != null && includedTags.isNotEmpty;
|
||||
final hasExcludes = excludedTags != null && excludedTags.isNotEmpty;
|
||||
|
||||
// 如果没有指定任何过滤条件,返回原文档
|
||||
if (!hasIncludes && !hasExcludes) {
|
||||
return document;
|
||||
}
|
||||
|
||||
progress('🔍 过滤文档,只保留 tags: ${includedTags.join(", ")}');
|
||||
progress('🔍 正在根据 tags 过滤文档...');
|
||||
if (hasIncludes) progress(' 只保留 tags: ${includedTags.join(", ")}');
|
||||
if (hasExcludes) progress(' 排除 tags: ${excludedTags.join(", ")}');
|
||||
|
||||
// 过滤 paths:只保留包含指定 tags 的 paths
|
||||
// 过滤 paths
|
||||
final filteredPaths = <String, ApiPath>{};
|
||||
final usedModelNames = <String>{};
|
||||
|
||||
for (final entry in document.paths.entries) {
|
||||
final path = entry.value;
|
||||
final pathTags = path.tags;
|
||||
|
||||
// 检查该 path 是否包含任何指定的 tag
|
||||
final hasIncludedTag = path.tags.any((tag) => includedTags.contains(tag));
|
||||
|
||||
if (hasIncludedTag) {
|
||||
filteredPaths[entry.key] = path;
|
||||
|
||||
// 收集该 path 使用的所有 model 名称
|
||||
_collectUsedModels(path, usedModelNames);
|
||||
// 1. Inclusion check: 如果提供了 included_tags,则路径必须至少有一个 tag 在列表中
|
||||
final included =
|
||||
!hasIncludes || pathTags.any((tag) => includedTags.contains(tag));
|
||||
if (!included) {
|
||||
continue; // 不满足包含条件,跳过
|
||||
}
|
||||
|
||||
// 2. Exclusion check: 如果提供了 excluded_tags,则路径的所有 tags 不能都在排除列表中
|
||||
// 换句话说,如果路径的所有 tags 都在排除列表中,则排除该路径。
|
||||
final excluded = hasExcludes &&
|
||||
pathTags.isNotEmpty &&
|
||||
pathTags.every((tag) => excludedTags.contains(tag));
|
||||
if (excluded) {
|
||||
continue; // 满足排除条件,跳过
|
||||
}
|
||||
|
||||
// 如果路径通过了所有检查,则保留它
|
||||
filteredPaths[entry.key] = path;
|
||||
_collectUsedModels(path, usedModelNames);
|
||||
}
|
||||
|
||||
progress(' 保留了 ${filteredPaths.length}/${document.paths.length} 个接口');
|
||||
|
||||
// 过滤 models:只保留被使用的 models
|
||||
// 过滤 models:只保留被使用的 models (此逻辑与之前相同)
|
||||
final filteredModels = <String, ApiModel>{};
|
||||
final modelsToCheck = Set<String>.from(usedModelNames);
|
||||
final checkedModels = <String>{};
|
||||
|
||||
// 递归收集所有依赖的 models
|
||||
while (modelsToCheck.isNotEmpty) {
|
||||
final modelName = modelsToCheck.first;
|
||||
modelsToCheck.remove(modelName);
|
||||
|
|
@ -885,19 +940,25 @@ class GenerateCommand extends BaseCommand {
|
|||
final model = document.models[modelName];
|
||||
if (model != null) {
|
||||
filteredModels[modelName] = model;
|
||||
|
||||
// 收集该 model 依赖的其他 models
|
||||
_collectModelDependencies(model, modelsToCheck, checkedModels);
|
||||
}
|
||||
}
|
||||
|
||||
progress(' 保留了 ${filteredModels.length}/${document.models.length} 个模型');
|
||||
|
||||
// 过滤 controllers:只保留包含指定 tags 的 controllers
|
||||
// 过滤 controllers
|
||||
final filteredControllers = <String, ApiController>{};
|
||||
for (final entry in document.controllers.entries) {
|
||||
if (includedTags.contains(entry.key)) {
|
||||
filteredControllers[entry.key] = entry.value;
|
||||
final tagName = entry.key;
|
||||
bool shouldKeep = true;
|
||||
if (hasIncludes && !includedTags.contains(tagName)) {
|
||||
shouldKeep = false;
|
||||
}
|
||||
if (hasExcludes && excludedTags.contains(tagName)) {
|
||||
shouldKeep = false;
|
||||
}
|
||||
if (shouldKeep) {
|
||||
filteredControllers[tagName] = entry.value;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -920,40 +981,55 @@ class GenerateCommand extends BaseCommand {
|
|||
|
||||
/// 收集 ApiPath 使用的所有 model 名称
|
||||
void _collectUsedModels(ApiPath path, Set<String> usedModelNames) {
|
||||
// 递归地从 schema 中提取模型名称
|
||||
void extractModelsFromSchema(Map<String, dynamic> schema) {
|
||||
if (schema.containsKey('\$ref')) {
|
||||
final modelName = _extractModelNameFromRef(schema['\$ref']);
|
||||
if (modelName != null) {
|
||||
usedModelNames.add(modelName);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (schema.containsKey('type')) {
|
||||
final type = schema['type'];
|
||||
if (type == 'array' && schema.containsKey('items')) {
|
||||
extractModelsFromSchema(schema['items']);
|
||||
} else if (type == 'object' && schema.containsKey('properties')) {
|
||||
final properties = schema['properties'] as Map<String, dynamic>;
|
||||
for (final propSchema in properties.values) {
|
||||
extractModelsFromSchema(propSchema);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final key in ['allOf', 'anyOf', 'oneOf']) {
|
||||
if (schema.containsKey(key)) {
|
||||
final subSchemas = schema[key] as List<dynamic>;
|
||||
for (final subSchema in subSchemas) {
|
||||
extractModelsFromSchema(subSchema);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从 requestBody 收集
|
||||
if (path.requestBody != null) {
|
||||
final content = path.requestBody!.content;
|
||||
for (final mediaType in content.values) {
|
||||
for (final mediaType in path.requestBody!.content.values) {
|
||||
if (mediaType.schema != null) {
|
||||
final ref = mediaType.schema!['\$ref'] as String?;
|
||||
if (ref != null) {
|
||||
final modelName = _extractModelNameFromRef(ref);
|
||||
if (modelName != null) {
|
||||
usedModelNames.add(modelName);
|
||||
}
|
||||
}
|
||||
extractModelsFromSchema(mediaType.schema!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从 responses 收集
|
||||
for (final response in path.responses.values) {
|
||||
final content = response.content;
|
||||
for (final mediaType in content.values) {
|
||||
for (final mediaType in response.content.values) {
|
||||
if (mediaType.schema != null) {
|
||||
final ref = mediaType.schema!['\$ref'] as String?;
|
||||
if (ref != null) {
|
||||
final modelName = _extractModelNameFromRef(ref);
|
||||
if (modelName != null) {
|
||||
usedModelNames.add(modelName);
|
||||
}
|
||||
}
|
||||
extractModelsFromSchema(mediaType.schema!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从 parameters 收集 - ApiParameter 没有 schema 字段,跳过
|
||||
// parameters 通常是基本类型,不需要收集
|
||||
}
|
||||
|
||||
/// 收集 ApiModel 依赖的其他 models
|
||||
|
|
@ -1019,6 +1095,7 @@ class GenerateOptions {
|
|||
final bool useSimpleModels;
|
||||
final bool splitByTags;
|
||||
final List<String>? includedTags;
|
||||
final List<String>? excludedTags;
|
||||
|
||||
const GenerateOptions({
|
||||
required this.generateModels,
|
||||
|
|
@ -1027,5 +1104,6 @@ class GenerateOptions {
|
|||
required this.useSimpleModels,
|
||||
required this.splitByTags,
|
||||
this.includedTags,
|
||||
this.excludedTags,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -438,43 +438,17 @@ class ConfigLoader {
|
|||
/// 获取 BaseResult 导入路径
|
||||
static String getBaseResultImport([Map<String, dynamic>? config]) {
|
||||
final cfg = config ?? loadConfig();
|
||||
if (cfg == null) {
|
||||
return 'package:learning_officer_oa/common/models/common/base_result.dart';
|
||||
}
|
||||
|
||||
final generation = cfg['generation'] as Map<String, dynamic>?;
|
||||
if (generation == null) {
|
||||
return 'package:learning_officer_oa/common/models/common/base_result.dart';
|
||||
}
|
||||
|
||||
final api = generation['api'] as Map<String, dynamic>?;
|
||||
if (api == null) {
|
||||
return 'package:learning_officer_oa/common/models/common/base_result.dart';
|
||||
}
|
||||
|
||||
return api['base_result_import'] as String? ??
|
||||
'package:learning_officer_oa/common/models/common/base_result.dart';
|
||||
final generation = cfg?['generation'] as Map<String, dynamic>?;
|
||||
final api = generation?['api'] as Map<String, dynamic>?;
|
||||
return api?['base_result_import'] as String? ?? '';
|
||||
}
|
||||
|
||||
/// 获取 BasePageResult 导入路径
|
||||
static String getBasePageResultImport([Map<String, dynamic>? config]) {
|
||||
final cfg = config ?? loadConfig();
|
||||
if (cfg == null) {
|
||||
return 'package:learning_officer_oa/common/models/common/base_page_result.dart';
|
||||
}
|
||||
|
||||
final generation = cfg['generation'] as Map<String, dynamic>?;
|
||||
if (generation == null) {
|
||||
return 'package:learning_officer_oa/common/models/common/base_page_result.dart';
|
||||
}
|
||||
|
||||
final api = generation['api'] as Map<String, dynamic>?;
|
||||
if (api == null) {
|
||||
return 'package:learning_officer_oa/common/models/common/base_page_result.dart';
|
||||
}
|
||||
|
||||
return api['base_page_result_import'] as String? ??
|
||||
'package:learning_officer_oa/common/models/common/base_page_result.dart';
|
||||
final generation = cfg?['generation'] as Map<String, dynamic>?;
|
||||
final api = generation?['api'] as Map<String, dynamic>?;
|
||||
return api?['base_page_result_import'] as String? ?? '';
|
||||
}
|
||||
|
||||
/// 获取 API Client 类名
|
||||
|
|
@ -556,6 +530,33 @@ class ConfigLoader {
|
|||
return result.isEmpty ? null : result;
|
||||
}
|
||||
|
||||
/// 获取排除的 tags 列表
|
||||
/// 从配置文件的 output.excluded_tags 读取
|
||||
/// 如果未配置,返回 null
|
||||
static List<String>? getExcludedTags([Map<String, dynamic>? config]) {
|
||||
final cfg = config ?? loadConfig();
|
||||
if (cfg == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final output = cfg['output'] as Map<String, dynamic>?;
|
||||
if (output == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final excludedTags = output['excluded_tags'];
|
||||
if (excludedTags is! List) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final result = excludedTags
|
||||
.map((tag) => tag.toString().trim())
|
||||
.where((tag) => tag.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
return result.isEmpty ? null : result;
|
||||
}
|
||||
|
||||
/// 获取是否按 tags 分组生成 API 文件
|
||||
/// 从配置文件的 output.split_by_tags 读取
|
||||
/// 默认: true
|
||||
|
|
|
|||
|
|
@ -169,13 +169,12 @@ class RetrofitApiGenerator extends BaseGenerator {
|
|||
buffer.writeln('');
|
||||
|
||||
// package: 第三方库导入(按字母顺序)
|
||||
if (useDio) {
|
||||
buffer.writeln('import \'package:dio/dio.dart\';');
|
||||
}
|
||||
if (useRetrofit) {
|
||||
buffer.writeln('import \'package:retrofit/retrofit.dart\';');
|
||||
buffer
|
||||
.writeln('import \'package:json_annotation/json_annotation.dart\';');
|
||||
} else if (useDio) {
|
||||
buffer.writeln('import \'package:dio/dio.dart\';');
|
||||
}
|
||||
|
||||
// 其他工具包导入
|
||||
|
|
@ -1368,11 +1367,10 @@ class RetrofitApiGenerator extends BaseGenerator {
|
|||
// 每组之间用空行分隔
|
||||
|
||||
// package: 第三方库导入(按字母顺序)
|
||||
if (useDio) {
|
||||
buffer.writeln('import \'package:dio/dio.dart\';');
|
||||
}
|
||||
if (useRetrofit) {
|
||||
buffer.writeln('import \'package:retrofit/retrofit.dart\';');
|
||||
} else if (useDio) {
|
||||
buffer.writeln('import \'package:dio/dio.dart\';');
|
||||
}
|
||||
|
||||
buffer.writeln('');
|
||||
|
|
@ -1430,18 +1428,6 @@ class RetrofitApiGenerator extends BaseGenerator {
|
|||
return '${StringUtils.toPascalCase(tagName)}Api';
|
||||
}
|
||||
|
||||
/// 检查整个文档是否需要导入分页相关类型
|
||||
bool _needsPaginationImportForDocument() {
|
||||
for (final path in document.paths.values) {
|
||||
final returnType = _generateReturnType(path);
|
||||
// 检查返回类型是否包含 BasePageResult
|
||||
if (returnType.contains('BasePageResult')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// 检查是否需要请求体
|
||||
bool _needsRequestBody(ApiPath path) {
|
||||
// 如果有明确定义的 requestBody,则需要
|
||||
|
|
@ -1497,18 +1483,6 @@ class RetrofitApiGenerator extends BaseGenerator {
|
|||
return false;
|
||||
}
|
||||
|
||||
/// 检查指定路径列表是否需要导入分页相关类型
|
||||
bool _needsPaginationImport(List<ApiPath> paths) {
|
||||
for (final path in paths) {
|
||||
final returnType = _generateReturnType(path);
|
||||
// 检查返回类型是否包含 BasePageResult
|
||||
if (returnType.contains('BasePageResult')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// 获取指定路径列表所需的模型导入
|
||||
Set<String> _getRequiredModelImportsForPaths(List<ApiPath> paths) {
|
||||
final imports = <String>{};
|
||||
|
|
|
|||
Loading…
Reference in New Issue