From 990a590e0e0c56282f6650eb0990e7aca002e35f Mon Sep 17 00:00:00 2001 From: Elena Ferro Date: Mon, 16 Oct 2023 16:04:20 +0200 Subject: [PATCH 1/7] feat: generating @Headers annotation on rest clients for retrofit when content type is not application json --- swagger_parser/example/swagger_parser.yaml | 0 .../generator/models/universal_request.dart | 55 +++++++++++++++++-- .../dart_retrofit_client_template.dart | 10 +++- swagger_parser/lib/src/parser/parser.dart | 46 ++++++++-------- .../test/generator/rest_clients_test.dart | 52 +++++++++--------- swagger_parser/test/parser/requests_test.dart | 4 +- 6 files changed, 108 insertions(+), 59 deletions(-) mode change 100644 => 100755 swagger_parser/example/swagger_parser.yaml diff --git a/swagger_parser/example/swagger_parser.yaml b/swagger_parser/example/swagger_parser.yaml old mode 100644 new mode 100755 diff --git a/swagger_parser/lib/src/generator/models/universal_request.dart b/swagger_parser/lib/src/generator/models/universal_request.dart index db15ac31..84eae9c4 100644 --- a/swagger_parser/lib/src/generator/models/universal_request.dart +++ b/swagger_parser/lib/src/generator/models/universal_request.dart @@ -12,11 +12,10 @@ final class UniversalRequest { required this.route, required this.returnType, required this.parameters, + final HttpContentType? contentType, this.description, - this.isMultiPart = false, - this.isFormUrlEncoded = false, this.isDeprecated = false, - }); + }) : contentType = contentType ?? HttpContentType.applicationJson; /// Request name final String name; @@ -36,11 +35,20 @@ final class UniversalRequest { /// Request parameters final List parameters; - /// Request type 'multipart/form-data' - final bool isMultiPart; + final HttpContentType contentType; + + /// Request has Content-Type 'multipart/form-data' + bool get isMultiPart => contentType.isMultipart; /// Request type 'application/x-www-form-urlencoded' - final bool isFormUrlEncoded; + bool get isFormUrlEncoded => + contentType == HttpContentType.applicationXWwwFormUrlencoded; + + /// if is application/json or multipart/form-data or application/x-www-form-urlencoded do not add header + bool get shouldAddContentTypeHeader => + !(contentType == HttpContentType.applicationJson || + isMultiPart || + isFormUrlEncoded); /// Value indicating whether this request is deprecated final bool isDeprecated; @@ -105,3 +113,38 @@ enum HttpRequestType { static HttpRequestType? fromString(String type) => HttpRequestType.values.firstWhereOrNull((e) => e.name == type); } + +/// Content-Type header of request +enum HttpContentType { + applicationOctetStream('application/octet-stream'), + applicationJson('application/json'), + applicationXml('application/xml'), + applicationJsonPatch('application/json-patch+json'), + applicationXWwwFormUrlencoded('application/x-www-form-urlencoded'), + applicationPdf('application/pdf'), + multipartFormData('multipart/form-data'), + imageGif('image/gif'), + imageJpeg('image/jpeg'), + imagePng('image/png'), + textPlain('text/plain'), + textXml('text/xml'), + textHtml('text/html'); + + const HttpContentType(this.value); + + /// The Content-Type value, e.g. "application/json". + final String value; + + static HttpContentType? fromString(String? type) => + HttpContentType.values.firstWhereOrNull( + (e) => e.value == type, + ); + + bool get isApplication => value.startsWith('application/'); + + bool get isMultipart => value.startsWith('multipart/'); + + bool get isText => value.startsWith('text/'); + + bool get isImage => value.startsWith('image/'); +} diff --git a/swagger_parser/lib/src/generator/templates/dart_retrofit_client_template.dart b/swagger_parser/lib/src/generator/templates/dart_retrofit_client_template.dart index aa096dec..9ef48977 100644 --- a/swagger_parser/lib/src/generator/templates/dart_retrofit_client_template.dart +++ b/swagger_parser/lib/src/generator/templates/dart_retrofit_client_template.dart @@ -17,7 +17,7 @@ String dartRetrofitClientTemplate({ }) { final sb = StringBuffer( ''' -${generatedFileComment(markFileAsGenerated: markFileAsGenerated)}${_fileImport(restClient)}import 'package:dio/dio.dart'; +${generatedFileComment(markFileAsGenerated: markFileAsGenerated)}${_fileImport(restClient)}import 'package:dio/dio.dart' hide Headers; import 'package:retrofit/retrofit.dart'; ${dartImports(imports: restClient.imports, pathPrefix: '../models/')} part '${name.toSnake}.g.dart'; @@ -38,7 +38,7 @@ String _toClientRequest(UniversalRequest request) { final sb = StringBuffer( ''' - ${descriptionComment(request.description, tabForFirstLine: false, tab: ' ', end: ' ')}${request.isDeprecated ? "@Deprecated('This method is marked as deprecated')\n " : ''}${request.isMultiPart ? '@MultiPart()\n ' : ''}${request.isFormUrlEncoded ? '@FormUrlEncoded()\n ' : ''}@${request.requestType.name.toUpperCase()}('${request.route}') + ${descriptionComment(request.description, tabForFirstLine: false, tab: ' ', end: ' ')}${request.isDeprecated ? "@Deprecated('This method is marked as deprecated')\n " : ''}${request.isMultiPart ? '@MultiPart()\n ' : ''}${request.isFormUrlEncoded ? '@FormUrlEncoded()\n ' : ''}${_toContentTypeHeader(request)}@${request.requestType.name.toUpperCase()}('${request.route}') Future<${request.returnType == null ? 'void' : request.returnType!.toSuitableType(ProgrammingLanguage.dart)}> ${request.name}(''', ); if (request.parameters.isNotEmpty) { @@ -73,6 +73,12 @@ String _toParameter(UniversalRequestType parameter) => '${parameter.type.toSuitableType(ProgrammingLanguage.dart)} ' '${parameter.type.name!.toCamel}${_defaultValue(parameter.type)},'; +String _toContentTypeHeader(UniversalRequest request) { + return request.shouldAddContentTypeHeader + ? "@Headers({'Content-Type': '${request.contentType.value}'})\n " + : ''; +} + /// return required if isRequired String _required(UniversalType t) => t.isRequired && t.defaultValue == null ? 'required ' : ''; diff --git a/swagger_parser/lib/src/parser/parser.dart b/swagger_parser/lib/src/parser/parser.dart index baefc1fa..4791e79b 100644 --- a/swagger_parser/lib/src/parser/parser.dart +++ b/swagger_parser/lib/src/parser/parser.dart @@ -82,11 +82,13 @@ class OpenApiParser { static const _descriptionConst = 'description'; static const _enumConst = 'enum'; static const _formatConst = 'format'; - static const _formUrlEncodedConst = 'application/x-www-form-urlencoded'; + static final _formUrlEncodedConst = + HttpContentType.applicationXWwwFormUrlencoded.value; static const _inConst = 'in'; static const _infoConst = 'info'; static const _itemsConst = 'items'; - static const _multipartFormDataConst = 'multipart/form-data'; + static final _multipartFormDataConst = + HttpContentType.multipartFormData.value; static const _nameConst = 'name'; static const _nullableConst = 'nullable'; static const _objectConst = 'object'; @@ -129,8 +131,7 @@ class OpenApiParser { Iterable parseRestClients() { final restClients = []; final imports = SplayTreeSet(); - var isMultiPart = false; - var isFormUrlEncoded = false; + HttpContentType? httpContentType = null; /// Parses return type for client query for OpenApi v3 UniversalType? returnTypeV3( @@ -234,16 +235,20 @@ class OpenApiParser { if (!requestBody.containsKey(_contentConst)) { throw const ParserException('Request body must always have content.'); } + final contentTypes = requestBody[_contentConst] as Map; Map? contentType; + httpContentType = + HttpContentType.fromString(contentTypes.entries.first.key); + if (contentTypes.containsKey(_multipartFormDataConst)) { contentType = contentTypes[_multipartFormDataConst] as Map; - isMultiPart = true; + httpContentType = HttpContentType.multipartFormData; } else if (contentTypes.containsKey(_formUrlEncodedConst)) { contentType = contentTypes[_formUrlEncodedConst] as Map; - isFormUrlEncoded = true; + httpContentType = HttpContentType.applicationXWwwFormUrlencoded; } else { final content = (requestBody[_contentConst] as Map) .entries @@ -251,12 +256,14 @@ class OpenApiParser { contentType = content == null ? null : content.value as Map; } + if (contentType == null) { throw const ParserException( 'Response must always have a content type.', ); } - if (isMultiPart) { + + if (httpContentType!.isMultipart) { if ((contentType[_schemaConst] as Map) .containsKey(_refConst)) { final isRequired = requestBody[_requiredConst]?.toString().toBool(); @@ -284,11 +291,11 @@ class OpenApiParser { ), ); } - final schemaContentType = + final schemaContent = contentType[_schemaConst] as Map; - if (schemaContentType.containsKey(_propertiesConst)) { + if (schemaContent.containsKey(_propertiesConst)) { for (final e - in (schemaContentType[_propertiesConst] as Map) + in (schemaContent[_propertiesConst] as Map) .entries) { final typeWithImport = _findType( e.value as Map, @@ -376,15 +383,10 @@ class OpenApiParser { if (!map.containsKey(_parametersConst)) { return types; } - if (map.containsKey(_consumesConst) && - (map[_consumesConst] as List) - .contains(_multipartFormDataConst)) { - isMultiPart = true; - } - if (map.containsKey(_consumesConst) && - (map[_consumesConst] as List) - .contains(_formUrlEncodedConst)) { - isFormUrlEncoded = true; + + if (map.containsKey(_consumesConst)) { + final consumes = map[_consumesConst] as List; + httpContentType = HttpContentType.fromString(consumes.first.toString()); } for (final rawParameter in map[_parametersConst] as List) { final isRequired = @@ -469,8 +471,7 @@ class OpenApiParser { description: description, requestType: HttpRequestType.fromString(key)!, route: path, - isMultiPart: isMultiPart, - isFormUrlEncoded: isFormUrlEncoded, + contentType: httpContentType, returnType: returnType, parameters: parameters, isDeprecated: requestPath[_deprecatedConst].toString().toBool(), @@ -490,8 +491,7 @@ class OpenApiParser { restClients[sameTagIndex].requests.add(request); restClients[sameTagIndex].imports.addAll(imports); } - isMultiPart = false; - isFormUrlEncoded = false; + httpContentType = HttpContentType.applicationJson; imports.clear(); }); }); diff --git a/swagger_parser/test/generator/rest_clients_test.dart b/swagger_parser/test/generator/rest_clients_test.dart index 0ee93c0f..ac684a8d 100644 --- a/swagger_parser/test/generator/rest_clients_test.dart +++ b/swagger_parser/test/generator/rest_clients_test.dart @@ -16,7 +16,7 @@ void main() { const fillController = FillController(); final filledContent = fillController.fillRestClientContent(restClient); const expectedContents = ''' -import 'package:dio/dio.dart'; +import 'package:dio/dio.dart' hide Headers; import 'package:retrofit/retrofit.dart'; part 'some_client.g.dart'; @@ -52,7 +52,7 @@ interface SomeClient {} const fillController = FillController(putClientsInFolder: true); final filledContent = fillController.fillRestClientContent(restClient); const expectedContents = ''' -import 'package:dio/dio.dart'; +import 'package:dio/dio.dart' hide Headers; import 'package:retrofit/retrofit.dart'; part 'some_client.g.dart'; @@ -89,7 +89,7 @@ interface SomeClient {} const fillController = FillController(clientPostfix: 'Api'); final filledContent = fillController.fillRestClientContent(restClient); const expectedContents = ''' -import 'package:dio/dio.dart'; +import 'package:dio/dio.dart' hide Headers; import 'package:retrofit/retrofit.dart'; part 'class_name_api.g.dart'; @@ -126,7 +126,7 @@ interface SomeApi {} const fillController = FillController(clientPostfix: ''); final filledContent = fillController.fillRestClientContent(restClient); const expectedContents = ''' -import 'package:dio/dio.dart'; +import 'package:dio/dio.dart' hide Headers; import 'package:retrofit/retrofit.dart'; part 'some.g.dart'; @@ -172,7 +172,7 @@ interface Some {} const fillController = FillController(); final filledContent = fillController.fillRestClientContent(restClient); const expectedContents = ''' -import 'package:dio/dio.dart'; +import 'package:dio/dio.dart' hide Headers; import 'package:retrofit/retrofit.dart'; import '../models/camel_class.dart'; @@ -213,7 +213,7 @@ abstract class ClassNameClient { const fillController = FillController(); final filledContent = fillController.fillRestClientContent(restClient); const expectedContents = ''' -import 'package:dio/dio.dart'; +import 'package:dio/dio.dart' hide Headers; import 'package:retrofit/retrofit.dart'; part 'class_name_client.g.dart'; @@ -333,7 +333,7 @@ interface ClassNameClient { const fillController = FillController(); final filledContent = fillController.fillRestClientContent(restClient); const expectedContents = ''' -import 'package:dio/dio.dart'; +import 'package:dio/dio.dart' hide Headers; import 'package:retrofit/retrofit.dart'; part 'class_name_client.g.dart'; @@ -515,7 +515,7 @@ interface ClassNameClient { const fillController = FillController(); final filledContent = fillController.fillRestClientContent(restClient); const expectedContents = ''' -import 'package:dio/dio.dart'; +import 'package:dio/dio.dart' hide Headers; import 'package:retrofit/retrofit.dart'; part 'class_name_client.g.dart'; @@ -630,7 +630,7 @@ interface ClassNameClient { const fillController = FillController(); final filledContent = fillController.fillRestClientContent(restClient); const expectedContents = ''' -import 'package:dio/dio.dart'; +import 'package:dio/dio.dart' hide Headers; import 'package:retrofit/retrofit.dart'; part 'class_name_client.g.dart'; @@ -737,7 +737,7 @@ interface ClassNameClient { const fillController = FillController(); final filledContent = fillController.fillRestClientContent(restClient); const expectedContents = ''' -import 'package:dio/dio.dart'; +import 'package:dio/dio.dart' hide Headers; import 'package:retrofit/retrofit.dart'; part 'class_name_client.g.dart'; @@ -831,7 +831,7 @@ interface ClassNameClient { const fillController = FillController(); final filledContent = fillController.fillRestClientContent(restClient); const expectedContents = ''' -import 'package:dio/dio.dart'; +import 'package:dio/dio.dart' hide Headers; import 'package:retrofit/retrofit.dart'; part 'class_name_client.g.dart'; @@ -918,7 +918,7 @@ interface ClassNameClient { requestType: HttpRequestType.post, route: '/send', returnType: null, - isMultiPart: true, + contentType: HttpContentType.multipartFormData, parameters: [ UniversalRequestType( parameterType: HttpParameterType.header, @@ -956,7 +956,7 @@ interface ClassNameClient { requestType: HttpRequestType.post, route: '/single', returnType: UniversalType(type: 'boolean'), - isMultiPart: true, + contentType: HttpContentType.multipartFormData, parameters: [ UniversalRequestType( parameterType: HttpParameterType.body, @@ -971,7 +971,7 @@ interface ClassNameClient { const expectedContents = ''' import 'dart:io'; -import 'package:dio/dio.dart'; +import 'package:dio/dio.dart' hide Headers; import 'package:retrofit/retrofit.dart'; import '../models/another_file.dart'; @@ -1012,7 +1012,7 @@ abstract class ClassNameClient { requestType: HttpRequestType.post, route: '/send', returnType: null, - isMultiPart: true, + contentType: HttpContentType.multipartFormData, parameters: [ UniversalRequestType( parameterType: HttpParameterType.header, @@ -1050,7 +1050,7 @@ abstract class ClassNameClient { requestType: HttpRequestType.post, route: '/single', returnType: UniversalType(type: 'boolean'), - isMultiPart: true, + contentType: HttpContentType.multipartFormData, parameters: [ UniversalRequestType( parameterType: HttpParameterType.body, @@ -1100,7 +1100,7 @@ interface ClassNameClient { requestType: HttpRequestType.post, route: '/send', returnType: null, - isFormUrlEncoded: true, + contentType: HttpContentType.applicationXWwwFormUrlencoded, parameters: [ UniversalRequestType( parameterType: HttpParameterType.header, @@ -1118,7 +1118,7 @@ interface ClassNameClient { const fillController = FillController(); final filledContent = fillController.fillRestClientContent(restClient); const expectedContents = ''' -import 'package:dio/dio.dart'; +import 'package:dio/dio.dart' hide Headers; import 'package:retrofit/retrofit.dart'; import '../models/lol.dart'; @@ -1150,7 +1150,7 @@ abstract class ClassNameClient { requestType: HttpRequestType.post, route: '/send', returnType: null, - isFormUrlEncoded: true, + contentType: HttpContentType.applicationXWwwFormUrlencoded, parameters: [ UniversalRequestType( parameterType: HttpParameterType.header, @@ -1232,7 +1232,7 @@ interface ClassNameClient { const fillController = FillController(); final filledContent = fillController.fillRestClientContent(restClient); const expectedContents = ''' -import 'package:dio/dio.dart'; +import 'package:dio/dio.dart' hide Headers; import 'package:retrofit/retrofit.dart'; part 'class_name_client.g.dart'; @@ -1382,7 +1382,7 @@ interface ClassNameClient { const fillController = FillController(); final filledContent = fillController.fillRestClientContent(restClient); const expectedContents = ''' -import 'package:dio/dio.dart'; +import 'package:dio/dio.dart' hide Headers; import 'package:retrofit/retrofit.dart'; import '../models/unit.dart'; @@ -1539,7 +1539,7 @@ interface ClassNameClient { const fillController = FillController(); final filledContent = fillController.fillRestClientContent(restClient); const expectedContents = ''' -import 'package:dio/dio.dart'; +import 'package:dio/dio.dart' hide Headers; import 'package:retrofit/retrofit.dart'; part 'class_name_client.g.dart'; @@ -1631,7 +1631,7 @@ abstract class ClassNameClient { const fillController = FillController(); final filledContent = fillController.fillRestClientContent(restClient); const expectedContents = ''' -import 'package:dio/dio.dart'; +import 'package:dio/dio.dart' hide Headers; import 'package:retrofit/retrofit.dart'; part 'class_name_client.g.dart'; @@ -1827,7 +1827,7 @@ interface ClassNameClient { const fillController = FillController(); final filledContent = fillController.fillRestClientContent(restClient); const expectedContents = ''' -import 'package:dio/dio.dart'; +import 'package:dio/dio.dart' hide Headers; import 'package:retrofit/retrofit.dart'; import '../models/some.dart'; @@ -1925,7 +1925,7 @@ interface ClassNameClient { const fillController = FillController(); final filledContent = fillController.fillRestClientContent(restClient); const expectedContents = ''' -import 'package:dio/dio.dart'; +import 'package:dio/dio.dart' hide Headers; import 'package:retrofit/retrofit.dart'; part 'class_name_client.g.dart'; @@ -2017,7 +2017,7 @@ abstract class ClassNameClient { const fillController = FillController(); final filledContent = fillController.fillRestClientContent(restClient); const expectedContents = ''' -import 'package:dio/dio.dart'; +import 'package:dio/dio.dart' hide Headers; import 'package:retrofit/retrofit.dart'; part 'class_name_client.g.dart'; diff --git a/swagger_parser/test/parser/requests_test.dart b/swagger_parser/test/parser/requests_test.dart index c9732e73..752b34cd 100644 --- a/swagger_parser/test/parser/requests_test.dart +++ b/swagger_parser/test/parser/requests_test.dart @@ -81,7 +81,7 @@ void main() { requestType: HttpRequestType.patch, route: '/api/User/{id}/avatar', returnType: null, - isMultiPart: true, + contentType: HttpContentType.multipartFormData, parameters: [ UniversalRequestType( name: 'avatar', @@ -184,7 +184,7 @@ void main() { requestType: HttpRequestType.patch, route: '/api/User/{id}/avatar', returnType: null, - isMultiPart: true, + contentType: HttpContentType.multipartFormData, parameters: [ UniversalRequestType( name: 'id', From 9b7443fb2cb21873e7995c76e86af377c4eb1859 Mon Sep 17 00:00:00 2001 From: Roman Laptev Date: Thu, 26 Oct 2023 13:43:50 +0300 Subject: [PATCH 2/7] Change content-type from enum to String --- .../lib/src/config/yaml_config.dart | 10 ++++ .../lib/src/generator/generator.dart | 12 +++-- .../generator/models/universal_request.dart | 50 ++----------------- swagger_parser/lib/src/parser/parser.dart | 9 ++-- .../test/generator/rest_clients_test.dart | 12 ++--- swagger_parser/test/parser/requests_test.dart | 4 +- .../parser/schemas/basic_types_class.3.0.json | 1 - .../parser/schemas/of_like_class.3.1.json | 14 +++--- .../schemas/reference_types_class.3.0.json | 2 +- 9 files changed, 45 insertions(+), 69 deletions(-) diff --git a/swagger_parser/lib/src/config/yaml_config.dart b/swagger_parser/lib/src/config/yaml_config.dart index 7557e5bc..b03e52da 100644 --- a/swagger_parser/lib/src/config/yaml_config.dart +++ b/swagger_parser/lib/src/config/yaml_config.dart @@ -39,6 +39,7 @@ final class YamlConfig { this.unknownEnumValue, this.markFilesAsGenerated, this.originalHttpResponse, + this.defaultContentType, this.replacementRules = const [], }); @@ -218,6 +219,13 @@ final class YamlConfig { ); } + final defaultContentType = yamlConfig['default_content_type']; + if (defaultContentType is! String?) { + throw const ConfigException( + "Config parameter 'default_content_type' must be String.", + ); + } + final originalHttpResponse = yamlConfig['original_http_response']; if (originalHttpResponse is! bool?) { throw const ConfigException( @@ -277,6 +285,7 @@ final class YamlConfig { unknownEnumValue: unknownEnumValue ?? rootConfig?.unknownEnumValue, markFilesAsGenerated: markFilesAsGenerated ?? rootConfig?.markFilesAsGenerated, + defaultContentType: defaultContentType ?? rootConfig?.defaultContentType, replacementRules: replacementRules ?? rootConfig?.replacementRules ?? [], ); } @@ -372,6 +381,7 @@ final class YamlConfig { final bool? enumsPrefix; final bool? unknownEnumValue; final bool? markFilesAsGenerated; + final String? defaultContentType; final bool? originalHttpResponse; final List replacementRules; } diff --git a/swagger_parser/lib/src/generator/generator.dart b/swagger_parser/lib/src/generator/generator.dart index e1e7a9cf..63e71ca1 100644 --- a/swagger_parser/lib/src/generator/generator.dart +++ b/swagger_parser/lib/src/generator/generator.dart @@ -46,6 +46,7 @@ final class Generator { bool? enumsToJson, bool? enumsPrefix, bool? unknownEnumValue, + String? defaultContentType, bool? markFilesAsGenerated, List? replacementRules, }) : _schemaPath = schemaPath, @@ -68,8 +69,9 @@ final class Generator { _putInFolder = putInFolder ?? false, _enumsToJson = enumsToJson ?? false, _enumsPrefix = enumsPrefix ?? false, - unknownEnumValue = unknownEnumValue ?? true, + _unknownEnumValue = unknownEnumValue ?? true, _markFilesAsGenerated = markFilesAsGenerated ?? true, + _defaultContentType = defaultContentType ?? 'application/json', _replacementRules = replacementRules ?? const []; /// Applies parameters set from yaml config file @@ -160,11 +162,14 @@ final class Generator { final bool _enumsPrefix; /// If true, adds an unknown value for all enums to maintain backward compatibility when adding new values on the backend. - final bool unknownEnumValue; + final bool _unknownEnumValue; /// If true, generated files will be marked as generated final bool _markFilesAsGenerated; + /// Content type for all requests, default 'application/json' + final String _defaultContentType; + /// List of rules used to replace patterns in generated class names final List _replacementRules; @@ -270,6 +275,7 @@ final class Generator { squashClients: _squashClients, replacementRules: _replacementRules, originalHttpResponse: _originalHttpResponse, + defaultContentType: _defaultContentType, ); _openApiInfo = parser.parseOpenApiInfo(); _restClients = parser.parseRestClients(); @@ -299,7 +305,7 @@ final class Generator { freezed: _freezed, putClientsInFolder: _putClientsInFolder, enumsToJson: _enumsToJson, - unknownEnumValue: unknownEnumValue, + unknownEnumValue: _unknownEnumValue, markFilesAsGenerated: _markFilesAsGenerated, ); final files = []; diff --git a/swagger_parser/lib/src/generator/models/universal_request.dart b/swagger_parser/lib/src/generator/models/universal_request.dart index 36621a05..ee2e9290 100644 --- a/swagger_parser/lib/src/generator/models/universal_request.dart +++ b/swagger_parser/lib/src/generator/models/universal_request.dart @@ -12,7 +12,7 @@ final class UniversalRequest { required this.route, required this.returnType, required this.parameters, - final HttpContentType? contentType, + this.contentType = 'application/json', this.description, this.isDeprecated = false, this.isOriginalHttpResponse = false, @@ -36,20 +36,15 @@ final class UniversalRequest { /// Request parameters final List parameters; - final HttpContentType contentType; + /// Request content-type + final String contentType; /// Request has Content-Type 'multipart/form-data' - bool get isMultiPart => contentType.isMultipart; + bool get isMultiPart => contentType == 'multipart/form-data'; /// Request type 'application/x-www-form-urlencoded' bool get isFormUrlEncoded => - contentType == HttpContentType.applicationXWwwFormUrlencoded; - - /// if is application/json or multipart/form-data or application/x-www-form-urlencoded do not add header - bool get shouldAddContentTypeHeader => - !(contentType == HttpContentType.applicationJson || - isMultiPart || - isFormUrlEncoded); + contentType == 'application/x-www-form-urlencoded'; /// Value indicating whether this request is deprecated final bool isDeprecated; @@ -117,38 +112,3 @@ enum HttpRequestType { static HttpRequestType? fromString(String type) => HttpRequestType.values.firstWhereOrNull((e) => e.name == type); } - -/// Content-Type header of request -enum HttpContentType { - applicationOctetStream('application/octet-stream'), - applicationJson('application/json'), - applicationXml('application/xml'), - applicationJsonPatch('application/json-patch+json'), - applicationXWwwFormUrlencoded('application/x-www-form-urlencoded'), - applicationPdf('application/pdf'), - multipartFormData('multipart/form-data'), - imageGif('image/gif'), - imageJpeg('image/jpeg'), - imagePng('image/png'), - textPlain('text/plain'), - textXml('text/xml'), - textHtml('text/html'); - - const HttpContentType(this.value); - - /// The Content-Type value, e.g. "application/json". - final String value; - - static HttpContentType? fromString(String? type) => - HttpContentType.values.firstWhereOrNull( - (e) => e.value == type, - ); - - bool get isApplication => value.startsWith('application/'); - - bool get isMultipart => value.startsWith('multipart/'); - - bool get isText => value.startsWith('text/'); - - bool get isImage => value.startsWith('image/'); -} diff --git a/swagger_parser/lib/src/parser/parser.dart b/swagger_parser/lib/src/parser/parser.dart index d0d1c3b9..2729e14d 100644 --- a/swagger_parser/lib/src/parser/parser.dart +++ b/swagger_parser/lib/src/parser/parser.dart @@ -30,12 +30,14 @@ class OpenApiParser { bool pathMethodName = false, bool squashClients = false, bool originalHttpResponse = false, + String defaultContentType = 'application/json', List replacementRules = const [], }) : _name = name, _pathMethodName = pathMethodName, _enumsPrefix = enumsPrefix, _squashClients = squashClients, _originalHttpResponse = originalHttpResponse, + _defaultContentType = defaultContentType, _replacementRules = replacementRules { _definitionFileContent = isYaml ? (loadYaml(fileContent) as YamlMap).toMap() @@ -65,6 +67,7 @@ class OpenApiParser { final String? _name; final bool _squashClients; final bool _originalHttpResponse; + final String _defaultContentType; final List _replacementRules; late final Map _definitionFileContent; late final OAS _version; @@ -85,13 +88,11 @@ class OpenApiParser { static const _descriptionConst = 'description'; static const _enumConst = 'enum'; static const _formatConst = 'format'; - static final _formUrlEncodedConst = - HttpContentType.applicationXWwwFormUrlencoded.value; + static const _formUrlEncodedConst = 'application/x-www-form-urlencoded'; static const _inConst = 'in'; static const _infoConst = 'info'; static const _itemsConst = 'items'; - static final _multipartFormDataConst = - HttpContentType.multipartFormData.value; + static const _multipartFormDataConst = 'multipart/form-data'; static const _nameConst = 'name'; static const _nullableConst = 'nullable'; static const _objectConst = 'object'; diff --git a/swagger_parser/test/generator/rest_clients_test.dart b/swagger_parser/test/generator/rest_clients_test.dart index ac684a8d..083d12e2 100644 --- a/swagger_parser/test/generator/rest_clients_test.dart +++ b/swagger_parser/test/generator/rest_clients_test.dart @@ -918,7 +918,7 @@ interface ClassNameClient { requestType: HttpRequestType.post, route: '/send', returnType: null, - contentType: HttpContentType.multipartFormData, + contentType: 'multipart/form-data', parameters: [ UniversalRequestType( parameterType: HttpParameterType.header, @@ -956,7 +956,7 @@ interface ClassNameClient { requestType: HttpRequestType.post, route: '/single', returnType: UniversalType(type: 'boolean'), - contentType: HttpContentType.multipartFormData, + contentType: 'multipart/form-data', parameters: [ UniversalRequestType( parameterType: HttpParameterType.body, @@ -1012,7 +1012,7 @@ abstract class ClassNameClient { requestType: HttpRequestType.post, route: '/send', returnType: null, - contentType: HttpContentType.multipartFormData, + contentType: 'multipart/form-data', parameters: [ UniversalRequestType( parameterType: HttpParameterType.header, @@ -1050,7 +1050,7 @@ abstract class ClassNameClient { requestType: HttpRequestType.post, route: '/single', returnType: UniversalType(type: 'boolean'), - contentType: HttpContentType.multipartFormData, + contentType: 'multipart/form-data', parameters: [ UniversalRequestType( parameterType: HttpParameterType.body, @@ -1100,7 +1100,7 @@ interface ClassNameClient { requestType: HttpRequestType.post, route: '/send', returnType: null, - contentType: HttpContentType.applicationXWwwFormUrlencoded, + contentType: 'application/x-www-form-urlencoded', parameters: [ UniversalRequestType( parameterType: HttpParameterType.header, @@ -1150,7 +1150,7 @@ abstract class ClassNameClient { requestType: HttpRequestType.post, route: '/send', returnType: null, - contentType: HttpContentType.applicationXWwwFormUrlencoded, + contentType: 'application/x-www-form-urlencoded', parameters: [ UniversalRequestType( parameterType: HttpParameterType.header, diff --git a/swagger_parser/test/parser/requests_test.dart b/swagger_parser/test/parser/requests_test.dart index 752b34cd..5f3cd65a 100644 --- a/swagger_parser/test/parser/requests_test.dart +++ b/swagger_parser/test/parser/requests_test.dart @@ -81,7 +81,7 @@ void main() { requestType: HttpRequestType.patch, route: '/api/User/{id}/avatar', returnType: null, - contentType: HttpContentType.multipartFormData, + contentType: 'multipart/form-data', parameters: [ UniversalRequestType( name: 'avatar', @@ -184,7 +184,7 @@ void main() { requestType: HttpRequestType.patch, route: '/api/User/{id}/avatar', returnType: null, - contentType: HttpContentType.multipartFormData, + contentType: 'multipart/form-data', parameters: [ UniversalRequestType( name: 'id', diff --git a/swagger_parser/test/parser/schemas/basic_types_class.3.0.json b/swagger_parser/test/parser/schemas/basic_types_class.3.0.json index 0d56c585..d37431b7 100644 --- a/swagger_parser/test/parser/schemas/basic_types_class.3.0.json +++ b/swagger_parser/test/parser/schemas/basic_types_class.3.0.json @@ -1,4 +1,3 @@ - { "openapi": "3.0.0", "paths": {}, diff --git a/swagger_parser/test/parser/schemas/of_like_class.3.1.json b/swagger_parser/test/parser/schemas/of_like_class.3.1.json index 5bc8f6a3..f96759ad 100644 --- a/swagger_parser/test/parser/schemas/of_like_class.3.1.json +++ b/swagger_parser/test/parser/schemas/of_like_class.3.1.json @@ -9,14 +9,14 @@ "allClass": { "allOf": [ { - "$ref": "#/components/schemas/EnumClass" + "$ref": "#/components/schemas/EnumClass" } ] }, "anyClass": { "anyOf": [ { - "$ref": "#/components/schemas/EnumClass" + "$ref": "#/components/schemas/EnumClass" } ], "default": "value1" @@ -24,14 +24,14 @@ "oneClass": { "oneOf": [ { - "$ref": "#/components/schemas/EnumClass" + "$ref": "#/components/schemas/EnumClass" } ] }, "allType": { "allOf": [ { - "type": "integer" + "type": "integer" } ] }, @@ -83,11 +83,11 @@ "EnumClass": { "type": "string", "enum": [ - "value1", - "value2" + "value1", + "value2" ], "title": "EnumClass" - } + } } } } \ No newline at end of file diff --git a/swagger_parser/test/parser/schemas/reference_types_class.3.0.json b/swagger_parser/test/parser/schemas/reference_types_class.3.0.json index 5ab15db8..378e01a3 100644 --- a/swagger_parser/test/parser/schemas/reference_types_class.3.0.json +++ b/swagger_parser/test/parser/schemas/reference_types_class.3.0.json @@ -12,7 +12,7 @@ "type": "integer" }, "another": { - "$ref": "#/components/schemas/AnotherClass" + "$ref": "#/components/schemas/AnotherClass" } } } From 2aef9f381961cfb7773b021521920d6a4bc18354 Mon Sep 17 00:00:00 2001 From: Carapacik Date: Thu, 26 Oct 2023 17:23:25 +0300 Subject: [PATCH 3/7] Fix template --- .../lib/src/generator/fill_controller.dart | 6 ++++- .../lib/src/generator/generator.dart | 1 + .../models/programming_language.dart | 2 ++ .../dart_retrofit_client_template.dart | 25 +++++++++++++------ swagger_parser/lib/src/parser/parser.dart | 2 -- 5 files changed, 26 insertions(+), 10 deletions(-) diff --git a/swagger_parser/lib/src/generator/fill_controller.dart b/swagger_parser/lib/src/generator/fill_controller.dart index 6e0cea9d..dc2dbb7c 100644 --- a/swagger_parser/lib/src/generator/fill_controller.dart +++ b/swagger_parser/lib/src/generator/fill_controller.dart @@ -18,6 +18,7 @@ final class FillController { bool enumsToJson = false, bool unknownEnumValue = true, bool markFilesAsGenerated = false, + String defaultContentType = 'application/json', }) : _openApiInfo = openApiInfo, _clientPostfix = clientPostfix, _programmingLanguage = programmingLanguage, @@ -26,7 +27,8 @@ final class FillController { _freezed = freezed, _enumsToJson = enumsToJson, _unknownEnumValue = unknownEnumValue, - _markFilesAsGenerated = markFilesAsGenerated; + _markFilesAsGenerated = markFilesAsGenerated, + _defaultContentType = defaultContentType; final OpenApiInfo _openApiInfo; final ProgrammingLanguage _programmingLanguage; @@ -37,6 +39,7 @@ final class FillController { final bool _enumsToJson; final bool _unknownEnumValue; final bool _markFilesAsGenerated; + final String _defaultContentType; /// Return [GeneratedFile] generated from given [UniversalDataClass] GeneratedFile fillDtoContent(UniversalDataClass dataClass) => GeneratedFile( @@ -66,6 +69,7 @@ final class FillController { restClient, restClient.name.toPascal + _clientPostfix.toPascal, markFilesAsGenerated: _markFilesAsGenerated, + defaultContentType: _defaultContentType, ), ); } diff --git a/swagger_parser/lib/src/generator/generator.dart b/swagger_parser/lib/src/generator/generator.dart index 63e71ca1..838eeeda 100644 --- a/swagger_parser/lib/src/generator/generator.dart +++ b/swagger_parser/lib/src/generator/generator.dart @@ -307,6 +307,7 @@ final class Generator { enumsToJson: _enumsToJson, unknownEnumValue: _unknownEnumValue, markFilesAsGenerated: _markFilesAsGenerated, + defaultContentType: _defaultContentType, ); final files = []; for (final client in _restClients) { diff --git a/swagger_parser/lib/src/generator/models/programming_language.dart b/swagger_parser/lib/src/generator/models/programming_language.dart index 7b176e0b..c29b6740 100644 --- a/swagger_parser/lib/src/generator/models/programming_language.dart +++ b/swagger_parser/lib/src/generator/models/programming_language.dart @@ -96,12 +96,14 @@ enum ProgrammingLanguage { UniversalRestClient restClient, String name, { required bool markFilesAsGenerated, + required String defaultContentType, }) => switch (this) { dart => dartRetrofitClientTemplate( restClient: restClient, name: name, markFileAsGenerated: markFilesAsGenerated, + defaultContentType: defaultContentType, ), kotlin => kotlinRetrofitClientTemplate( restClient: restClient, diff --git a/swagger_parser/lib/src/generator/templates/dart_retrofit_client_template.dart b/swagger_parser/lib/src/generator/templates/dart_retrofit_client_template.dart index 3cf0aa48..8b98442e 100644 --- a/swagger_parser/lib/src/generator/templates/dart_retrofit_client_template.dart +++ b/swagger_parser/lib/src/generator/templates/dart_retrofit_client_template.dart @@ -14,6 +14,7 @@ String dartRetrofitClientTemplate({ required UniversalRestClient restClient, required String name, required bool markFileAsGenerated, + required String defaultContentType, }) { final sb = StringBuffer( ''' @@ -28,20 +29,20 @@ abstract class $name { ''', ); for (final request in restClient.requests) { - sb.write(_toClientRequest(request)); + sb.write(_toClientRequest(request, defaultContentType)); } sb.write('}\n'); return sb.toString(); } -String _toClientRequest(UniversalRequest request) { +String _toClientRequest(UniversalRequest request, String defaultContentType) { final responseType = request.returnType == null ? 'void' : request.returnType!.toSuitableType(ProgrammingLanguage.dart); final sb = StringBuffer( ''' - ${descriptionComment(request.description, tabForFirstLine: false, tab: ' ', end: ' ')}${request.isDeprecated ? "@Deprecated('This method is marked as deprecated')\n " : ''}${request.isMultiPart ? '@MultiPart()\n ' : ''}${request.isFormUrlEncoded ? '@FormUrlEncoded()\n ' : ''}@${request.requestType.name.toUpperCase()}('${request.route}') + ${descriptionComment(request.description, tabForFirstLine: false, tab: ' ', end: ' ')}${request.isDeprecated ? "@Deprecated('This method is marked as deprecated')\n " : ''}${_contentTypeHeader(request, defaultContentType)}@${request.requestType.name.toUpperCase()}('${request.route}') Future<${request.isOriginalHttpResponse ? 'HttpResponse<$responseType>' : responseType}> ${request.name}(''', ); if (request.parameters.isNotEmpty) { @@ -76,10 +77,20 @@ String _toParameter(UniversalRequestType parameter) => '${parameter.type.toSuitableType(ProgrammingLanguage.dart)} ' '${parameter.type.name!.toCamel}${_defaultValue(parameter.type)},'; -String _toContentTypeHeader(UniversalRequest request) { - return request.shouldAddContentTypeHeader - ? "@Headers({'Content-Type': '${request.contentType.value}'})\n " - : ''; +String _contentTypeHeader( + UniversalRequest request, + String defaultContentType, +) { + if (request.isMultiPart) { + return '@MultiPart()\n '; + } + if (request.isFormUrlEncoded) { + return '@FormUrlEncoded()\n '; + } + if (request.contentType != defaultContentType) { + return "@Headers({'Content-Type': '${request.contentType}'})\n "; + } + return ''; } /// return required if isRequired diff --git a/swagger_parser/lib/src/parser/parser.dart b/swagger_parser/lib/src/parser/parser.dart index 2729e14d..30bbe6b9 100644 --- a/swagger_parser/lib/src/parser/parser.dart +++ b/swagger_parser/lib/src/parser/parser.dart @@ -477,8 +477,6 @@ class OpenApiParser { requestType: HttpRequestType.fromString(key)!, route: path, contentType: httpContentType, - isMultiPart: isMultiPart, - isFormUrlEncoded: isFormUrlEncoded, isOriginalHttpResponse: _originalHttpResponse, returnType: returnType, parameters: parameters, From 21aba6ec2b9f9c0646bb2be061a7696a7d7bd50a Mon Sep 17 00:00:00 2001 From: Carapacik Date: Thu, 26 Oct 2023 17:50:08 +0300 Subject: [PATCH 4/7] Fix parser --- swagger_parser/lib/src/parser/parser.dart | 32 ++++++++++++----------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/swagger_parser/lib/src/parser/parser.dart b/swagger_parser/lib/src/parser/parser.dart index 30bbe6b9..e6d8c7c4 100644 --- a/swagger_parser/lib/src/parser/parser.dart +++ b/swagger_parser/lib/src/parser/parser.dart @@ -135,7 +135,7 @@ class OpenApiParser { Iterable parseRestClients() { final restClients = []; final imports = SplayTreeSet(); - HttpContentType? httpContentType = null; + var resultContentType = _defaultContentType; /// Parses return type for client query for OpenApi v3 UniversalType? returnTypeV3( @@ -243,23 +243,20 @@ class OpenApiParser { final contentTypes = requestBody[_contentConst] as Map; Map? contentType; - httpContentType = - HttpContentType.fromString(contentTypes.entries.first.key); - if (contentTypes.containsKey(_multipartFormDataConst)) { contentType = contentTypes[_multipartFormDataConst] as Map; - httpContentType = HttpContentType.multipartFormData; + resultContentType = _multipartFormDataConst; } else if (contentTypes.containsKey(_formUrlEncodedConst)) { contentType = contentTypes[_formUrlEncodedConst] as Map; - httpContentType = HttpContentType.applicationXWwwFormUrlencoded; + resultContentType = _formUrlEncodedConst; } else { - final content = (requestBody[_contentConst] as Map) - .entries - .firstOrNull; + final content = contentTypes.containsKey(_defaultContentType) + ? contentTypes[_defaultContentType] + : contentTypes.entries.firstOrNull?.value; contentType = - content == null ? null : content.value as Map; + content == null ? null : content as Map; } if (contentType == null) { @@ -268,7 +265,7 @@ class OpenApiParser { ); } - if (httpContentType!.isMultipart) { + if (resultContentType == _multipartFormDataConst) { if ((contentType[_schemaConst] as Map) .containsKey(_refConst)) { final isRequired = requestBody[_requiredConst]?.toString().toBool(); @@ -389,9 +386,14 @@ class OpenApiParser { return types; } - if (map.containsKey(_consumesConst)) { + if (map.containsKey(_consumesConst) && + map[_consumesConst] is List) { final consumes = map[_consumesConst] as List; - httpContentType = HttpContentType.fromString(consumes.first.toString()); + if (consumes.contains(_multipartFormDataConst)) { + resultContentType = _multipartFormDataConst; + } else if (consumes.contains(_formUrlEncodedConst)) { + resultContentType = _formUrlEncodedConst; + } } for (final parameter in map[_parametersConst] as List) { final isRequired = (parameter as Map)[_requiredConst] @@ -476,7 +478,7 @@ class OpenApiParser { description: description, requestType: HttpRequestType.fromString(key)!, route: path, - contentType: httpContentType, + contentType: resultContentType, isOriginalHttpResponse: _originalHttpResponse, returnType: returnType, parameters: parameters, @@ -497,7 +499,7 @@ class OpenApiParser { restClients[sameTagIndex].requests.add(request); restClients[sameTagIndex].imports.addAll(imports); } - httpContentType = HttpContentType.applicationJson; + resultContentType = _defaultContentType; imports.clear(); }); }); From 6e743a8716e066f910b48ef6845b6557be34e4a2 Mon Sep 17 00:00:00 2001 From: Carapacik Date: Thu, 26 Oct 2023 17:53:13 +0300 Subject: [PATCH 5/7] Changelog --- swagger_parser/CHANGELOG.md | 1 + swagger_parser/README.md | 5 ++++- swagger_parser/pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/swagger_parser/CHANGELOG.md b/swagger_parser/CHANGELOG.md index c632ef6a..5571d31e 100644 --- a/swagger_parser/CHANGELOG.md +++ b/swagger_parser/CHANGELOG.md @@ -1,6 +1,7 @@ ## 1.11.0 - Added unknown value to all enums to maintain backwards compatibility when adding new values on the backend - Add new config parameter `unknown_enum_value` (dart only) ([#106](https://github.com/Carapacik/swagger_parser/issues/106)) +- Add new config parameter `default_content_type` - Support String values with spaces for enums ([#127](https://github.com/Carapacik/swagger_parser/issues/127)) ## 1.10.6 diff --git a/swagger_parser/README.md b/swagger_parser/README.md index 86ea3908..446f7072 100644 --- a/swagger_parser/README.md +++ b/swagger_parser/README.md @@ -77,7 +77,10 @@ swagger_parser: # Optional (dart only). Set root client name root_client_name: RestClient - # Optional. Set API name for folder and export file (coming soon). + # Optional. Set default content-type for all requests + default_content_type: "application/json" + + # Optional. Set API name for folder and export file # If not specified, the file name is used. name: null diff --git a/swagger_parser/pubspec.yaml b/swagger_parser/pubspec.yaml index 6ad12c19..59133a1a 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.10.6 +version: 1.11.0 repository: https://github.com/Carapacik/swagger_parser/tree/main/swagger_parser homepage: https://omega-r.com topics: From 01d42ed178a6ec714fddd6015734afcf047bd248 Mon Sep 17 00:00:00 2001 From: Carapacik Date: Thu, 26 Oct 2023 17:55:39 +0300 Subject: [PATCH 6/7] Fix example --- swagger_parser/example/swagger_parser.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) mode change 100755 => 100644 swagger_parser/example/swagger_parser.yaml diff --git a/swagger_parser/example/swagger_parser.yaml b/swagger_parser/example/swagger_parser.yaml old mode 100755 new mode 100644 index f8aef017..32270f6c --- a/swagger_parser/example/swagger_parser.yaml +++ b/swagger_parser/example/swagger_parser.yaml @@ -28,6 +28,9 @@ swagger_parser: # Optional (dart only). Set root client name root_client_name: RestClient + # Optional. Set default content-type for all requests + default_content_type: "application/json" + # Optional. Set API name for folder and export file (coming soon). # If not specified, the file name is used. name: null @@ -58,9 +61,6 @@ swagger_parser: # Optional. Set 'true' to set enum prefix from parent component. enums_prefix: false - # Optional (dart only). Set 'true' to maintain backwards compatibility when adding new values on the backend. - unknown_enum_value: true - # Optional. Set 'false' to not put a comment at the beginning of the generated files. mark_files_as_generated: true From 900265e3941a761061b3fe6d34640310b9bef423 Mon Sep 17 00:00:00 2001 From: Carapacik Date: Thu, 26 Oct 2023 17:58:55 +0300 Subject: [PATCH 7/7] Add types --- .../src/generator/templates/dart_retrofit_client_template.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/swagger_parser/lib/src/generator/templates/dart_retrofit_client_template.dart b/swagger_parser/lib/src/generator/templates/dart_retrofit_client_template.dart index 8b98442e..f03db741 100644 --- a/swagger_parser/lib/src/generator/templates/dart_retrofit_client_template.dart +++ b/swagger_parser/lib/src/generator/templates/dart_retrofit_client_template.dart @@ -88,7 +88,7 @@ String _contentTypeHeader( return '@FormUrlEncoded()\n '; } if (request.contentType != defaultContentType) { - return "@Headers({'Content-Type': '${request.contentType}'})\n "; + return "@Headers({'Content-Type': '${request.contentType}'})\n "; } return ''; }