feat: 增加 excluded_tags 支持

This commit is contained in:
Max 2025-11-17 20:07:17 +08:00
parent 03406d3fbb
commit a3f9cb78e6
5 changed files with 180 additions and 113 deletions

View File

@ -163,6 +163,7 @@ void main() async {
#### 高级选项 #### 高级选项
- `--included-tags` / `-i`: 只生成指定 tags 的 API 和模型 - `--included-tags` / `-i`: 只生成指定 tags 的 API 和模型
- `--excluded-tags` / `-e`: 从生成中排除指定的 tags
**示例:** **示例:**
```bash ```bash

View File

@ -46,6 +46,14 @@ output:
# - "Pet" # - "Pet"
# - "Store" # - "Store"
# 从代码生成中排除指定的 tags
# 适用于内部、废弃或不需要的 API
# 如果一个 endpoint 的所有 tags 都被排除,则该 endpoint 不会生成
excluded_tags:
# - "Internal"
# - "Deprecated"
# - "Legacy"
# 跳过的目录列表(这些目录下的文件将不会被生成) # 跳过的目录列表(这些目录下的文件将不会被生成)
# 支持相对路径和绝对路径,支持目录名或完整路径 # 支持相对路径和绝对路径,支持目录名或完整路径
ignored_directories: ignored_directories:
@ -94,10 +102,15 @@ generation:
default_version: "v1" default_version: "v1"
# 基础类型配置(根据您的项目调整) # 基础类型配置(根据您的项目调整)
base_result_type: "BaseResult" # 如果您的项目有统一的响应模型,请在此处配置
base_page_result_type: "BasePageResult" base_result_type: "BaseResult" # 基础响应模型名称
base_result_import: "package:your_project/common/models/base_result.dart" base_page_result_type: "BasePageResult" # 分页响应模型名称
base_page_result_import: "package:your_project/common/models/base_page_result.dart"
# 基础模型的导入路径(可选)
# 如果提供了路径,将在 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 method_naming: "camelCase" # camelCase, snake_case

View File

@ -75,6 +75,12 @@ class GenerateCommand extends BaseCommand {
description: '只生成指定tags的API和模型逗号分隔User,Pet,Store', description: '只生成指定tags的API和模型逗号分隔User,Pet,Store',
type: OptionType.string, type: OptionType.string,
), ),
const CommandOption(
name: 'excluded-tags',
shortName: 'e',
description: '从生成中排除指定的tags逗号分隔',
type: OptionType.string,
),
]; ];
@override @override
@ -161,9 +167,12 @@ class GenerateCommand extends BaseCommand {
// //
final options = _parseGenerateOptions(parsedArgs); final options = _parseGenerateOptions(parsedArgs);
// includedTags // includedTags excludedTags
final document = final document = _filterDocumentByTags(
_filterDocumentByTags(mergedDocument, options.includedTags); mergedDocument,
options.includedTags,
options.excludedTags,
);
// 使 // 使
final baseDir = SwaggerConfig.generatorDir; final baseDir = SwaggerConfig.generatorDir;
@ -233,6 +242,14 @@ class GenerateCommand extends BaseCommand {
progress(' 正在生成 $version 版本 API${versionPaths.length} 个接口)...'); 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( final versionDocument = SwaggerDocument(
title: document.title, title: document.title,
@ -240,7 +257,7 @@ class GenerateCommand extends BaseCommand {
version: document.version, version: document.version,
paths: {for (var p in versionPaths) p.path: p}, paths: {for (var p in versionPaths) p.path: p},
models: document.models, models: document.models,
controllers: document.controllers, controllers: versionControllers, // 使 controllers
); );
// 使 // 使
@ -439,6 +456,29 @@ class GenerateCommand extends BaseCommand {
progress('📂 [配置文件] 按 tags 分组: ${splitByTags ? "" : ""}'); 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( return GenerateOptions(
generateModels: hasAnyFlag generateModels: hasAnyFlag
? (args.getOption<bool>('models') ?? false) ? (args.getOption<bool>('models') ?? false)
@ -452,6 +492,7 @@ class GenerateCommand extends BaseCommand {
useSimpleModels: args.getOption<bool>('simple') ?? false, useSimpleModels: args.getOption<bool>('simple') ?? false,
splitByTags: splitByTags, splitByTags: splitByTags,
includedTags: includedTags, includedTags: includedTags,
excludedTags: excludedTags,
); );
} }
@ -832,46 +873,60 @@ class GenerateCommand extends BaseCommand {
await FileUtils.writeFile(indexPath, buffer.toString()); await FileUtils.writeFile(indexPath, buffer.toString());
} }
/// includedTags /// includedTags excludedTags
/// includedTags null
/// tags paths models
SwaggerDocument _filterDocumentByTags( SwaggerDocument _filterDocumentByTags(
SwaggerDocument document, SwaggerDocument document,
List<String>? includedTags, List<String>? includedTags,
List<String>? excludedTags,
) { ) {
// tags final hasIncludes = includedTags != null && includedTags.isNotEmpty;
if (includedTags == null || includedTags.isEmpty) { final hasExcludes = excludedTags != null && excludedTags.isNotEmpty;
//
if (!hasIncludes && !hasExcludes) {
return document; 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 filteredPaths = <String, ApiPath>{};
final usedModelNames = <String>{}; final usedModelNames = <String>{};
for (final entry in document.paths.entries) { for (final entry in document.paths.entries) {
final path = entry.value; final path = entry.value;
final pathTags = path.tags;
// path tag // 1. Inclusion check: included_tags tag
final hasIncludedTag = path.tags.any((tag) => includedTags.contains(tag)); final included =
!hasIncludes || pathTags.any((tag) => includedTags.contains(tag));
if (hasIncludedTag) { if (!included) {
filteredPaths[entry.key] = path; continue; //
// path 使 model
_collectUsedModels(path, usedModelNames);
} }
// 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} 个接口'); progress(' 保留了 ${filteredPaths.length}/${document.paths.length} 个接口');
// models使 models // models使 models ()
final filteredModels = <String, ApiModel>{}; final filteredModels = <String, ApiModel>{};
final modelsToCheck = Set<String>.from(usedModelNames); final modelsToCheck = Set<String>.from(usedModelNames);
final checkedModels = <String>{}; final checkedModels = <String>{};
// models
while (modelsToCheck.isNotEmpty) { while (modelsToCheck.isNotEmpty) {
final modelName = modelsToCheck.first; final modelName = modelsToCheck.first;
modelsToCheck.remove(modelName); modelsToCheck.remove(modelName);
@ -885,19 +940,25 @@ class GenerateCommand extends BaseCommand {
final model = document.models[modelName]; final model = document.models[modelName];
if (model != null) { if (model != null) {
filteredModels[modelName] = model; filteredModels[modelName] = model;
// model models
_collectModelDependencies(model, modelsToCheck, checkedModels); _collectModelDependencies(model, modelsToCheck, checkedModels);
} }
} }
progress(' 保留了 ${filteredModels.length}/${document.models.length} 个模型'); progress(' 保留了 ${filteredModels.length}/${document.models.length} 个模型');
// controllers tags controllers // controllers
final filteredControllers = <String, ApiController>{}; final filteredControllers = <String, ApiController>{};
for (final entry in document.controllers.entries) { for (final entry in document.controllers.entries) {
if (includedTags.contains(entry.key)) { final tagName = entry.key;
filteredControllers[entry.key] = entry.value; bool shouldKeep = true;
if (hasIncludes && !includedTags.contains(tagName)) {
shouldKeep = false;
}
if (hasExcludes && excludedTags.contains(tagName)) {
shouldKeep = false;
}
if (shouldKeep) {
filteredControllers[tagName] = entry.value;
} }
} }
@ -920,41 +981,56 @@ class GenerateCommand extends BaseCommand {
/// ApiPath 使 model /// ApiPath 使 model
void _collectUsedModels(ApiPath path, Set<String> usedModelNames) { void _collectUsedModels(ApiPath path, Set<String> usedModelNames) {
// requestBody // schema
if (path.requestBody != null) { void extractModelsFromSchema(Map<String, dynamic> schema) {
final content = path.requestBody!.content; if (schema.containsKey('\$ref')) {
for (final mediaType in content.values) { final modelName = _extractModelNameFromRef(schema['\$ref']);
if (mediaType.schema != null) {
final ref = mediaType.schema!['\$ref'] as String?;
if (ref != null) {
final modelName = _extractModelNameFromRef(ref);
if (modelName != null) { if (modelName != null) {
usedModelNames.add(modelName); 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) {
for (final mediaType in path.requestBody!.content.values) {
if (mediaType.schema != null) {
extractModelsFromSchema(mediaType.schema!);
} }
} }
} }
// responses // responses
for (final response in path.responses.values) { for (final response in path.responses.values) {
final content = response.content; for (final mediaType in response.content.values) {
for (final mediaType in content.values) {
if (mediaType.schema != null) { if (mediaType.schema != null) {
final ref = mediaType.schema!['\$ref'] as String?; extractModelsFromSchema(mediaType.schema!);
if (ref != null) {
final modelName = _extractModelNameFromRef(ref);
if (modelName != null) {
usedModelNames.add(modelName);
} }
} }
} }
} }
}
// parameters - ApiParameter schema
// parameters
}
/// ApiModel models /// ApiModel models
void _collectModelDependencies( void _collectModelDependencies(
@ -1019,6 +1095,7 @@ class GenerateOptions {
final bool useSimpleModels; final bool useSimpleModels;
final bool splitByTags; final bool splitByTags;
final List<String>? includedTags; final List<String>? includedTags;
final List<String>? excludedTags;
const GenerateOptions({ const GenerateOptions({
required this.generateModels, required this.generateModels,
@ -1027,5 +1104,6 @@ class GenerateOptions {
required this.useSimpleModels, required this.useSimpleModels,
required this.splitByTags, required this.splitByTags,
this.includedTags, this.includedTags,
this.excludedTags,
}); });
} }

View File

@ -438,43 +438,17 @@ class ConfigLoader {
/// BaseResult /// BaseResult
static String getBaseResultImport([Map<String, dynamic>? config]) { static String getBaseResultImport([Map<String, dynamic>? config]) {
final cfg = config ?? loadConfig(); final cfg = config ?? loadConfig();
if (cfg == null) { final generation = cfg?['generation'] as Map<String, dynamic>?;
return 'package:learning_officer_oa/common/models/common/base_result.dart'; final api = generation?['api'] as Map<String, dynamic>?;
} return api?['base_result_import'] as String? ?? '';
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';
} }
/// BasePageResult /// BasePageResult
static String getBasePageResultImport([Map<String, dynamic>? config]) { static String getBasePageResultImport([Map<String, dynamic>? config]) {
final cfg = config ?? loadConfig(); final cfg = config ?? loadConfig();
if (cfg == null) { final generation = cfg?['generation'] as Map<String, dynamic>?;
return 'package:learning_officer_oa/common/models/common/base_page_result.dart'; final api = generation?['api'] as Map<String, dynamic>?;
} return api?['base_page_result_import'] as String? ?? '';
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';
} }
/// API Client /// API Client
@ -556,6 +530,33 @@ class ConfigLoader {
return result.isEmpty ? null : result; 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 /// tags API
/// output.split_by_tags /// output.split_by_tags
/// : true /// : true

View File

@ -169,13 +169,12 @@ class RetrofitApiGenerator extends BaseGenerator {
buffer.writeln(''); buffer.writeln('');
// package: // package:
if (useDio) {
buffer.writeln('import \'package:dio/dio.dart\';');
}
if (useRetrofit) { if (useRetrofit) {
buffer.writeln('import \'package:retrofit/retrofit.dart\';'); buffer.writeln('import \'package:retrofit/retrofit.dart\';');
buffer buffer
.writeln('import \'package:json_annotation/json_annotation.dart\';'); .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: // package:
if (useDio) {
buffer.writeln('import \'package:dio/dio.dart\';');
}
if (useRetrofit) { if (useRetrofit) {
buffer.writeln('import \'package:retrofit/retrofit.dart\';'); buffer.writeln('import \'package:retrofit/retrofit.dart\';');
} else if (useDio) {
buffer.writeln('import \'package:dio/dio.dart\';');
} }
buffer.writeln(''); buffer.writeln('');
@ -1430,18 +1428,6 @@ class RetrofitApiGenerator extends BaseGenerator {
return '${StringUtils.toPascalCase(tagName)}Api'; 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) { bool _needsRequestBody(ApiPath path) {
// requestBody // requestBody
@ -1497,18 +1483,6 @@ class RetrofitApiGenerator extends BaseGenerator {
return false; 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) { Set<String> _getRequiredModelImportsForPaths(List<ApiPath> paths) {
final imports = <String>{}; final imports = <String>{};