refactor: 代码格式优化和测试更新

🔧 代码优化
- 优化代码格式,去除多余空行
- 改进枚举映射配置解析逻辑
- 优化字符串处理

📝 配置更新
- example/generator_config.yaml: 添加 v3 swagger URL 配置

 测试更新
- 更新分页包裹测试
- 更新文本清理测试
This commit is contained in:
Max 2025-12-03 17:25:25 +08:00
parent edd53fd487
commit 2838f00795
12 changed files with 119 additions and 48 deletions

View File

@ -19,7 +19,8 @@ input:
enabled: true enabled: true
- url: "http://192.168.2.7:17288/swagger/v2/swagger.json" - url: "http://192.168.2.7:17288/swagger/v2/swagger.json"
enabled: true enabled: true
- url: "http://192.168.2.7:17288/swagger/v3/swagger.json"
enabled: true
# 验证配置 # 验证配置
validate_schema: true validate_schema: true
strict_mode: false strict_mode: false

View File

@ -558,7 +558,7 @@ packages:
path: ".." path: ".."
relative: true relative: true
source: path source: path
version: "3.0.0" version: "3.1.0"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:

View File

@ -298,22 +298,22 @@ class ConfigRepository {
final generation = _config['generation'] as Map<String, dynamic>?; final generation = _config['generation'] as Map<String, dynamic>?;
final models = generation?['models'] as Map<String, dynamic>?; final models = generation?['models'] as Map<String, dynamic>?;
final mappings = models?['enum_key_mappings'] as Map<String, dynamic>?; final mappings = models?['enum_key_mappings'] as Map<String, dynamic>?;
if (mappings == null) return null; if (mappings == null) return null;
final result = <String, Map<dynamic, EnumKeyMapping>>{}; final result = <String, Map<dynamic, EnumKeyMapping>>{};
mappings.forEach((enumName, enumMappings) { mappings.forEach((enumName, enumMappings) {
if (enumMappings is! List) return; if (enumMappings is! List) return;
final valueMap = <dynamic, EnumKeyMapping>{}; final valueMap = <dynamic, EnumKeyMapping>{};
for (final mapping in enumMappings) { for (final mapping in enumMappings) {
if (mapping is! Map) continue; if (mapping is! Map) continue;
final value = mapping['value']; final value = mapping['value'];
final name = mapping['name'] as String?; final name = mapping['name'] as String?;
final description = mapping['description'] as String?; final description = mapping['description'] as String?;
if (value != null && name != null) { if (value != null && name != null) {
valueMap[value] = EnumKeyMapping( valueMap[value] = EnumKeyMapping(
name: name, name: name,
@ -321,12 +321,12 @@ class ConfigRepository {
); );
} }
} }
if (valueMap.isNotEmpty) { if (valueMap.isNotEmpty) {
result[enumName.toString()] = valueMap; result[enumName] = valueMap;
} }
}); });
return result.isEmpty ? null : result; return result.isEmpty ? null : result;
} }

View File

@ -115,11 +115,16 @@ class ApiParameter {
this.format, this.format,
this.example, this.example,
this.defaultValue, this.defaultValue,
this.schemaRef,
}); });
/// JSON创建ApiParameter /// JSON创建ApiParameter
factory ApiParameter.fromJson(Map<String, dynamic> json) { factory ApiParameter.fromJson(Map<String, dynamic> json) {
final schema = json['schema'] as Map<String, dynamic>?; final schema = json['schema'] as Map<String, dynamic>?;
// $ref
final schemaRef = schema?[r'$ref'] as String?;
final type = final type =
schema?['type'] as String? ?? json['type'] as String? ?? 'string'; schema?['type'] as String? ?? json['type'] as String? ?? 'string';
@ -132,6 +137,7 @@ class ApiParameter {
format: schema?['format'] as String? ?? json['format'] as String?, format: schema?['format'] as String? ?? json['format'] as String?,
example: json['example'], example: json['example'],
defaultValue: schema?['default'] ?? json['default'], defaultValue: schema?['default'] ?? json['default'],
schemaRef: schemaRef,
); );
} }
final String name; final String name;
@ -142,6 +148,9 @@ class ApiParameter {
final String? format; final String? format;
final dynamic example; final dynamic example;
final dynamic defaultValue; final dynamic defaultValue;
/// Schema ( #/components/schemas/SysTaskTypeEnums)
final String? schemaRef;
} }
/// API响应信息 (OpenAPI 3.0) /// API响应信息 (OpenAPI 3.0)

View File

@ -371,7 +371,7 @@ class ApiModel {
final isEnum = json['enum'] != null; final isEnum = json['enum'] != null;
final enumValues = final enumValues =
isEnum ? (json['enum'] as List<dynamic>?) ?? <dynamic>[] : <dynamic>[]; isEnum ? (json['enum'] as List<dynamic>?) ?? <dynamic>[] : <dynamic>[];
// OpenAPI x-enum-varnames x-enum-descriptions // OpenAPI x-enum-varnames x-enum-descriptions
final enumVarNames = json['x-enum-varnames'] != null final enumVarNames = json['x-enum-varnames'] != null
? (json['x-enum-varnames'] as List<dynamic>?) ? (json['x-enum-varnames'] as List<dynamic>?)
@ -466,11 +466,11 @@ class ApiModel {
final bool isEnum; final bool isEnum;
final List<dynamic> enumValues; final List<dynamic> enumValues;
final PropertyType? enumType; final PropertyType? enumType;
/// OpenAPI extension: x-enum-varnames /// OpenAPI extension: x-enum-varnames
/// enumValues /// enumValues
final List<String>? enumVarNames; final List<String>? enumVarNames;
/// OpenAPI extension: x-enum-descriptions /// OpenAPI extension: x-enum-descriptions
/// enumValues /// enumValues
final List<String>? enumDescriptions; final List<String>? enumDescriptions;

View File

@ -30,10 +30,10 @@ String _generateEnumCodeWithoutImports(ApiModel model) {
for (var i = 0; i < model.enumValues.length; i++) { for (var i = 0; i < model.enumValues.length; i++) {
final value = model.enumValues[i]; final value = model.enumValues[i];
String enumName; String enumName;
String? description; String? description;
// 1: // 1:
if (enumMappings != null && enumMappings.containsKey(value)) { if (enumMappings != null && enumMappings.containsKey(value)) {
final mapping = enumMappings[value]!; final mapping = enumMappings[value]!;
@ -44,7 +44,8 @@ String _generateEnumCodeWithoutImports(ApiModel model) {
else if (model.enumVarNames != null && i < model.enumVarNames!.length) { else if (model.enumVarNames != null && i < model.enumVarNames!.length) {
enumName = model.enumVarNames![i]; enumName = model.enumVarNames![i];
// 使 x-enum-descriptions // 使 x-enum-descriptions
if (model.enumDescriptions != null && i < model.enumDescriptions!.length) { if (model.enumDescriptions != null &&
i < model.enumDescriptions!.length) {
description = model.enumDescriptions![i]; description = model.enumDescriptions![i];
} }
} }
@ -52,12 +53,12 @@ String _generateEnumCodeWithoutImports(ApiModel model) {
else { else {
enumName = StringHelper.generateEnumValueName(value, i); enumName = StringHelper.generateEnumValueName(value, i);
} }
// //
if (description != null && description.isNotEmpty) { if (description != null && description.isNotEmpty) {
buffer.writeln(' /// $description'); buffer.writeln(' /// $description');
} }
final enumLine = enumType == 'integer' || enumType == 'number' final enumLine = enumType == 'integer' || enumType == 'number'
? ' $enumName($value),' ? ' $enumName($value),'
: " $enumName('$value'),"; : " $enumName('$value'),";

View File

@ -34,7 +34,7 @@ mixin RetrofitApiParameterEntities {
..writeln('class $className {'); ..writeln('class $className {');
for (final param in queryParams) { for (final param in queryParams) {
final dartName = StringHelper.toDartPropertyName(param.name); final dartName = StringHelper.toDartPropertyName(param.name);
final dartType = _g._getDartType(param.type); final dartType = _g._getDartTypeForParameter(param);
final nullable = param.required ? '' : '?'; final nullable = param.required ? '' : '?';
final cleanDescription = param.description final cleanDescription = param.description

View File

@ -143,6 +143,37 @@ mixin RetrofitApiParameters {
String _getDartType(PropertyType type) => _typeMap[type] ?? 'dynamic'; 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) { bool _needsRequestBody(ApiPath path) {
if (path.requestBody != null) { if (path.requestBody != null) {

View File

@ -130,8 +130,8 @@ class ReferenceResolver {
/// ///
ApiModel _parseEnumModel(String name, Map<String, dynamic> json) { ApiModel _parseEnumModel(String name, Map<String, dynamic> json) {
final enumValues = List<dynamic>.from((json['enum'] as List?) ?? []); final enumValues = List<dynamic>.from((json['enum'] as List?) ?? []);
final enumType = final type = json['type'] as String?;
PropertyType.fromString(json['type'] as String? ?? 'string'); final enumType = PropertyType.fromString(type ?? 'string');
return ApiModel( return ApiModel(
name: name, name: name,
@ -141,6 +141,7 @@ class ReferenceResolver {
isEnum: true, isEnum: true,
enumValues: enumValues, enumValues: enumValues,
enumType: enumType, enumType: enumType,
type: type,
); );
} }

View File

@ -21,17 +21,46 @@ class NamingConverter {
final parts = input.split('_').where((p) => p.isNotEmpty).toList(); final parts = input.split('_').where((p) => p.isNotEmpty).toList();
if (parts.isEmpty) return input; 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++) { for (var i = 1; i < parts.length; i++) {
final part = parts[i]; final part = parts[i];
if (part.isNotEmpty) { if (part.isNotEmpty) {
result += part[0].toUpperCase() + part.substring(1).toLowerCase(); result += _convertPartToPascal(part);
} }
} }
return result.isEmpty ? input : result; 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 /// Convert to PascalCase
static String toPascalCase(String input) { static String toPascalCase(String input) {
if (input.isEmpty) return input; if (input.isEmpty) return input;

View File

@ -8,7 +8,7 @@ void main() {
group('BasePageResult 包裹逻辑测试', () { group('BasePageResult 包裹逻辑测试', () {
test('应该识别包含 total 和 items 的分页响应模型', () { test('应该识别包含 total 和 items 的分页响应模型', () {
// total items // total items
final paginationModel = ApiModel( const paginationModel = ApiModel(
name: 'SuperiorTaskListResultPageResponse', name: 'SuperiorTaskListResultPageResponse',
description: '分页响应实体类', description: '分页响应实体类',
properties: { properties: {
@ -59,7 +59,7 @@ void main() {
}); });
test('分页模型的 items 类型应该被正确提取', () { test('分页模型的 items 类型应该被正确提取', () {
final itemsProperty = ApiProperty( const itemsProperty = ApiProperty(
name: 'items', name: 'items',
type: PropertyType.array, type: PropertyType.array,
description: '数据列表', description: '数据列表',
@ -77,7 +77,7 @@ void main() {
}); });
test('非分页模型不应该被识别为分页模型', () { test('非分页模型不应该被识别为分页模型', () {
final normalModel = ApiModel( const normalModel = ApiModel(
name: 'NormalResult', name: 'NormalResult',
description: '普通响应', description: '普通响应',
properties: { properties: {
@ -121,4 +121,3 @@ void main() {
}); });
}); });
} }

View File

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