diff --git a/CHANGELOG.md b/CHANGELOG.md index f2b1bd6..a879d2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. +## [3.2.0] - 2026-01-12 + +### 🎉 新特性 + +#### 模型类名前缀支持 +- ✅ **支持配置文件配置**:在 `generator_config.yaml` 中配置 `models.class_prefix`,自动为生成的模型类添加前缀。 +- ✅ **性能优化**:为 `ConfigRepository` 添加缓存机制,减少磁盘 I/O。 ## [3.1.0] - 2025-11-24 ### 🎉 新特性 diff --git a/lib/core/config.dart b/lib/core/config.dart index fb5906e..4fbef0e 100644 --- a/lib/core/config.dart +++ b/lib/core/config.dart @@ -58,6 +58,10 @@ class SwaggerConfig { static JsonSerializableConfig? get jsonSerializableConfig => ConfigRepository.loadSync().jsonSerializableConfig; + /// 获取模型类名前缀(从配置文件读取) + static String? get modelClassPrefix => + ConfigRepository.loadSync().modelClassPrefix; + /// 默认文档文件名 static const String defaultDocumentationFile = 'generated_api_documentation.md'; diff --git a/lib/core/config_repository.dart b/lib/core/config_repository.dart index c1d6a2a..28e5067 100644 --- a/lib/core/config_repository.dart +++ b/lib/core/config_repository.dart @@ -64,8 +64,16 @@ class ConfigRepository { } } + static ConfigRepository? _cachedConfig; + /// 同步加载配置(用于向后兼容或必须同步的场景) + /// 为默认配置路径启用缓存 static ConfigRepository loadSync([String? configPath]) { + // 仅在使用默认路径时使用缓存 + if (configPath == null && _cachedConfig != null) { + return _cachedConfig!; + } + final file = File(configPath ?? PathResolver.findConfigFile() ?? ''); if (!file.existsSync()) { return ConfigRepository({}); @@ -75,7 +83,12 @@ class ConfigRepository { final content = file.readAsStringSync(); final yaml = loadYaml(content); final map = _yamlToMap(yaml); - return ConfigRepository(map); + final config = ConfigRepository(map); + + if (configPath == null) { + _cachedConfig = config; + } + return config; } on Exception catch (e) { appLogger.warning('⚠️ 配置文件解析失败: $e'); return ConfigRepository({}); @@ -323,6 +336,13 @@ class ConfigRepository { return JsonSerializableConfig.fromMap(jsonSerializable); } + /// 获取模型类名前缀 + String? get modelClassPrefix { + final generation = _config['generation'] as Map?; + final models = generation?['models'] as Map?; + return models?['class_prefix'] as String?; + } + /// 获取枚举键名映射配置 /// 返回格式: { "EnumName": { value: { "name": "KEY_NAME", "description": "描述" } } } Map>? get enumKeyMappings { diff --git a/lib/utils/string_helper.dart b/lib/utils/string_helper.dart index a2f82e0..c186241 100644 --- a/lib/utils/string_helper.dart +++ b/lib/utils/string_helper.dart @@ -21,6 +21,7 @@ /// library; +import 'package:swagger_generator_flutter/core/config.dart'; import 'package:swagger_generator_flutter/core/models.dart'; import 'package:swagger_generator_flutter/utils/string_utils/formatting_utils.dart'; import 'package:swagger_generator_flutter/utils/string_utils/naming_converter.dart'; @@ -47,8 +48,16 @@ class StringHelper { NamingConverter.toDartPropertyName(propName); /// 生成 Dart 类名 - static String generateClassName(String name) => - NamingConverter.generateClassName(name); + static String generateClassName(String name) { + var className = NamingConverter.generateClassName(name); + final prefix = SwaggerConfig.modelClassPrefix; + if (prefix != null && prefix.isNotEmpty) { + if (!className.startsWith(prefix)) { + className = prefix + className; + } + } + return className; + } /// 生成常量名称 (UPPER_SNAKE_CASE) static String generateConstantName(String name) => diff --git a/pubspec.yaml b/pubspec.yaml index 5ad9605..d7dcdde 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: swagger_generator_flutter description: A powerful Swagger/OpenAPI code generator for Flutter projects with Dio + Retrofit support -version: 3.1.4 +version: 3.2.0 environment: sdk: '>=3.0.0 <4.0.0' diff --git a/test/config_prefix_test.dart b/test/config_prefix_test.dart new file mode 100644 index 0000000..21f31dd --- /dev/null +++ b/test/config_prefix_test.dart @@ -0,0 +1,77 @@ +import 'dart:io'; + +import 'package:swagger_generator_flutter/core/config.dart'; +import 'package:swagger_generator_flutter/core/config_repository.dart'; +import 'package:swagger_generator_flutter/utils/string_helper.dart'; +import 'package:test/test.dart'; + +void main() { + group('Model Class Prefix Support', () { + late Directory tempDir; + late File configFile; + + setUp(() { + tempDir = Directory.systemTemp.createTempSync('swagger_gen_test_'); + configFile = File('${tempDir.path}/generator_config.yaml'); + }); + + tearDown(() { + tempDir.deleteSync(recursive: true); + }); + + test('should parse class_prefix from config', () { + configFile.writeAsStringSync(''' +generator: + name: test + +output: + models: + class_prefix: "MyPrefix" + +generation: + models: + class_prefix: "MyPrefix" +'''); + + final config = ConfigRepository.loadSync(configFile.path); + expect(config.modelClassPrefix, equals('MyPrefix')); + }); + + test('should return null when class_prefix is missing', () { + configFile.writeAsStringSync(''' +generator: + name: test +'''); + + final config = ConfigRepository.loadSync(configFile.path); + expect(config.modelClassPrefix, isNull); + }); + + // NOTE: Testing StringHelper.generateClassName directly implies checking if it reads from the GLOBAL config. + // However, ConfigRepository.loadSync() creates an instance, but SwaggerConfig accessors call ConfigRepository.loadSync() individually. + // Since ConfigRepository.loadSync() without args looks for default file, we need a way to inject the config or point it to our file. + // The current implementation of SwaggerConfig calls ConfigRepository.loadSync() which defaults to finding a config file. + + // Changing the implementation of StringHelper to depend on a reloadable config or global state would be better, + // but without changing that, we rely on how ConfigRepository finds the file. + // Ideally we should test ConfigRepository logic separately from StringHelper if StringHelper uses a static/global config lookup. + + // BUT, wait. ConfigRepository.loadSync() does THIS: + // final file = File(configPath ?? PathResolver.findConfigFile() ?? ''); + + // If we want SwaggerConfig (static) to pick up our test config, we might need to trick PathResolver + // OR we can explicitly pass the config path if the code supported it, but StringHelper uses static SwaggerConfig.modelClassPrefix. + + // The current implementation of `SwaggerConfig.modelClassPrefix` is: + // static String? get modelClassPrefix => ConfigRepository.loadSync().modelClassPrefix; + + // So every time we call `StringHelper.generateClassName`, it calls `ConfigRepository.loadSync()`. + // `ConfigRepository.loadSync()` calls `PathResolver.findConfigFile()`. + + // Since we cannot easily mock `PathResolver`'s static method or filesystem search path in a unit test without dependency injection, + // we might face issues testing `StringHelper` integration end-to-end here unless we run this test in a context where `findConfigFile` returns our temp file. + + // However, for verify purposes, verifying `ConfigRepository` parses it is the most critical part we added. + // The `StringHelper` logic is simple string concatenation. + }); +} diff --git a/test_verification.dart b/test_verification.dart new file mode 100644 index 0000000..2bf2cb0 --- /dev/null +++ b/test_verification.dart @@ -0,0 +1,58 @@ +import 'dart:io'; + +import 'package:swagger_generator_flutter/core/config.dart'; +import 'package:swagger_generator_flutter/core/config_repository.dart'; +import 'package:swagger_generator_flutter/utils/string_helper.dart'; + +void main() { + final configFile = File('generator_config.yaml'); + final resultFile = File('test_result.txt'); + + try { + // 1. Setup config file + configFile.writeAsStringSync(''' +generator: + name: test +output: + models: + class_prefix: "MyPrefix" +generation: + models: + class_prefix: "MyPrefix" +'''); + + // 2. Test direct ConfigRepository load + final config = ConfigRepository.loadSync('generator_config.yaml'); + if (config.modelClassPrefix != 'MyPrefix') { + throw 'ConfigRepository failed to PARSE prefix. Got: ${config.modelClassPrefix}'; + } + + // 3. Test SwaggerConfig (static access) + // Note: ConfigRepository.loadSync() tries to find config file. + // Since we created generator_config.yaml in CWD, and PathResolver likely checks CWD, this matches. + if (SwaggerConfig.modelClassPrefix != 'MyPrefix') { + throw 'SwaggerConfig failed to READ prefix. Got: ${SwaggerConfig.modelClassPrefix}'; + } + + // 4. Test StringHelper + final className = StringHelper.generateClassName('User'); + if (className != 'MyPrefixUser') { + throw 'StringHelper failed to APPLY prefix. Got: $className'; + } + + // 5. Test Prefix Avoidance (Idempotency) + final className2 = StringHelper.generateClassName('MyPrefixUser'); + if (className2 != 'MyPrefixUser') { + throw 'StringHelper double-prefixed. Got: $className2'; + } + + resultFile.writeAsStringSync('PASS'); + print('Verification Passed'); + } catch (e, stack) { + resultFile.writeAsStringSync('FAIL: $e\n$stack'); + print('Verification Failed: $e'); + } finally { + // Cleanup + if (configFile.existsSync()) configFile.deleteSync(); + } +}