diff --git a/README.md b/README.md index 0f15fe79..1ac78f0a 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ targets: | `use_required_attribute_for_headers` | `true` | `false` | If this option is false, generator will not add @required attribute to headers. | | `with_converter` | `true` | `false` | If option is true, combination of all mappings will be generated. | | `ignore_headers` | `false` | `false` | If option is true, headers will not be generated. | -| `additional_headers` | `false` | `false` | List of additional headers, not specified in Swagger. Example of usage: [build.yaml](https://github.com/epam-cross-platform-lab/swagger-dart-code-generator/blob/master/example/build.yaml) +| `additional_headers` | `false` | `false` | List of additional headers, not specified in Swagger. Example of usage: [build.yaml](https://github.com/epam-cross-platform-lab/swagger-dart-code-generator/blob/master/example/build.yaml) | | `enums_case_sensitive` | `true` | `false` | If value is false, 'enumValue' will be defined like Enum.enumValue even it's json key equals 'ENUMVALUE' | | `include_paths` | `[]` | `false` | List of Regex If not empty - includes only paths matching reges | | `exclude_paths` | `[]` | `false` | List of Regex If not empty -exclude paths matching reges | @@ -96,6 +96,7 @@ targets: | `override_to_string` | `bool` | `true` | Overrides `toString()` method using `jsonEncode(this)` | | `generate_first_succeed_response` | `true` | `false` | If request has multiple success responses, first one will be generated. Otherwice - `dynamic` | | `multipart_file_type` | `List` | `false` | Type if input parameter of Multipart request | +| `scalars` | `-` | `{}` | A map of custom types that are used for string properties with a given [format](https://swagger.io/docs/specification/data-models/data-types/#format). See example [here](#overriden-formats-implementation) | @@ -154,6 +155,23 @@ targets: - "Result" ``` +### **Scalars Implementation** + +```yaml + swagger_dart_code_generator: + options: + input_folder: "input_folder/" + output_folder: "lib/swagger_generated_code/" + import_paths: + - "package:uuid/uuid.dart" + scalars: + uuid: + type: Uuid + deserialize: Uuid.parse + # optional - default is toString() + serialize: myCustomUuidSerializeFunction +``` + ### **Response Override Value Map for requests generation** If you want to override response for concrete request, you can use response_override_value_map. For example: diff --git a/lib/src/code_generators/swagger_additions_generator.dart b/lib/src/code_generators/swagger_additions_generator.dart index 0a2e9aeb..230ac033 100644 --- a/lib/src/code_generators/swagger_additions_generator.dart +++ b/lib/src/code_generators/swagger_additions_generator.dart @@ -76,6 +76,7 @@ import 'package:chopper/chopper.dart' as chopper;'''; // ignore_for_file: type=lint import 'package:json_annotation/json_annotation.dart'; +import 'package:json_annotation/json_annotation.dart' as json; import 'package:collection/collection.dart'; ${options.overrideToString ? "import 'dart:convert';" : ''} """); diff --git a/lib/src/code_generators/swagger_models_generator.dart b/lib/src/code_generators/swagger_models_generator.dart index 246d50f6..d77ce7fd 100644 --- a/lib/src/code_generators/swagger_models_generator.dart +++ b/lib/src/code_generators/swagger_models_generator.dart @@ -273,6 +273,7 @@ abstract class SwaggerModelsGenerator extends SwaggerGeneratorBase { required List allEnums, required bool generateEnumsMethods, }) { + final converters = generateJsonConverters(); final allEnumsString = generateEnumsMethods ? allEnums .map((e) => e.generateFromJsonToJson(options.enumsCaseSensitive)) @@ -319,7 +320,7 @@ abstract class SwaggerModelsGenerator extends SwaggerGeneratorBase { results = results.replaceAll(' $listEnum ', ' List<$listEnum> '); } - return results + allEnumsString; + return converters + results + allEnumsString; } static String getValidatedParameterName(String parameterName) { @@ -396,7 +397,10 @@ abstract class SwaggerModelsGenerator extends SwaggerGeneratorBase { case 'boolean': return 'bool'; case 'string': - if (parameter.format == 'date-time' || parameter.format == 'date') { + final scalar = options.scalars[parameter.format]; + if (scalar != null) { + return scalar.type; + } else if (parameter.format == 'date-time' || parameter.format == 'date') { return 'DateTime'; } else if (parameter.isEnum) { return 'enums.${getValidatedClassName(generateEnumName(getValidatedClassName(className), parameterName))}'; @@ -437,6 +441,41 @@ abstract class SwaggerModelsGenerator extends SwaggerGeneratorBase { return ', includeIfNull: ${options.includeIfNull}'; } + String generatePropertyJsonConverterAnnotation(SwaggerSchema schema) { + final override = schema.type == 'string' ? options.scalars[schema.format] : null; + if (override == null) { + return ''; + } + + return '@_\$${schema.format.pascalCase}JsonConverter()'; + } + + String generateJsonConverters() { + if (options.scalars.isEmpty) { + return ''; + } + + var result = ''; + + for (final MapEntry(:key, :value) in options.scalars.entries) { + final className = '_\$${key.pascalCase}JsonConverter'; + + result += ''' +class $className implements json.JsonConverter<${value.type}, String> { + const $className(); + + @override + fromJson(json) => ${value.deserialize}(json); + + @override + toJson(json) => ${value.serialize.isEmpty ? 'json.toString()' : '${value.serialize}(json)'}; +} +'''; + } + + return result; + } + String generatePropertyContentByDefault({ required SwaggerSchema prop, required String propertyName, @@ -978,6 +1017,7 @@ static $returnType $fromJsonFunction($valueType? value) => $enumNameCamelCase$fr required Map allClasses, required bool isDeprecated, }) { + final jsonConverterAnnotation = prop.items == null ? '' : generatePropertyJsonConverterAnnotation(prop.items!); final typeName = _generateListPropertyTypeName( allEnumListNames: allEnumListNames, allEnumNames: allEnumNames, @@ -1026,8 +1066,7 @@ static $returnType $fromJsonFunction($valueType? value) => $enumNameCamelCase$fr !requiredProperties.contains(propertyKey)) { listPropertyName = listPropertyName.makeNullable(); } - - return '$jsonKeyContent$deprecatedContent $listPropertyName ${generateFieldName(propertyName)};${unknownEnumValue.fromJson}'; + return '$jsonConverterAnnotation$jsonKeyContent$deprecatedContent $listPropertyName ${generateFieldName(propertyName)};${unknownEnumValue.fromJson}'; } String generateGeneralPropertyContent({ @@ -1042,6 +1081,7 @@ static $returnType $fromJsonFunction($valueType? value) => $enumNameCamelCase$fr required bool isDeprecated, }) { final includeIfNullString = generateIncludeIfNullString(); + final jsonConverterAnnotation = generatePropertyJsonConverterAnnotation(prop); var jsonKeyContent = "@JsonKey(name: '${_validatePropertyKey(propertyKey)}'$includeIfNullString"; @@ -1102,8 +1142,7 @@ static $returnType $fromJsonFunction($valueType? value) => $enumNameCamelCase$fr !requiredProperties.contains(propertyKey)) { typeName = typeName.makeNullable(); } - - return '\t$jsonKeyContent$isDeprecatedContent $typeName $propertyName;${unknownEnumValue.fromJson}'; + return '\t$jsonConverterAnnotation$jsonKeyContent$isDeprecatedContent $typeName $propertyName;${unknownEnumValue.fromJson}'; } String generatePropertyContentByType( @@ -1293,7 +1332,7 @@ static $returnType $fromJsonFunction($valueType? value) => $enumNameCamelCase$fr allClasses.forEach((key, value) { if (kBasicTypes.contains(value.type.toLowerCase()) && !value.isEnum) { - result.addAll({key: _mapBasicTypeToDartType(value.type, value.format)}); + result.addAll({key: _mapBasicTypeToDartType(value.type, value.format, options)}); } if (value.type == kArray && value.items != null) { @@ -1306,7 +1345,7 @@ static $returnType $fromJsonFunction($valueType? value) => $enumNameCamelCase$fr final schema = allClasses[typeName]; if (kBasicTypes.contains(schema?.type)) { - typeName = _mapBasicTypeToDartType(schema!.type, value.format); + typeName = _mapBasicTypeToDartType(schema!.type, value.format, options); } else { typeName = getValidatedClassName(typeName); } @@ -1319,14 +1358,17 @@ static $returnType $fromJsonFunction($valueType? value) => $enumNameCamelCase$fr return result; } - static String _mapBasicTypeToDartType(String basicType, String format) { - if (basicType.toLowerCase() == kString && - (format == 'date-time' || format == 'datetime')) { - return 'DateTime'; - } + static String _mapBasicTypeToDartType(String basicType, String format, GeneratorOptions options) { switch (basicType.toLowerCase()) { case 'string': - return 'String'; + final scalar = options.scalars[format]; + if (scalar != null) { + return scalar.type; + } else if (format == 'date-time' || format == 'datetime') { + return kDateTimeType; + } else { + return 'String'; + } case 'int': case 'integer': return 'int'; diff --git a/lib/src/models/generator_options.dart b/lib/src/models/generator_options.dart index 0c2d6b78..3092ac40 100644 --- a/lib/src/models/generator_options.dart +++ b/lib/src/models/generator_options.dart @@ -34,6 +34,7 @@ class GeneratorOptions { this.overrideEqualsAndHashcode = true, this.overrideToString = true, this.pageWidth, + this.scalars = const {}, this.overridenModels = const [], this.generateToJsonFor = const [], this.multipartFileType = 'List', @@ -55,6 +56,7 @@ class GeneratorOptions { final String multipartFileType; final String urlencodedFileType; final bool withConverter; + final Map scalars; final List overridenModels; final List generateToJsonFor; final List additionalHeaders; @@ -178,3 +180,24 @@ class OverridenModelsItem { factory OverridenModelsItem.fromJson(Map json) => _$OverridenModelsItemFromJson(json); } + +@JsonSerializable(fieldRename: FieldRename.snake) +class CustomScalar { + @JsonKey() + final String type; + @JsonKey() + final String deserialize; + @JsonKey(defaultValue: '') + final String serialize; + + factory CustomScalar.fromJson(Map json) => + _$CustomScalarFromJson(json); + + CustomScalar({ + required this.type, + required this.deserialize, + this.serialize = '', + }); + + Map toJson() => _$CustomScalarToJson(this); +} \ No newline at end of file diff --git a/lib/src/models/generator_options.g.dart b/lib/src/models/generator_options.g.dart index 3f1be435..3ac43703 100644 --- a/lib/src/models/generator_options.g.dart +++ b/lib/src/models/generator_options.g.dart @@ -75,7 +75,12 @@ GeneratorOptions _$GeneratorOptionsFromJson(Map json) => GeneratorOptions( overrideEqualsAndHashcode: json['override_equals_and_hashcode'] as bool? ?? true, overrideToString: json['override_to_string'] as bool? ?? true, - pageWidth: json['page_width'] as int?, + pageWidth: (json['page_width'] as num?)?.toInt(), + scalars: (json['scalars'] as Map?)?.map( + (k, e) => MapEntry(k as String, + CustomScalar.fromJson(Map.from(e as Map))), + ) ?? + const {}, overridenModels: (json['overriden_models'] as List?) ?.map((e) => OverridenModelsItem.fromJson( Map.from(e as Map))) @@ -104,6 +109,7 @@ Map _$GeneratorOptionsToJson(GeneratorOptions instance) => 'multipart_file_type': instance.multipartFileType, 'urlencoded_file_type': instance.urlencodedFileType, 'with_converter': instance.withConverter, + 'scalars': instance.scalars, 'overriden_models': instance.overridenModels, 'generate_to_json_for': instance.generateToJsonFor, 'additional_headers': instance.additionalHeaders, @@ -128,7 +134,6 @@ Map _$GeneratorOptionsToJson(GeneratorOptions instance) => 'import_paths': instance.importPaths, 'custom_return_type': instance.customReturnType, 'exclude_paths': instance.excludePaths, - 'generate_first_succeed_response': instance.generateFirstSucceedResponse, }; DefaultValueMap _$DefaultValueMapFromJson(Map json) => @@ -199,3 +204,16 @@ Map _$OverridenModelsItemToJson( 'overriden_models': instance.overridenModels, 'import_url': instance.importUrl, }; + +CustomScalar _$CustomScalarFromJson(Map json) => CustomScalar( + type: json['type'] as String, + deserialize: json['deserialize'] as String, + serialize: json['serialize'] as String? ?? '', + ); + +Map _$CustomScalarToJson(CustomScalar instance) => + { + 'type': instance.type, + 'deserialize': instance.deserialize, + 'serialize': instance.serialize, + }; diff --git a/test/code_examples.dart b/test/code_examples.dart index b655516f..78e60e02 100644 --- a/test/code_examples.dart +++ b/test/code_examples.dart @@ -834,3 +834,63 @@ const objectWithadditionalProperties = ''' } } '''; + +const String schemasWithUuidsInProperties = ''' +{ + "openapi": "3.0.1", + "info": { + "title": "Some service", + "version": "1.0" + }, + "components": { + "responses": { + "SpaResponse": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "required": [ + "showPageAvailable" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid", + "description": "Some description" + }, + "list": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + "showPageAvailable": { + "type": "boolean", + "description": "Flag indicating showPage availability" + } + } + } + } + } + } + }, + "schemas": { + "SpaSchema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "description": "Some description" + }, + "showPageAvailable": { + "type": "boolean", + "description": "Flag indicating showPage availability" + } + } + } + } + } +} +'''; diff --git a/test/generator_tests/models_generator_test.dart b/test/generator_tests/models_generator_test.dart index 76a6e378..ec0fe8f0 100644 --- a/test/generator_tests/models_generator_test.dart +++ b/test/generator_tests/models_generator_test.dart @@ -127,6 +127,29 @@ void main() { expect(result, contains(expectedResult)); }); + + test('Should return Uuid', () { + final generator = SwaggerModelsGeneratorV2( + GeneratorOptions( + inputFolder: '', + outputFolder: '', + importPaths: [ + 'package:uuid/uuid.dart', + ], + scalars: { + 'uuid': CustomScalar(type: 'Uuid', deserialize: 'parse') + } + ), + ); + const className = 'Person'; + const parameterName = 'id'; + final parameter = SwaggerSchema(type: 'string', format: 'uuid'); + const expectedResult = 'Uuid'; + final result = generator.getParameterTypeName( + className, parameterName, parameter, '', null); + + expect(result, contains(expectedResult)); + }); }); group('generateFieldName', () { @@ -631,4 +654,62 @@ void main() { expect(result, contains('final Map? metadata')); }); }); + + group('Tests for overridden format types', () { + test('Should include deserialize function in JsonKey annotation', () { + final map = SwaggerRoot.parse(schemasWithUuidsInProperties); + final generator = SwaggerModelsGeneratorV3( + GeneratorOptions( + inputFolder: '', + outputFolder: '', + scalars: { + 'uuid': CustomScalar( + type: 'Uuid', + deserialize: 'Uuid.parse', + ), + }, + ), + ); + + final result = generator.generate( + root: map, + fileName: 'fileName', + allEnums: [], + ); + + expect(result, contains(RegExp(r'''@_\$UuidJsonConverter\(\)\s*@JsonKey\(name: 'id'\)\s*final Uuid\? id;'''))); + expect(result, contains(RegExp(r'''@_\$UuidJsonConverter\(\)\s*@JsonKey\(name: 'list', defaultValue: \[\]\)\s*final List\? list;'''))); + expect(result, contains('class _\$UuidJsonConverter implements json.JsonConverter')); + expect(result, contains('fromJson(json) => Uuid.parse(json);')); + expect(result, contains('toJson(json) => json.toString();')); + }); + + test('Should include serialize/deserialize functions in JsonKey annotation', () { + final map = SwaggerRoot.parse(schemasWithUuidsInProperties); + final generator = SwaggerModelsGeneratorV3( + GeneratorOptions( + inputFolder: '', + outputFolder: '', + scalars: { + 'uuid': CustomScalar( + type: 'Uuid', + deserialize: 'customUuidParse', + serialize: 'customUuidToString', + ), + }, + ), + ); + + final result = generator.generate( + root: map, + fileName: 'fileName', + allEnums: [], + ); + + expect(result, contains(RegExp(r'''@_\$UuidJsonConverter\(\)\s*@JsonKey\(name: 'id'\)\s*final Uuid\? id;'''))); + expect(result, contains('class _\$UuidJsonConverter implements json.JsonConverter')); + expect(result, contains('fromJson(json) => customUuidParse(json);')); + expect(result, contains('toJson(json) => customUuidToString(json);')); + }); + }); }