diff --git a/swagger_parser/CHANGELOG.md b/swagger_parser/CHANGELOG.md index 5fb9be8f0..6fbc68372 100644 --- a/swagger_parser/CHANGELOG.md +++ b/swagger_parser/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.44.0 + +- Add `preserve_schema_casing` option to preserve original casing of schema-derived identifiers, defaults to `false` for backwards compatibility, which normalises to PascalCase + ## 1.43.1 - Fix escaping `$unknown` in generated enum `toJson` error messages - Fix generated code when using `json_serializable` diff --git a/swagger_parser/lib/src/config/swp_config.dart b/swagger_parser/lib/src/config/swp_config.dart index ce7cad9c4..dfa9ca1fb 100644 --- a/swagger_parser/lib/src/config/swp_config.dart +++ b/swagger_parser/lib/src/config/swp_config.dart @@ -53,6 +53,7 @@ class SWPConfig { this.useFlutterCompute = false, this.generateUrlsConstants = false, this.fieldParsers = const [], + this.preserveSchemaCasing = false, }); /// Internal constructor of [SWPConfig] @@ -98,6 +99,7 @@ class SWPConfig { required this.useFlutterCompute, required this.generateUrlsConstants, required this.fieldParsers, + required this.preserveSchemaCasing, this.fallbackUnion, }); @@ -357,6 +359,9 @@ class SWPConfig { final generateUrlsConstants = yamlMap['generate_urls_constants'] as bool? ?? rootConfig?.generateUrlsConstants; + final preserveSchemaCasing = yamlMap['preserve_schema_casing'] as bool? ?? + rootConfig?.preserveSchemaCasing; + final rawFieldParsers = yamlMap['field_parsers'] as YamlList?; List? fieldParsers; if (rawFieldParsers != null) { @@ -432,6 +437,7 @@ class SWPConfig { useFlutterCompute: useFlutterCompute ?? dc.useFlutterCompute, includePaths: includePathsList ?? dc.includePaths, generateUrlsConstants: generateUrlsConstants ?? dc.generateUrlsConstants, + preserveSchemaCasing: preserveSchemaCasing ?? dc.preserveSchemaCasing, ); } @@ -674,6 +680,31 @@ class SWPConfig { /// {@endtemplate} final List fieldParsers; + /// Optional. When `true`, schema and enum names are projected into the + /// target language by stripping separator characters (spaces, dashes, + /// dots, underscores) while preserving the casing of every other + /// character. Defaults to `false` — the default behaviour, which + /// normalises every name to PascalCase and loses internal acronym and + /// lowercase-prefix casing (e.g. `XMLHttpRequest` becomes + /// `XmlHttpRequest`). + /// + /// Examples (flag `true`): + /// - `kUserStatus` → `kUserStatus` + /// - `XMLHttpRequest` → `XMLHttpRequest` + /// - `iOSDevice` → `iOSDevice` + /// - `URL` → `URL` + /// - `HTTPSConnection` → `HTTPSConnection` + /// - `UserStatus` → `UserStatus` + /// - `user_status` → `userstatus` + /// - `My-Class` → `MyClass` + /// + /// Useful when the spec author has deliberate casing intent the + /// generated code should honour (e.g. `XMLHttpRequest`, a `k`-prefixed + /// constant-style enum). `replacement_rules` cannot recover this because + /// the normalisation runs first; this flag opts out of the normalisation + /// instead. + final bool preserveSchemaCasing; + /// Convert [SWPConfig] to [GeneratorConfig] GeneratorConfig toGeneratorConfig() { return GeneratorConfig( @@ -731,6 +762,7 @@ class SWPConfig { includePaths: includePaths, fallbackClient: fallbackClient, inferRequiredFromNullable: inferRequiredFromNullable, + preserveSchemaCasing: preserveSchemaCasing, ); } } diff --git a/swagger_parser/lib/src/generator/templates/dart_dart_mappable_dto_template.dart b/swagger_parser/lib/src/generator/templates/dart_dart_mappable_dto_template.dart index 11b5d036b..827373d28 100644 --- a/swagger_parser/lib/src/generator/templates/dart_dart_mappable_dto_template.dart +++ b/swagger_parser/lib/src/generator/templates/dart_dart_mappable_dto_template.dart @@ -19,7 +19,7 @@ String dartDartMappableDtoTemplate( // Use fallback union only if explicitly provided // Auto-fallback is disabled to avoid breaking existing tests final effectiveFallbackUnion = fallbackUnion; - final originalClassName = dataClass.name.toPascal; + final originalClassName = dataClass.name; final discriminator = dataClass.discriminator; final isUndiscriminatedUnion = dataClass.undiscriminatedUnionVariants?.isNotEmpty ?? false; diff --git a/swagger_parser/lib/src/generator/templates/dart_enum_dto_template.dart b/swagger_parser/lib/src/generator/templates/dart_enum_dto_template.dart index 96c4f097d..d00df31d5 100644 --- a/swagger_parser/lib/src/generator/templates/dart_enum_dto_template.dart +++ b/swagger_parser/lib/src/generator/templates/dart_enum_dto_template.dart @@ -22,7 +22,7 @@ String dartEnumDtoTemplate( useFlutterCompute: useFlutterCompute, ); } else { - final className = enumClass.name.toPascal; + final className = enumClass.name; final jsonParam = unknownEnumValue || enumsToJson; final asyncImport = useFlutterCompute ? "import 'dart:async';\n\n" : ''; @@ -63,7 +63,7 @@ String _dartEnumDartMappableTemplate( required bool unknownEnumValue, required bool useFlutterCompute, }) { - final className = enumClass.name.toPascal; + final className = enumClass.name; final jsonParam = unknownEnumValue || enumsToJson; final asyncImport = useFlutterCompute ? "import 'dart:async';\n\n" : ''; diff --git a/swagger_parser/lib/src/generator/templates/dart_freezed_dto_template.dart b/swagger_parser/lib/src/generator/templates/dart_freezed_dto_template.dart index d682a63c4..533ff76cb 100644 --- a/swagger_parser/lib/src/generator/templates/dart_freezed_dto_template.dart +++ b/swagger_parser/lib/src/generator/templates/dart_freezed_dto_template.dart @@ -15,7 +15,7 @@ String dartFreezedDtoTemplate( bool useFlutterCompute = false, String? fallbackUnion, }) { - final className = dataClass.name.toPascal; + final className = dataClass.name; final discriminator = dataClass.discriminator; final isUndiscriminatedUnion = dataClass.undiscriminatedUnionVariants?.isNotEmpty ?? false; diff --git a/swagger_parser/lib/src/generator/templates/dart_json_serializable_dto_template.dart b/swagger_parser/lib/src/generator/templates/dart_json_serializable_dto_template.dart index d6508de0a..d4490c71f 100644 --- a/swagger_parser/lib/src/generator/templates/dart_json_serializable_dto_template.dart +++ b/swagger_parser/lib/src/generator/templates/dart_json_serializable_dto_template.dart @@ -16,7 +16,7 @@ String dartJsonSerializableDtoTemplate( bool useFlutterCompute = false, String? fallbackUnion, }) { - final originalClassName = dataClass.name.toPascal; + final originalClassName = dataClass.name; // Check if this is a union type final isUnion = dataClass.discriminator != null || diff --git a/swagger_parser/lib/src/generator/templates/dart_typedef_template.dart b/swagger_parser/lib/src/generator/templates/dart_typedef_template.dart index 3327a2943..0458d60f9 100644 --- a/swagger_parser/lib/src/generator/templates/dart_typedef_template.dart +++ b/swagger_parser/lib/src/generator/templates/dart_typedef_template.dart @@ -8,7 +8,7 @@ import 'package:swagger_parser/src/utils/type_utils.dart'; /// Provides template for generating dart typedefs using JSON serializable String dartTypeDefTemplate(UniversalComponentClass dataClass, {required bool useMultipartFile}) { - final className = dataClass.name.toPascal; + final className = dataClass.name; final type = dataClass.parameters.firstOrNull; final import = dataClass.imports.firstOrNull; if (type == null) { diff --git a/swagger_parser/lib/src/generator/templates/kotlin_enum_dto_template.dart b/swagger_parser/lib/src/generator/templates/kotlin_enum_dto_template.dart index 62431e44f..115445435 100644 --- a/swagger_parser/lib/src/generator/templates/kotlin_enum_dto_template.dart +++ b/swagger_parser/lib/src/generator/templates/kotlin_enum_dto_template.dart @@ -10,7 +10,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -enum class ${dataClass.name.toPascal} {${_parameters(dataClass)} +enum class ${dataClass.name} {${_parameters(dataClass)} } '''; } diff --git a/swagger_parser/lib/src/generator/templates/kotlin_moshi_dto_template.dart b/swagger_parser/lib/src/generator/templates/kotlin_moshi_dto_template.dart index f29dd0a9c..e73e5c67b 100644 --- a/swagger_parser/lib/src/generator/templates/kotlin_moshi_dto_template.dart +++ b/swagger_parser/lib/src/generator/templates/kotlin_moshi_dto_template.dart @@ -1,5 +1,4 @@ import 'package:swagger_parser/src/generator/model/programming_language.dart'; -import 'package:swagger_parser/src/parser/model/normalized_identifier.dart'; import 'package:swagger_parser/src/parser/swagger_parser_core.dart'; import 'package:swagger_parser/src/utils/base_utils.dart'; import 'package:swagger_parser/src/utils/type_utils.dart'; @@ -11,7 +10,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass ${descriptionComment(dataClass.description)}@JsonClass(generateAdapter = true) -data class ${dataClass.name.toPascal}(${_parameters(dataClass.parameters)}${dataClass.parameters.isNotEmpty ? '\n)' : ')'} +data class ${dataClass.name}(${_parameters(dataClass.parameters)}${dataClass.parameters.isNotEmpty ? '\n)' : ')'} '''; } diff --git a/swagger_parser/lib/src/generator/templates/kotlin_typedef_template.dart b/swagger_parser/lib/src/generator/templates/kotlin_typedef_template.dart index 4f08827cf..944fdc4e9 100644 --- a/swagger_parser/lib/src/generator/templates/kotlin_typedef_template.dart +++ b/swagger_parser/lib/src/generator/templates/kotlin_typedef_template.dart @@ -1,13 +1,12 @@ import 'package:collection/collection.dart'; import 'package:swagger_parser/src/generator/model/programming_language.dart'; -import 'package:swagger_parser/src/parser/model/normalized_identifier.dart'; import 'package:swagger_parser/src/parser/swagger_parser_core.dart'; import 'package:swagger_parser/src/utils/base_utils.dart'; import 'package:swagger_parser/src/utils/type_utils.dart'; /// Provides template for generating dart typedefs using JSON serializable String kotlinTypeDefTemplate(UniversalComponentClass dataClass) { - final className = dataClass.name.toPascal; + final className = dataClass.name; final type = dataClass.parameters.firstOrNull; if (type == null) { return ''; diff --git a/swagger_parser/lib/src/parser/config/parser_config.dart b/swagger_parser/lib/src/parser/config/parser_config.dart index fff50188c..3760678f2 100644 --- a/swagger_parser/lib/src/parser/config/parser_config.dart +++ b/swagger_parser/lib/src/parser/config/parser_config.dart @@ -20,6 +20,7 @@ class ParserConfig { this.includePaths, this.fallbackClient = 'fallback', this.inferRequiredFromNullable = false, + this.preserveSchemaCasing = false, }); /// Specification file content as [String] @@ -88,4 +89,17 @@ class ParserConfig { /// Properties without nullable: true in schema are marked as required. /// Only applies when schema has no explicit required array. final bool inferRequiredFromNullable; + + /// When `true`, schema and enum names are projected by stripping + /// separator characters while preserving the casing of every other + /// character. + /// + /// Examples (flag `true`): + /// - `kUserStatus` → `kUserStatus` + /// - `XMLHttpRequest` → `XMLHttpRequest` + /// - `iOSDevice` → `iOSDevice` + /// - `URL` → `URL` + /// - `user_status` → `userstatus` + /// - `My-Class` → `MyClass` + final bool preserveSchemaCasing; } diff --git a/swagger_parser/lib/src/parser/corrector/open_api_corrector.dart b/swagger_parser/lib/src/parser/corrector/open_api_corrector.dart index 26d727968..82c807e08 100644 --- a/swagger_parser/lib/src/parser/corrector/open_api_corrector.dart +++ b/swagger_parser/lib/src/parser/corrector/open_api_corrector.dart @@ -38,7 +38,9 @@ class OpenApiCorrector { for (final rule in config.replacementRules) { correctType = rule.apply(correctType)!; } - correctType = correctType.toPascal; + correctType = config.preserveSchemaCasing + ? correctType.toPreservedCase + : correctType.toPascal; if (correctType != type) { typeCorrections[type] = correctType; } diff --git a/swagger_parser/lib/src/parser/model/normalized_identifier.dart b/swagger_parser/lib/src/parser/model/normalized_identifier.dart index e54a2f2bd..5cf512e96 100644 --- a/swagger_parser/lib/src/parser/model/normalized_identifier.dart +++ b/swagger_parser/lib/src/parser/model/normalized_identifier.dart @@ -240,6 +240,33 @@ extension StringToCaseX on String { : identifier.pascalCase; } + /// Strip separator characters (spaces, dashes, dots, underscores, etc.) + /// while preserving the casing of every other character. + /// + /// - `XMLHttpRequest` → `XMLHttpRequest` + /// - `kUserStatus` → `kUserStatus` + /// - `iOSDevice` → `iOSDevice` + /// - `URL` → `URL` + /// - `user_status` → `userstatus` + /// - `My-Class` → `MyClass` + /// - `XML Http Request` → `XMLHttpRequest` + /// + /// Mirrors [toPascal]'s `Private` prefix handling for inputs that start + /// with an underscore. + /// + /// Used by the `preserve_schema_casing` config option to project + /// spec-author identifiers into the target language verbatim where + /// possible. + String get toPreservedCase { + if (isEmpty) { + return ''; + } + final isPrivate = startsWith('_'); + final body = isPrivate ? substring(1) : this; + final stripped = body.replaceAll(_separatorPattern, ''); + return isPrivate ? 'Private$stripped' : stripped; + } + /// Return text formatted to snake_case /// /// The result is prefixed with `private_` if the given text indicates a private entity diff --git a/swagger_parser/lib/src/parser/parser/open_api_parser.dart b/swagger_parser/lib/src/parser/parser/open_api_parser.dart index 2a81bb9e7..7e676a6b4 100644 --- a/swagger_parser/lib/src/parser/parser/open_api_parser.dart +++ b/swagger_parser/lib/src/parser/parser/open_api_parser.dart @@ -32,6 +32,12 @@ class OpenApiParser { /// [ParserConfig] that [OpenApiParser] use final ParserConfig config; + /// Normalises a schema-derived identifier — preserves the spec author's + /// casing (stripping separators only) when [ParserConfig.preserveSchemaCasing] + /// is on, otherwise falls back to PascalCase. + String _schemaIdentifier(String input) => + config.preserveSchemaCasing ? input.toPreservedCase : input.toPascal; + /// `info` section in specification late final OpenApiInfo _apiInfo; @@ -129,7 +135,7 @@ class OpenApiParser { return UniversalEnumClass( originalName: name, - name: uniqueName.toPascal, + name: _schemaIdentifier(uniqueName), type: type, items: items, defaultValue: defaultValue, @@ -1533,7 +1539,7 @@ class OpenApiParser { final (parameters, imports) = _findParametersAndImports(map); - var type = newName.toPascal; + var type = _schemaIdentifier(newName); for (final replacementRule in config.replacementRules) { type = replacementRule.apply(type)!; @@ -1991,15 +1997,15 @@ class OpenApiParser { String? import; String type; if (map.containsKey(_refConst)) { - import = _formatRef(map).toPascal; + import = _schemaIdentifier(_formatRef(map)); } else if (map.containsKey(_additionalPropertiesConst) && map[_additionalPropertiesConst] is Map && (map[_additionalPropertiesConst] as Map).containsKey( _refConst, )) { - import = _formatRef( + import = _schemaIdentifier(_formatRef( map[_additionalPropertiesConst] as Map, - ).toPascal; + )); } if (map.containsKey(_typeConst)) { @@ -2010,7 +2016,7 @@ class OpenApiParser { type = import ?? _objectConst; } if (import != null) { - type = type.toPascal; + type = _schemaIdentifier(type); } final defaultValue = map[_defaultConst]?.toString(); @@ -2178,7 +2184,7 @@ class OpenApiParser { for (final item in otherItems) { if (item.containsKey(_refConst)) { - final refName = _formatRef(item).toPascal; + final refName = _schemaIdentifier(_formatRef(item)); // Locate the referenced component to get its properties if available. if (_definitionFileContent diff --git a/swagger_parser/pubspec.yaml b/swagger_parser/pubspec.yaml index 378a4ad44..7861ba0df 100644 --- a/swagger_parser/pubspec.yaml +++ b/swagger_parser/pubspec.yaml @@ -1,6 +1,6 @@ name: swagger_parser description: Package that generates REST clients and data classes from OpenApi definition file -version: 1.43.1 +version: 1.44.0 repository: https://github.com/Carapacik/swagger_parser topics: - swagger diff --git a/swagger_parser/test/config/swp_config_test.dart b/swagger_parser/test/config/swp_config_test.dart index b3875841e..d9f51f8c0 100644 --- a/swagger_parser/test/config/swp_config_test.dart +++ b/swagger_parser/test/config/swp_config_test.dart @@ -41,6 +41,7 @@ void main() { expect(config.includeTags, isEmpty); expect(config.fallbackClient, 'fallback'); expect(config.includeIfNull, isFalse); + expect(config.preserveSchemaCasing, isFalse); }); test('should create config with all parameters specified', () { @@ -82,6 +83,7 @@ void main() { includeTags: ['public', 'stable'], fallbackClient: 'common', includeIfNull: true, + preserveSchemaCasing: true, ); expect(config.outputDirectory, equals('lib/generated')); @@ -118,6 +120,7 @@ void main() { expect(config.includeTags, equals(['public', 'stable'])); expect(config.fallbackClient, equals('common')); expect(config.includeIfNull, isTrue); + expect(config.preserveSchemaCasing, isTrue); }); }); @@ -169,6 +172,7 @@ void main() { 'exclude_tags': ['internal', 'deprecated'], 'include_tags': ['public', 'stable'], 'include_if_null': true, + 'preserve_schema_casing': true, 'replacement_rules': [ {'pattern': 'Test', 'replacement': 'Mock'}, {'pattern': 'Dto', 'replacement': 'Model'}, @@ -209,6 +213,7 @@ void main() { expect(config.excludeTags, equals(['internal', 'deprecated'])); expect(config.includeTags, equals(['public', 'stable'])); expect(config.includeIfNull, isTrue); + expect(config.preserveSchemaCasing, isTrue); expect(config.replacementRules, hasLength(2)); expect(config.replacementRules[0].pattern.pattern, equals('Test')); expect(config.replacementRules[0].replacement, equals('Mock')); @@ -767,6 +772,79 @@ void main() { }); }); + group('Preserve Schema Casing Configuration', () { + test('should default preserveSchemaCasing to false', () { + const config = SWPConfig(outputDirectory: 'lib/api'); + expect(config.preserveSchemaCasing, isFalse); + }); + + test('should parse preserveSchemaCasing from YAML when set to true', () { + final yamlMap = YamlMap.wrap({ + 'schema_path': 'api/openapi.yaml', + 'output_directory': 'lib/api', + 'preserve_schema_casing': true, + }); + + final config = SWPConfig.fromYaml(yamlMap); + expect(config.preserveSchemaCasing, isTrue); + }); + + test('should parse preserveSchemaCasing from YAML when set to false', () { + final yamlMap = YamlMap.wrap({ + 'schema_path': 'api/openapi.yaml', + 'output_directory': 'lib/api', + 'preserve_schema_casing': false, + }); + + final config = SWPConfig.fromYaml(yamlMap); + expect(config.preserveSchemaCasing, isFalse); + }); + + test('should inherit preserveSchemaCasing from root config', () { + const rootConfig = SWPConfig( + outputDirectory: 'lib/shared', + preserveSchemaCasing: true, + ); + + final yamlMap = YamlMap.wrap({ + 'schema_path': 'api/user.yaml', + 'name': 'user_api', + }); + + final config = SWPConfig.fromYaml(yamlMap, rootConfig: rootConfig); + expect(config.preserveSchemaCasing, isTrue); + }); + + test('should override root config preserveSchemaCasing with local value', + () { + const rootConfig = SWPConfig( + outputDirectory: 'lib/shared', + preserveSchemaCasing: true, + ); + + final yamlMap = YamlMap.wrap({ + 'schema_path': 'api/user.yaml', + 'preserve_schema_casing': false, + }); + + final config = SWPConfig.fromYaml(yamlMap, rootConfig: rootConfig); + expect(config.preserveSchemaCasing, isFalse); + }); + + test('should pass preserveSchemaCasing to ParserConfig', () { + const swpConfig = SWPConfig( + outputDirectory: 'lib/api', + preserveSchemaCasing: true, + ); + + final parserConfig = swpConfig.toParserConfig( + fileContent: '{}', + isJson: true, + ); + expect(parserConfig.preserveSchemaCasing, isTrue); + }); + }); + group('Include If Null Configuration', () { test('should default includeIfNull to false', () { const config = SWPConfig(outputDirectory: 'lib/api'); diff --git a/swagger_parser/test/e2e/e2e_test.dart b/swagger_parser/test/e2e/e2e_test.dart index 9b318a1c0..e037a7a43 100644 --- a/swagger_parser/test/e2e/e2e_test.dart +++ b/swagger_parser/test/e2e/e2e_test.dart @@ -19,6 +19,20 @@ void main() { ); }); + test('preserve_schema_casing', () async { + await e2eTest( + 'preserve_schema_casing', + (outputDirectory, schemaPath) => SWPConfig( + outputDirectory: outputDirectory, + schemaPath: schemaPath, + jsonSerializer: JsonSerializer.freezed, + putClientsInFolder: true, + preserveSchemaCasing: true, + ), + schemaFileName: 'openapi.yaml', + ); + }); + test('generate_urls_constants', () async { await e2eTest( 'generate_urls_constants', diff --git a/swagger_parser/test/e2e/tests/preserve_schema_casing/expected_files/clients/fallback_client.dart b/swagger_parser/test/e2e/tests/preserve_schema_casing/expected_files/clients/fallback_client.dart new file mode 100644 index 000000000..9d486f80e --- /dev/null +++ b/swagger_parser/test/e2e/tests/preserve_schema_casing/expected_files/clients/fallback_client.dart @@ -0,0 +1,18 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_import, invalid_annotation_target, unnecessary_import + +import 'package:dio/dio.dart'; +import 'package:retrofit/retrofit.dart'; + +import '../models/user.dart'; + +part 'fallback_client.g.dart'; + +@RestApi() +abstract class FallbackClient { + factory FallbackClient(Dio dio, {String? baseUrl}) = _FallbackClient; + + @GET('/me') + Future getMe(); +} diff --git a/swagger_parser/test/e2e/tests/preserve_schema_casing/expected_files/export.dart b/swagger_parser/test/e2e/tests/preserve_schema_casing/expected_files/export.dart new file mode 100644 index 000000000..bdbfb8c00 --- /dev/null +++ b/swagger_parser/test/e2e/tests/preserve_schema_casing/expected_files/export.dart @@ -0,0 +1,14 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_import, invalid_annotation_target, unnecessary_import + +// Clients +export 'clients/fallback_client.dart'; +// Data classes +export 'models/k_user_status.dart'; +export 'models/url.dart'; +export 'models/xml_http_request.dart'; +export 'models/i_os_device.dart'; +export 'models/user.dart'; +// Root client +export 'rest_client.dart'; diff --git a/swagger_parser/test/e2e/tests/preserve_schema_casing/expected_files/models/i_os_device.dart b/swagger_parser/test/e2e/tests/preserve_schema_casing/expected_files/models/i_os_device.dart new file mode 100644 index 000000000..83e5ae75a --- /dev/null +++ b/swagger_parser/test/e2e/tests/preserve_schema_casing/expected_files/models/i_os_device.dart @@ -0,0 +1,18 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_import, invalid_annotation_target, unnecessary_import + +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'i_os_device.freezed.dart'; +part 'i_os_device.g.dart'; + +@Freezed() +class iOSDevice with _$iOSDevice { + const factory iOSDevice({ + required String deviceId, + }) = _iOSDevice; + + factory iOSDevice.fromJson(Map json) => + _$iOSDeviceFromJson(json); +} diff --git a/swagger_parser/test/e2e/tests/preserve_schema_casing/expected_files/models/k_user_status.dart b/swagger_parser/test/e2e/tests/preserve_schema_casing/expected_files/models/k_user_status.dart new file mode 100644 index 000000000..021681fbe --- /dev/null +++ b/swagger_parser/test/e2e/tests/preserve_schema_casing/expected_files/models/k_user_status.dart @@ -0,0 +1,32 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_import, invalid_annotation_target, unnecessary_import + +import 'package:freezed_annotation/freezed_annotation.dart'; + +@JsonEnum() +enum kUserStatus { + @JsonValue('PENDING') + pending('PENDING'), + @JsonValue('ACTIVE') + active('ACTIVE'), + + /// Default value for all unparsed values, allows backward compatibility when adding new values on the backend. + $unknown(null); + + const kUserStatus(this.json); + + factory kUserStatus.fromJson(String json) => values.firstWhere( + (e) => e.json == json, + orElse: () => $unknown, + ); + + final String? json; + + @override + String toString() => json?.toString() ?? super.toString(); + + /// Returns all defined enum values excluding the $unknown value. + static List get $valuesDefined => + values.where((value) => value != $unknown).toList(); +} diff --git a/swagger_parser/test/e2e/tests/preserve_schema_casing/expected_files/models/url.dart b/swagger_parser/test/e2e/tests/preserve_schema_casing/expected_files/models/url.dart new file mode 100644 index 000000000..a63def25f --- /dev/null +++ b/swagger_parser/test/e2e/tests/preserve_schema_casing/expected_files/models/url.dart @@ -0,0 +1,5 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_import, invalid_annotation_target, unnecessary_import + +typedef URL = String; diff --git a/swagger_parser/test/e2e/tests/preserve_schema_casing/expected_files/models/user.dart b/swagger_parser/test/e2e/tests/preserve_schema_casing/expected_files/models/user.dart new file mode 100644 index 000000000..b384bf07d --- /dev/null +++ b/swagger_parser/test/e2e/tests/preserve_schema_casing/expected_files/models/user.dart @@ -0,0 +1,26 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_import, invalid_annotation_target, unnecessary_import + +import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'url.dart'; +import 'xml_http_request.dart'; +import 'i_os_device.dart'; +import 'k_user_status.dart'; + +part 'user.freezed.dart'; +part 'user.g.dart'; + +@Freezed() +class User with _$User { + const factory User({ + required String userId, + required kUserStatus status, + required iOSDevice device, + required URL homepage, + required XMLHttpRequest lastRequest, + }) = _User; + + factory User.fromJson(Map json) => _$UserFromJson(json); +} diff --git a/swagger_parser/test/e2e/tests/preserve_schema_casing/expected_files/models/xml_http_request.dart b/swagger_parser/test/e2e/tests/preserve_schema_casing/expected_files/models/xml_http_request.dart new file mode 100644 index 000000000..d6a8bfe7b --- /dev/null +++ b/swagger_parser/test/e2e/tests/preserve_schema_casing/expected_files/models/xml_http_request.dart @@ -0,0 +1,20 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_import, invalid_annotation_target, unnecessary_import + +import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'url.dart'; + +part 'xml_http_request.freezed.dart'; +part 'xml_http_request.g.dart'; + +@Freezed() +class XMLHttpRequest with _$XMLHttpRequest { + const factory XMLHttpRequest({ + required URL url, + }) = _XMLHttpRequest; + + factory XMLHttpRequest.fromJson(Map json) => + _$XMLHttpRequestFromJson(json); +} diff --git a/swagger_parser/test/e2e/tests/preserve_schema_casing/expected_files/rest_client.dart b/swagger_parser/test/e2e/tests/preserve_schema_casing/expected_files/rest_client.dart new file mode 100644 index 000000000..8dd977c6f --- /dev/null +++ b/swagger_parser/test/e2e/tests/preserve_schema_casing/expected_files/rest_client.dart @@ -0,0 +1,26 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_import, invalid_annotation_target, unnecessary_import + +import 'package:dio/dio.dart'; + +import 'clients/fallback_client.dart'; + +/// Test API for preserve_schema_casing `v1.0.0` +class RestClient { + RestClient( + Dio dio, { + String? baseUrl, + }) : _dio = dio, + _baseUrl = baseUrl; + + final Dio _dio; + final String? _baseUrl; + + static String get version => '1.0.0'; + + FallbackClient? _fallback; + + FallbackClient get fallback => + _fallback ??= FallbackClient(_dio, baseUrl: _baseUrl); +} diff --git a/swagger_parser/test/e2e/tests/preserve_schema_casing/openapi.yaml b/swagger_parser/test/e2e/tests/preserve_schema_casing/openapi.yaml new file mode 100644 index 000000000..628a07d97 --- /dev/null +++ b/swagger_parser/test/e2e/tests/preserve_schema_casing/openapi.yaml @@ -0,0 +1,63 @@ +openapi: 3.0.0 +info: + title: Test API for preserve_schema_casing + version: 1.0.0 +paths: + /me: + get: + operationId: getMe + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/User" +components: + schemas: + # camelCase prefix should be preserved. + kUserStatus: + type: string + enum: + - PENDING + - ACTIVE + # All-uppercase acronym should be preserved. + URL: + type: string + # Acronym in the middle of a PascalCase name should be preserved + # (currently swagger_parser would emit XmlHttpRequest). + XMLHttpRequest: + type: object + required: + - url + properties: + url: + $ref: "#/components/schemas/URL" + # Lowercase prefix that wraps a PascalCase suffix should round-trip + # (currently swagger_parser would emit IOSDevice). + iOSDevice: + type: object + required: + - deviceId + properties: + deviceId: + type: string + User: + type: object + required: + - userId + - status + - device + - homepage + - lastRequest + properties: + userId: + type: string + status: + $ref: "#/components/schemas/kUserStatus" + device: + $ref: "#/components/schemas/iOSDevice" + homepage: + $ref: "#/components/schemas/URL" + lastRequest: + $ref: "#/components/schemas/XMLHttpRequest" diff --git a/swagger_parser/test/src/parser/model/normalized_identifier_test.dart b/swagger_parser/test/src/parser/model/normalized_identifier_test.dart index 833d3748a..31f6294a4 100644 --- a/swagger_parser/test/src/parser/model/normalized_identifier_test.dart +++ b/swagger_parser/test/src/parser/model/normalized_identifier_test.dart @@ -1060,5 +1060,54 @@ void main() { expect(identifier.words, edgeCase.expectedWords); }); } + + group('String.toPreservedCase', () { + test('preserves no-separator identifiers verbatim', () { + expect('UserStatus'.toPreservedCase, 'UserStatus'); + expect('User'.toPreservedCase, 'User'); + expect('kUserStatus'.toPreservedCase, 'kUserStatus'); + expect('XMLHttpRequest'.toPreservedCase, 'XMLHttpRequest'); + expect('iOSDevice'.toPreservedCase, 'iOSDevice'); + expect('URL'.toPreservedCase, 'URL'); + expect('HTTPSConnection'.toPreservedCase, 'HTTPSConnection'); + }); + + test('strips separators while keeping the casing of letters/digits', () { + expect('user_status'.toPreservedCase, 'userstatus'); + expect('My-Class'.toPreservedCase, 'MyClass'); + expect('XML Http Request'.toPreservedCase, 'XMLHttpRequest'); + expect('com.example.Api'.toPreservedCase, 'comexampleApi'); + }); + + test('mirrors toPascal Private-prefix handling', () { + expect('_kUserStatus'.toPreservedCase, 'PrivatekUserStatus'); + expect('_XMLHttpRequest'.toPreservedCase, 'PrivateXMLHttpRequest'); + expect('_UserStatus'.toPreservedCase, 'PrivateUserStatus'); + }); + + test('strips inner underscores even after a private prefix', () { + // The leading underscore is consumed as the private marker; any + // remaining underscores are stripped along with other separators. + expect('_user_status'.toPreservedCase, 'Privateuserstatus'); + }); + + test('is idempotent on its own output', () { + for (final input in [ + 'XMLHttpRequest', + 'kUserStatus', + 'UserStatus', + 'iOSDevice', + 'URL', + ]) { + final once = input.toPreservedCase; + expect(once.toPreservedCase, once, + reason: 'not idempotent for $input'); + } + }); + + test('returns empty for empty input', () { + expect(''.toPreservedCase, ''); + }); + }); }); }