From 2838f007956030d8fb6fe413acb9b3a58926e7d2 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 3 Dec 2025 17:25:25 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BB=A3=E7=A0=81=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E4=BC=98=E5=8C=96=E5=92=8C=E6=B5=8B=E8=AF=95=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🔧 代码优化 - 优化代码格式,去除多余空行 - 改进枚举映射配置解析逻辑 - 优化字符串处理 📝 配置更新 - example/generator_config.yaml: 添加 v3 swagger URL 配置 ✅ 测试更新 - 更新分页包裹测试 - 更新文本清理测试 --- example/generator_config.yaml | 3 +- example/pubspec.lock | 2 +- lib/core/config_repository.dart | 18 ++++----- lib/core/models/api_paths.dart | 9 +++++ lib/core/models/api_schema.dart | 6 +-- .../impl/model/model_content_builders.dart | 11 ++--- .../retrofit_api/api_parameter_entities.dart | 2 +- .../impl/retrofit_api/api_parameters.dart | 31 ++++++++++++++ lib/utils/reference_resolver.dart | 5 ++- lib/utils/string_utils/naming_converter.dart | 33 ++++++++++++++- test/pagination_wrapping_test.dart | 7 ++-- test/text_cleaner_test.dart | 40 +++++++++---------- 12 files changed, 119 insertions(+), 48 deletions(-) diff --git a/example/generator_config.yaml b/example/generator_config.yaml index 829089a..0c9c861 100644 --- a/example/generator_config.yaml +++ b/example/generator_config.yaml @@ -19,7 +19,8 @@ input: enabled: true - url: "http://192.168.2.7:17288/swagger/v2/swagger.json" enabled: true - + - url: "http://192.168.2.7:17288/swagger/v3/swagger.json" + enabled: true # 验证配置 validate_schema: true strict_mode: false diff --git a/example/pubspec.lock b/example/pubspec.lock index 732aa6d..cbf585c 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -558,7 +558,7 @@ packages: path: ".." relative: true source: path - version: "3.0.0" + version: "3.1.0" term_glyph: dependency: transitive description: diff --git a/lib/core/config_repository.dart b/lib/core/config_repository.dart index a489a88..dcaabb2 100644 --- a/lib/core/config_repository.dart +++ b/lib/core/config_repository.dart @@ -298,22 +298,22 @@ class ConfigRepository { final generation = _config['generation'] as Map?; final models = generation?['models'] as Map?; final mappings = models?['enum_key_mappings'] as Map?; - + if (mappings == null) return null; - + final result = >{}; - + mappings.forEach((enumName, enumMappings) { if (enumMappings is! List) return; - + final valueMap = {}; for (final mapping in enumMappings) { if (mapping is! Map) continue; - + final value = mapping['value']; final name = mapping['name'] as String?; final description = mapping['description'] as String?; - + if (value != null && name != null) { valueMap[value] = EnumKeyMapping( name: name, @@ -321,12 +321,12 @@ class ConfigRepository { ); } } - + if (valueMap.isNotEmpty) { - result[enumName.toString()] = valueMap; + result[enumName] = valueMap; } }); - + return result.isEmpty ? null : result; } diff --git a/lib/core/models/api_paths.dart b/lib/core/models/api_paths.dart index 89d6ab7..d27364e 100644 --- a/lib/core/models/api_paths.dart +++ b/lib/core/models/api_paths.dart @@ -115,11 +115,16 @@ class ApiParameter { this.format, this.example, this.defaultValue, + this.schemaRef, }); /// 从JSON创建ApiParameter factory ApiParameter.fromJson(Map json) { final schema = json['schema'] as Map?; + + // 检查是否有 $ref 引用 + final schemaRef = schema?[r'$ref'] as String?; + final type = schema?['type'] as String? ?? json['type'] as String? ?? 'string'; @@ -132,6 +137,7 @@ class ApiParameter { format: schema?['format'] as String? ?? json['format'] as String?, example: json['example'], defaultValue: schema?['default'] ?? json['default'], + schemaRef: schemaRef, ); } final String name; @@ -142,6 +148,9 @@ class ApiParameter { final String? format; final dynamic example; final dynamic defaultValue; + + /// Schema 引用 (如 #/components/schemas/SysTaskTypeEnums) + final String? schemaRef; } /// API响应信息 (OpenAPI 3.0) diff --git a/lib/core/models/api_schema.dart b/lib/core/models/api_schema.dart index a48cf21..86ea8c7 100644 --- a/lib/core/models/api_schema.dart +++ b/lib/core/models/api_schema.dart @@ -371,7 +371,7 @@ class ApiModel { final isEnum = json['enum'] != null; final enumValues = isEnum ? (json['enum'] as List?) ?? [] : []; - + // 解析 OpenAPI 扩展字段:x-enum-varnames 和 x-enum-descriptions final enumVarNames = json['x-enum-varnames'] != null ? (json['x-enum-varnames'] as List?) @@ -466,11 +466,11 @@ class ApiModel { final bool isEnum; final List enumValues; final PropertyType? enumType; - + /// OpenAPI extension: x-enum-varnames /// 枚举键名列表,与 enumValues 一一对应 final List? enumVarNames; - + /// OpenAPI extension: x-enum-descriptions /// 枚举描述列表,与 enumValues 一一对应 final List? enumDescriptions; diff --git a/lib/pipeline/generate/impl/model/model_content_builders.dart b/lib/pipeline/generate/impl/model/model_content_builders.dart index c78f82d..ce4d91c 100644 --- a/lib/pipeline/generate/impl/model/model_content_builders.dart +++ b/lib/pipeline/generate/impl/model/model_content_builders.dart @@ -30,10 +30,10 @@ String _generateEnumCodeWithoutImports(ApiModel model) { for (var i = 0; i < model.enumValues.length; i++) { final value = model.enumValues[i]; - + String enumName; String? description; - + // 优先级 1: 配置文件映射 if (enumMappings != null && enumMappings.containsKey(value)) { final mapping = enumMappings[value]!; @@ -44,7 +44,8 @@ String _generateEnumCodeWithoutImports(ApiModel model) { else if (model.enumVarNames != null && i < model.enumVarNames!.length) { enumName = model.enumVarNames![i]; // 使用 x-enum-descriptions - if (model.enumDescriptions != null && i < model.enumDescriptions!.length) { + if (model.enumDescriptions != null && + i < model.enumDescriptions!.length) { description = model.enumDescriptions![i]; } } @@ -52,12 +53,12 @@ String _generateEnumCodeWithoutImports(ApiModel model) { else { enumName = StringHelper.generateEnumValueName(value, i); } - + // 添加描述注释 if (description != null && description.isNotEmpty) { buffer.writeln(' /// $description'); } - + final enumLine = enumType == 'integer' || enumType == 'number' ? ' $enumName($value),' : " $enumName('$value'),"; diff --git a/lib/pipeline/generate/impl/retrofit_api/api_parameter_entities.dart b/lib/pipeline/generate/impl/retrofit_api/api_parameter_entities.dart index 51f0818..2539ebd 100644 --- a/lib/pipeline/generate/impl/retrofit_api/api_parameter_entities.dart +++ b/lib/pipeline/generate/impl/retrofit_api/api_parameter_entities.dart @@ -34,7 +34,7 @@ mixin RetrofitApiParameterEntities { ..writeln('class $className {'); for (final param in queryParams) { final dartName = StringHelper.toDartPropertyName(param.name); - final dartType = _g._getDartType(param.type); + final dartType = _g._getDartTypeForParameter(param); final nullable = param.required ? '' : '?'; final cleanDescription = param.description diff --git a/lib/pipeline/generate/impl/retrofit_api/api_parameters.dart b/lib/pipeline/generate/impl/retrofit_api/api_parameters.dart index 41776f2..0d7893d 100644 --- a/lib/pipeline/generate/impl/retrofit_api/api_parameters.dart +++ b/lib/pipeline/generate/impl/retrofit_api/api_parameters.dart @@ -143,6 +143,37 @@ mixin RetrofitApiParameters { String _getDartType(PropertyType type) => _typeMap[type] ?? 'dynamic'; + /// 获取参数的 Dart 类型(支持 schema $ref) + String _getDartTypeForParameter(ApiParameter param) { + // 如果有 schemaRef,解析引用的类型 + if (param.schemaRef != null) { + final refName = param.schemaRef!.split('/').last; + + // 检查引用的 schema 是否是枚举类型 + final refModel = _g.document.models[refName]; + if (refModel != null) { + // 如果引用的是枚举类型,返回枚举的基础类型 + if (refModel.isEnum && refModel.type != null) { + switch (refModel.type) { + case 'integer': + return 'int'; + case 'number': + return 'double'; + case 'string': + return 'String'; + default: + return 'String'; + } + } + // 如果是其他引用类型,返回类名 + return StringHelper.generateClassName(refName); + } + } + + // 否则使用默认的类型映射 + return _getDartType(param.type); + } + /// 检查是否需要请求体 bool _needsRequestBody(ApiPath path) { if (path.requestBody != null) { diff --git a/lib/utils/reference_resolver.dart b/lib/utils/reference_resolver.dart index 6b5e77d..a21df52 100644 --- a/lib/utils/reference_resolver.dart +++ b/lib/utils/reference_resolver.dart @@ -130,8 +130,8 @@ class ReferenceResolver { /// 解析枚举模型 ApiModel _parseEnumModel(String name, Map json) { final enumValues = List.from((json['enum'] as List?) ?? []); - final enumType = - PropertyType.fromString(json['type'] as String? ?? 'string'); + final type = json['type'] as String?; + final enumType = PropertyType.fromString(type ?? 'string'); return ApiModel( name: name, @@ -141,6 +141,7 @@ class ReferenceResolver { isEnum: true, enumValues: enumValues, enumType: enumType, + type: type, ); } diff --git a/lib/utils/string_utils/naming_converter.dart b/lib/utils/string_utils/naming_converter.dart index a3281cc..f390588 100644 --- a/lib/utils/string_utils/naming_converter.dart +++ b/lib/utils/string_utils/naming_converter.dart @@ -21,17 +21,46 @@ class NamingConverter { final parts = input.split('_').where((p) => p.isNotEmpty).toList(); if (parts.isEmpty) return input; - var result = parts.first.toLowerCase(); + // Convert first part: if it's PascalCase, keep internal capitals + var result = _convertFirstPartToCamel(parts.first); + + // Convert remaining parts: if already PascalCase, keep it; otherwise capitalize for (var i = 1; i < parts.length; i++) { final part = parts[i]; if (part.isNotEmpty) { - result += part[0].toUpperCase() + part.substring(1).toLowerCase(); + result += _convertPartToPascal(part); } } return result.isEmpty ? input : result; } + /// Convert the first part to camelCase (preserve internal capitals) + static String _convertFirstPartToCamel(String part) { + if (part.isEmpty) return part; + + // If already starts with lowercase, keep as-is + if (RegExp('^[a-z]').hasMatch(part)) { + return part; + } + + // If PascalCase (starts with uppercase), just lowercase first letter + return part[0].toLowerCase() + part.substring(1); + } + + /// Convert a part to PascalCase (preserve internal capitals) + static String _convertPartToPascal(String part) { + if (part.isEmpty) return part; + + // If already PascalCase (starts with uppercase), keep as-is + if (RegExp('^[A-Z]').hasMatch(part)) { + return part; + } + + // If starts with lowercase, capitalize first letter + return part[0].toUpperCase() + part.substring(1); + } + /// Convert to PascalCase static String toPascalCase(String input) { if (input.isEmpty) return input; diff --git a/test/pagination_wrapping_test.dart b/test/pagination_wrapping_test.dart index da5e65d..d62a8be 100644 --- a/test/pagination_wrapping_test.dart +++ b/test/pagination_wrapping_test.dart @@ -8,7 +8,7 @@ void main() { group('BasePageResult 包裹逻辑测试', () { test('应该识别包含 total 和 items 的分页响应模型', () { // 创建一个包含 total 和 items 的模型 - final paginationModel = ApiModel( + const paginationModel = ApiModel( name: 'SuperiorTaskListResultPageResponse', description: '分页响应实体类', properties: { @@ -59,7 +59,7 @@ void main() { }); test('分页模型的 items 类型应该被正确提取', () { - final itemsProperty = ApiProperty( + const itemsProperty = ApiProperty( name: 'items', type: PropertyType.array, description: '数据列表', @@ -77,7 +77,7 @@ void main() { }); test('非分页模型不应该被识别为分页模型', () { - final normalModel = ApiModel( + const normalModel = ApiModel( name: 'NormalResult', description: '普通响应', properties: { @@ -121,4 +121,3 @@ void main() { }); }); } - diff --git a/test/text_cleaner_test.dart b/test/text_cleaner_test.dart index 2587b29..3769ae3 100644 --- a/test/text_cleaner_test.dart +++ b/test/text_cleaner_test.dart @@ -7,7 +7,7 @@ void main() { test('removes newlines from text', () { const input = '部长新增工作任务指标\n(会删除所有管理的班级任务指标-删除所有管理的学习官的通用任务指标)'; final result = TextCleaner.cleanDescription(input); - + expect(result, isNot(contains('\n'))); expect(result, isNot(contains('\r'))); expect(result, '部长新增工作任务指标 (会删除所有管理的班级任务指标-删除所有管理的学习官的通用任务指标)'); @@ -16,7 +16,7 @@ void main() { test('removes carriage returns from text', () { const input = 'Line 1\r\nLine 2\rLine 3'; final result = TextCleaner.cleanDescription(input); - + expect(result, isNot(contains('\n'))); expect(result, isNot(contains('\r'))); expect(result, 'Line 1 Line 2 Line 3'); @@ -25,42 +25,42 @@ void main() { test('replaces multiple spaces with single space', () { const input = 'Text with multiple spaces'; final result = TextCleaner.cleanDescription(input); - + expect(result, 'Text with multiple spaces'); }); test('removes HTML tags', () { const input = '

Text with HTML tags

'; final result = TextCleaner.cleanDescription(input); - + expect(result, 'Text with HTML tags'); }); test('escapes comment end markers', () { const input = 'Text with */ comment end'; final result = TextCleaner.cleanDescription(input); - + expect(result, 'Text with * / comment end'); }); test('trims leading and trailing whitespace', () { const input = ' Text with spaces '; final result = TextCleaner.cleanDescription(input); - + expect(result, 'Text with spaces'); }); test('handles empty string', () { const input = ''; final result = TextCleaner.cleanDescription(input); - + expect(result, ''); }); test('handles complex Chinese text with newlines', () { const input = '获取用户信息\n包含用户的基本信息和扩展信息'; final result = TextCleaner.cleanDescription(input); - + expect(result, isNot(contains('\n'))); expect(result, '获取用户信息 包含用户的基本信息和扩展信息'); }); @@ -68,7 +68,7 @@ void main() { test('handles text with parentheses and newlines', () { const input = '部长新增工作任务指标\n(会删除所有管理的班级任务指标-删除所有管理的学习官的通用任务指标)'; final result = TextCleaner.cleanDescription(input); - + // Should not contain newlines expect(result, isNot(contains('\n'))); // Should preserve parentheses @@ -83,33 +83,34 @@ void main() { test('normalizes line endings', () { const input = 'Line 1\r\nLine 2\rLine 3\nLine 4'; final result = TextCleaner.normalize(input); - + expect(result, 'Line 1\nLine 2\nLine 3\nLine 4'); }); test('removes excessive blank lines', () { const input = 'Line 1\n\n\n\nLine 2'; final result = TextCleaner.normalize(input); - + expect(result, 'Line 1\n\nLine 2'); }); test('trims whitespace', () { const input = ' Text '; final result = TextCleaner.normalize(input); - + expect(result, 'Text'); }); }); group('escapeString', () { test('escapes special characters', () { - const input = "Text with 'quotes' and \"double quotes\" and \n newlines"; + const input = + "Text with 'quotes' and \"double quotes\" and \n newlines"; final result = TextCleaner.escapeString(input); - - expect(result, contains("\\'")); - expect(result, contains('\\"')); - expect(result, contains('\\n')); + + expect(result, contains(r"\'")); + expect(result, contains(r'\"')); + expect(result, contains(r'\n')); }); }); @@ -117,7 +118,7 @@ void main() { test('truncates long text', () { const input = 'This is a very long text that needs to be truncated'; final result = TextCleaner.truncate(input, 20); - + expect(result.length, lessThanOrEqualTo(20)); expect(result, endsWith('...')); }); @@ -125,10 +126,9 @@ void main() { test('does not truncate short text', () { const input = 'Short text'; final result = TextCleaner.truncate(input, 20); - + expect(result, input); }); }); }); } -