Skip to content

Commit

Permalink
Feature: String format type mapping (#766)
Browse files Browse the repository at this point in the history
* Allow string property types to be overridden based on the "format" schema property

* Added overridden_formats documentation & example to readme

* Removed whitespace

* Simplified tests for overridden format json converters

* Renamed overridden_formats property to scalars

* Updated readme URL for JSON schema format info

* Fixed arrays of scalar types not getting converter added to their property
  • Loading branch information
dan3988 authored Sep 18, 2024
1 parent 1148589 commit abb8575
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 15 deletions.
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> of Regex If not empty - includes only paths matching reges |
| `exclude_paths` | `[]` | `false` | List<String> of Regex If not empty -exclude paths matching reges |
Expand All @@ -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<int>` | `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) |



Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions lib/src/code_generators/swagger_additions_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,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';" : ''}
""");
Expand Down
68 changes: 56 additions & 12 deletions lib/src/code_generators/swagger_models_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ abstract class SwaggerModelsGenerator extends SwaggerGeneratorBase {
required List<EnumModel> allEnums,
required bool generateEnumsMethods,
}) {
final converters = generateJsonConverters();
final allEnumsString = generateEnumsMethods
? allEnums
.map((e) => e.generateFromJsonToJson(options.enumsCaseSensitive))
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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))}';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -978,6 +1017,7 @@ static $returnType $fromJsonFunction($valueType? value) => $enumNameCamelCase$fr
required Map<String, SwaggerSchema> allClasses,
required bool isDeprecated,
}) {
final jsonConverterAnnotation = prop.items == null ? '' : generatePropertyJsonConverterAnnotation(prop.items!);
final typeName = _generateListPropertyTypeName(
allEnumListNames: allEnumListNames,
allEnumNames: allEnumNames,
Expand Down Expand Up @@ -1027,7 +1067,7 @@ static $returnType $fromJsonFunction($valueType? value) => $enumNameCamelCase$fr
listPropertyName = listPropertyName.makeNullable();
}

return '$jsonKeyContent$deprecatedContent final $listPropertyName ${generateFieldName(propertyName)};${unknownEnumValue.fromJson}';
return '$jsonConverterAnnotation$jsonKeyContent$deprecatedContent final $listPropertyName ${generateFieldName(propertyName)};${unknownEnumValue.fromJson}';
}

String generateGeneralPropertyContent({
Expand All @@ -1042,6 +1082,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";
Expand Down Expand Up @@ -1103,7 +1144,7 @@ static $returnType $fromJsonFunction($valueType? value) => $enumNameCamelCase$fr
typeName = typeName.makeNullable();
}

return '\t$jsonKeyContent$isDeprecatedContent final $typeName $propertyName;${unknownEnumValue.fromJson}';
return '\t$jsonConverterAnnotation$jsonKeyContent$isDeprecatedContent final $typeName $propertyName;${unknownEnumValue.fromJson}';
}

String generatePropertyContentByType(
Expand Down Expand Up @@ -1293,7 +1334,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) {
Expand All @@ -1306,7 +1347,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);
}
Expand All @@ -1319,14 +1360,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';
Expand Down
23 changes: 23 additions & 0 deletions lib/src/models/generator_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>',
Expand All @@ -55,6 +56,7 @@ class GeneratorOptions {
final String multipartFileType;
final String urlencodedFileType;
final bool withConverter;
final Map<String, CustomScalar> scalars;
final List<OverridenModelsItem> overridenModels;
final List<String> generateToJsonFor;
final List<String> additionalHeaders;
Expand Down Expand Up @@ -178,3 +180,24 @@ class OverridenModelsItem {
factory OverridenModelsItem.fromJson(Map<String, dynamic> 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<String, dynamic> json) =>
_$CustomScalarFromJson(json);

CustomScalar({
required this.type,
required this.deserialize,
this.serialize = '',
});

Map<String, dynamic> toJson() => _$CustomScalarToJson(this);
}
22 changes: 20 additions & 2 deletions lib/src/models/generator_options.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 60 additions & 0 deletions test/code_examples.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
}
}
}
''';
Loading

0 comments on commit abb8575

Please sign in to comment.