From d5af8790f441fa0908bb157f16c298be11af85a6 Mon Sep 17 00:00:00 2001 From: jld3103 Date: Sat, 19 Nov 2022 21:40:16 +0100 Subject: [PATCH 1/2] dynamite: Cleanup OpenAPI models --- .../dynamite/lib/src/models/components.dart | 4 +-- packages/dynamite/lib/src/models/header.dart | 22 ++++++++++++ .../dynamite/lib/src/models/header.g.dart | 34 +++++++++++++++++++ packages/dynamite/lib/src/models/info.dart | 6 ++-- packages/dynamite/lib/src/models/info.g.dart | 6 ++-- packages/dynamite/lib/src/models/license.dart | 4 +-- .../dynamite/lib/src/models/media_type.dart | 2 +- .../dynamite/lib/src/models/open_api.dart | 10 +++--- .../dynamite/lib/src/models/operation.dart | 10 +++--- .../dynamite/lib/src/models/parameter.dart | 6 ++-- .../dynamite/lib/src/models/path_item.dart | 20 +++++------ .../dynamite/lib/src/models/request_body.dart | 6 ++-- .../dynamite/lib/src/models/response.dart | 6 +++- .../dynamite/lib/src/models/response.g.dart | 6 +++- packages/dynamite/lib/src/models/schema.dart | 26 +++++++------- .../lib/src/models/security_scheme.dart | 4 +-- packages/dynamite/lib/src/models/server.dart | 2 +- .../lib/src/models/server_variable.dart | 4 +-- 18 files changed, 121 insertions(+), 57 deletions(-) create mode 100644 packages/dynamite/lib/src/models/header.dart create mode 100644 packages/dynamite/lib/src/models/header.g.dart diff --git a/packages/dynamite/lib/src/models/components.dart b/packages/dynamite/lib/src/models/components.dart index 2796da62..e995236a 100644 --- a/packages/dynamite/lib/src/models/components.dart +++ b/packages/dynamite/lib/src/models/components.dart @@ -7,8 +7,8 @@ part 'components.g.dart'; @JsonSerializable() class Components { Components({ - required this.securitySchemes, - required this.schemas, + this.securitySchemes, + this.schemas, }); factory Components.fromJson(final Map json) => _$ComponentsFromJson(json); diff --git a/packages/dynamite/lib/src/models/header.dart b/packages/dynamite/lib/src/models/header.dart new file mode 100644 index 00000000..9bd47efa --- /dev/null +++ b/packages/dynamite/lib/src/models/header.dart @@ -0,0 +1,22 @@ +import 'package:dynamite/src/models/schema.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'header.g.dart'; + +@JsonSerializable() +class Header { + Header({ + this.description, + this.required, + this.schema, + }); + + factory Header.fromJson(final Map json) => _$HeaderFromJson(json); + Map toJson() => _$HeaderToJson(this); + + final String? description; + + final bool? required; + + final Schema? schema; +} diff --git a/packages/dynamite/lib/src/models/header.g.dart b/packages/dynamite/lib/src/models/header.g.dart new file mode 100644 index 00000000..48c2212b --- /dev/null +++ b/packages/dynamite/lib/src/models/header.g.dart @@ -0,0 +1,34 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'header.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Header _$HeaderFromJson(Map json) { + $checkKeys( + json, + allowedKeys: const ['description', 'required', 'schema'], + ); + return Header( + description: json['description'] as String?, + required: json['required'] as bool?, + schema: json['schema'] == null ? null : Schema.fromJson(json['schema'] as Map), + ); +} + +Map _$HeaderToJson(Header instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('description', instance.description); + writeNotNull('required', instance.required); + writeNotNull('schema', instance.schema?.toJson()); + return val; +} diff --git a/packages/dynamite/lib/src/models/info.dart b/packages/dynamite/lib/src/models/info.dart index ff727f2b..59d2f97a 100644 --- a/packages/dynamite/lib/src/models/info.dart +++ b/packages/dynamite/lib/src/models/info.dart @@ -8,8 +8,8 @@ class Info { Info({ required this.title, required this.version, - required this.description, required this.license, + this.description, }); factory Info.fromJson(final Map json) => _$InfoFromJson(json); @@ -19,7 +19,7 @@ class Info { final String version; - final String? description; - final License license; + + final String? description; } diff --git a/packages/dynamite/lib/src/models/info.g.dart b/packages/dynamite/lib/src/models/info.g.dart index 84e3239b..e25d100e 100644 --- a/packages/dynamite/lib/src/models/info.g.dart +++ b/packages/dynamite/lib/src/models/info.g.dart @@ -9,13 +9,13 @@ part of 'info.dart'; Info _$InfoFromJson(Map json) { $checkKeys( json, - allowedKeys: const ['title', 'version', 'description', 'license'], + allowedKeys: const ['title', 'version', 'license', 'description'], ); return Info( title: json['title'] as String, version: json['version'] as String, - description: json['description'] as String?, license: License.fromJson(json['license'] as Map), + description: json['description'] as String?, ); } @@ -23,6 +23,7 @@ Map _$InfoToJson(Info instance) { final val = { 'title': instance.title, 'version': instance.version, + 'license': instance.license.toJson(), }; void writeNotNull(String key, dynamic value) { @@ -32,6 +33,5 @@ Map _$InfoToJson(Info instance) { } writeNotNull('description', instance.description); - val['license'] = instance.license.toJson(); return val; } diff --git a/packages/dynamite/lib/src/models/license.dart b/packages/dynamite/lib/src/models/license.dart index 00dbb0cd..3bf19718 100644 --- a/packages/dynamite/lib/src/models/license.dart +++ b/packages/dynamite/lib/src/models/license.dart @@ -6,8 +6,8 @@ part 'license.g.dart'; class License { License({ required this.name, - required this.identifier, - required this.url, + this.identifier, + this.url, }); factory License.fromJson(final Map json) => _$LicenseFromJson(json); diff --git a/packages/dynamite/lib/src/models/media_type.dart b/packages/dynamite/lib/src/models/media_type.dart index 798ad5d9..4172cbae 100644 --- a/packages/dynamite/lib/src/models/media_type.dart +++ b/packages/dynamite/lib/src/models/media_type.dart @@ -6,7 +6,7 @@ part 'media_type.g.dart'; @JsonSerializable() class MediaType { MediaType({ - required this.schema, + this.schema, }); factory MediaType.fromJson(final Map json) => _$MediaTypeFromJson(json); diff --git a/packages/dynamite/lib/src/models/open_api.dart b/packages/dynamite/lib/src/models/open_api.dart index 08c51755..45101099 100644 --- a/packages/dynamite/lib/src/models/open_api.dart +++ b/packages/dynamite/lib/src/models/open_api.dart @@ -14,11 +14,11 @@ class OpenAPI { OpenAPI({ required this.version, required this.info, - required this.servers, - required this.security, - required this.tags, - required this.components, - required this.paths, + this.servers, + this.security, + this.tags, + this.components, + this.paths, }); factory OpenAPI.fromJson(final Map json) => _$OpenAPIFromJson(json); diff --git a/packages/dynamite/lib/src/models/operation.dart b/packages/dynamite/lib/src/models/operation.dart index d1590a3d..54b9d696 100644 --- a/packages/dynamite/lib/src/models/operation.dart +++ b/packages/dynamite/lib/src/models/operation.dart @@ -9,11 +9,11 @@ part 'operation.g.dart'; @JsonSerializable() class Operation { Operation({ - required this.operationId, - required this.tags, - required this.parameters, - required this.requestBody, - required this.responses, + this.operationId, + this.tags, + this.parameters, + this.requestBody, + this.responses, }); factory Operation.fromJson(final Map json) => _$OperationFromJson(json); diff --git a/packages/dynamite/lib/src/models/parameter.dart b/packages/dynamite/lib/src/models/parameter.dart index a3d309df..86dc7340 100644 --- a/packages/dynamite/lib/src/models/parameter.dart +++ b/packages/dynamite/lib/src/models/parameter.dart @@ -8,9 +8,9 @@ class Parameter { Parameter({ required this.name, required this.in_, - required this.description, - required this.required, - required this.schema, + this.description, + this.required, + this.schema, }); factory Parameter.fromJson(final Map json) => _$ParameterFromJson(json); diff --git a/packages/dynamite/lib/src/models/path_item.dart b/packages/dynamite/lib/src/models/path_item.dart index 8684f923..5d75e819 100644 --- a/packages/dynamite/lib/src/models/path_item.dart +++ b/packages/dynamite/lib/src/models/path_item.dart @@ -7,16 +7,16 @@ part 'path_item.g.dart'; @JsonSerializable() class PathItem { PathItem({ - required this.description, - required this.parameters, - required this.get, - required this.put, - required this.post, - required this.delete, - required this.options, - required this.head, - required this.patch, - required this.trace, + this.description, + this.parameters, + this.get, + this.put, + this.post, + this.delete, + this.options, + this.head, + this.patch, + this.trace, }); factory PathItem.fromJson(final Map json) => _$PathItemFromJson(json); diff --git a/packages/dynamite/lib/src/models/request_body.dart b/packages/dynamite/lib/src/models/request_body.dart index 3ed03da9..1fbe60eb 100644 --- a/packages/dynamite/lib/src/models/request_body.dart +++ b/packages/dynamite/lib/src/models/request_body.dart @@ -6,9 +6,9 @@ part 'request_body.g.dart'; @JsonSerializable() class RequestBody { RequestBody({ - required this.description, - required this.content, - required this.required, + this.description, + this.content, + this.required, }); factory RequestBody.fromJson(final Map json) => _$RequestBodyFromJson(json); diff --git a/packages/dynamite/lib/src/models/response.dart b/packages/dynamite/lib/src/models/response.dart index 9a917517..693e35c9 100644 --- a/packages/dynamite/lib/src/models/response.dart +++ b/packages/dynamite/lib/src/models/response.dart @@ -1,3 +1,4 @@ +import 'package:dynamite/src/models/header.dart'; import 'package:dynamite/src/models/media_type.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -7,7 +8,8 @@ part 'response.g.dart'; class Response { Response({ required this.description, - required this.content, + this.content, + this.headers, }); factory Response.fromJson(final Map json) => _$ResponseFromJson(json); @@ -16,4 +18,6 @@ class Response { final String description; final Map? content; + + final Map? headers; } diff --git a/packages/dynamite/lib/src/models/response.g.dart b/packages/dynamite/lib/src/models/response.g.dart index 47ef4691..10b887ef 100644 --- a/packages/dynamite/lib/src/models/response.g.dart +++ b/packages/dynamite/lib/src/models/response.g.dart @@ -9,13 +9,16 @@ part of 'response.dart'; Response _$ResponseFromJson(Map json) { $checkKeys( json, - allowedKeys: const ['description', 'content'], + allowedKeys: const ['description', 'content', 'headers'], ); return Response( description: json['description'] as String, content: (json['content'] as Map?)?.map( (k, e) => MapEntry(k, MediaType.fromJson(e as Map)), ), + headers: (json['headers'] as Map?)?.map( + (k, e) => MapEntry(k, Header.fromJson(e as Map)), + ), ); } @@ -31,5 +34,6 @@ Map _$ResponseToJson(Response instance) { } writeNotNull('content', instance.content?.map((k, e) => MapEntry(k, e.toJson()))); + writeNotNull('headers', instance.headers?.map((k, e) => MapEntry(k, e.toJson()))); return val; } diff --git a/packages/dynamite/lib/src/models/schema.dart b/packages/dynamite/lib/src/models/schema.dart index 0b3e48fd..c6185105 100644 --- a/packages/dynamite/lib/src/models/schema.dart +++ b/packages/dynamite/lib/src/models/schema.dart @@ -5,19 +5,19 @@ part 'schema.g.dart'; @JsonSerializable() class Schema { Schema({ - required this.ref, - required this.oneOf, - required this.anyOf, - required this.allOf, - required this.description, - required this.deprecated, - required this.type, - required this.format, - required this.default_, - required this.enum_, - required this.properties, - required this.required, - required this.items, + this.ref, + this.oneOf, + this.anyOf, + this.allOf, + this.description, + this.deprecated, + this.type, + this.format, + this.default_, + this.enum_, + this.properties, + this.required, + this.items, }); factory Schema.fromJson(final Map json) => _$SchemaFromJson(json); diff --git a/packages/dynamite/lib/src/models/security_scheme.dart b/packages/dynamite/lib/src/models/security_scheme.dart index 0cc60176..df8b4417 100644 --- a/packages/dynamite/lib/src/models/security_scheme.dart +++ b/packages/dynamite/lib/src/models/security_scheme.dart @@ -6,8 +6,8 @@ part 'security_scheme.g.dart'; class SecurityScheme { SecurityScheme({ required this.type, - required this.description, - required this.scheme, + this.description, + this.scheme, }); factory SecurityScheme.fromJson(final Map json) => _$SecuritySchemeFromJson(json); diff --git a/packages/dynamite/lib/src/models/server.dart b/packages/dynamite/lib/src/models/server.dart index 5bd129db..7feeac49 100644 --- a/packages/dynamite/lib/src/models/server.dart +++ b/packages/dynamite/lib/src/models/server.dart @@ -7,7 +7,7 @@ part 'server.g.dart'; class Server { Server({ required this.url, - required this.variables, + this.variables, }); factory Server.fromJson(final Map json) => _$ServerFromJson(json); diff --git a/packages/dynamite/lib/src/models/server_variable.dart b/packages/dynamite/lib/src/models/server_variable.dart index f41240aa..52788812 100644 --- a/packages/dynamite/lib/src/models/server_variable.dart +++ b/packages/dynamite/lib/src/models/server_variable.dart @@ -6,8 +6,8 @@ part 'server_variable.g.dart'; class ServerVariable { ServerVariable({ required this.default_, - required this.enum_, - required this.description, + this.enum_, + this.description, }); factory ServerVariable.fromJson(final Map json) => _$ServerVariableFromJson(json); From 90eab74b030542ee1b23a4a620a61cb63be0abe6 Mon Sep 17 00:00:00 2001 From: jld3103 Date: Sat, 19 Nov 2022 21:45:27 +0100 Subject: [PATCH 2/2] dynamite: Support response headers --- .../dynamite/lib/src/openapi_builder.dart | 175 +++++++++++++++--- 1 file changed, 154 insertions(+), 21 deletions(-) diff --git a/packages/dynamite/lib/src/openapi_builder.dart b/packages/dynamite/lib/src/openapi_builder.dart index da9df820..0ba2e9a9 100644 --- a/packages/dynamite/lib/src/openapi_builder.dart +++ b/packages/dynamite/lib/src/openapi_builder.dart @@ -76,6 +76,54 @@ class OpenAPIBuilder implements Builder { Class( (final b) => b ..name = 'Response' + ..types.addAll([ + refer('T'), + refer('U'), + ]) + ..fields.addAll([ + Field( + (final b) => b + ..name = 'data' + ..type = refer('T') + ..modifier = FieldModifier.final$, + ), + Field( + (final b) => b + ..name = 'headers' + ..type = refer('U') + ..modifier = FieldModifier.final$, + ), + ]) + ..constructors.add( + Constructor( + (final b) => b + ..requiredParameters.addAll( + ['data', 'headers'].map( + (final name) => Parameter( + (final b) => b + ..name = name + ..toThis = true, + ), + ), + ), + ), + ) + ..methods.add( + Method( + (final b) => b + ..name = 'toString' + ..returns = refer('String') + ..annotations.add(refer('override')) + ..lambda = true + ..body = const Code( + r"'Response(data: $data, headers: $headers)'", + ), + ), + ), + ).accept(emitter).toString(), + Class( + (final b) => b + ..name = '_Response' ..fields.addAll([ Field( (final b) => b @@ -117,15 +165,16 @@ class OpenAPIBuilder implements Builder { ..returns = refer('String') ..annotations.add(refer('override')) ..lambda = true - ..body = - const Code(r"'Response(statusCode: $statusCode, headers: $headers, body: ${utf8.decode(body)})'"), + ..body = const Code( + r"'_Response(statusCode: $statusCode, headers: $headers, body: ${utf8.decode(body)})'", + ), ), ), ).accept(emitter).toString(), Class( (final b) => b ..name = 'ApiException' - ..extend = refer('Response') + ..extend = refer('_Response') ..implements.add(refer('Exception')) ..constructors.addAll( [ @@ -150,7 +199,7 @@ class OpenAPIBuilder implements Builder { Parameter( (final b) => b ..name = 'response' - ..type = refer('Response'), + ..type = refer('_Response'), ), ) ..body = const Code('ApiException(response.statusCode, response.headers, response.body,)'), @@ -258,6 +307,7 @@ class OpenAPIBuilder implements Builder { final Schema schema, { final bool ignoreEnum = false, final Map? extraJsonSerializableValues, + final Map>? extraJsonKeyValues, }) { TypeResolveResult? result; if (schema.ref != null) { @@ -557,6 +607,15 @@ class OpenAPIBuilder implements Builder { [], { 'name': refer("'$propertyName'"), + if (extraJsonKeyValues != null) ...{ + for (final p in extraJsonKeyValues.keys) ...{ + if (p == propertyName) ...{ + for (final key in extraJsonKeyValues[p]!.keys) ...{ + key: refer(extraJsonKeyValues[p]![key]!), + }, + }, + }, + }, }, ), ); @@ -699,14 +758,6 @@ class OpenAPIBuilder implements Builder { paths[path] = PathItem( description: pathItem.description, parameters: pathItem.parameters, - get: null, - put: null, - post: null, - delete: null, - options: null, - head: null, - patch: null, - trace: null, ); } paths[path] = paths[path]!.copyWithOperations({method: operation}); @@ -829,7 +880,7 @@ class OpenAPIBuilder implements Builder { Method( (final b) => b ..name = 'doRequest' - ..returns = refer('Future') + ..returns = refer('Future<_Response>') ..modifier = MethodModifier.async ..requiredParameters.addAll([ Parameter( @@ -859,6 +910,7 @@ class OpenAPIBuilder implements Builder { for (final header in {...baseHeaders, ...headers}.entries) { request.headers.add(header.key, header.value); } +<<<<<<< Updated upstream if (body != null) { request.add(body.toList()); } @@ -875,6 +927,11 @@ class OpenAPIBuilder implements Builder { responseHeaders[name] = values.last; }); return Response( +======= + + final response = await http.Response.fromStream(await request.send()); + return _Response( +>>>>>>> Stashed changes response.statusCode, responseHeaders, await response.bodyBytes, @@ -913,6 +970,7 @@ class OpenAPIBuilder implements Builder { Method( (final b) { final operation = paths[path]!.operations[httpMethod]!; + final operationId = operation.operationId ?? _toDartName('$httpMethod-$path'); final pathParameters = [ if (paths[path]!.parameters != null) ...paths[path]!.parameters!, ]; @@ -920,7 +978,7 @@ class OpenAPIBuilder implements Builder { ...pathParameters, if (operation.parameters != null) ...operation.parameters!, ]; - final methodName = _toDartName(operation.operationId ?? _toDartName('$httpMethod-$path')); + final methodName = _toDartName(operationId); b ..name = methodName ..modifier = MethodModifier.async; @@ -986,7 +1044,7 @@ class OpenAPIBuilder implements Builder { break; case 'query': code.write( - "queryParameters['${parameter.name}'] = ${_toDartName(parameter.name)}$enumValueGetter.toString();", + "queryParameters['${parameter.name}${result.isList ? '[]' : ''}'] = ${_toDartName(parameter.name)}$enumValueGetter${result.isList ? '.map((final x) => x.toString()).toList()' : '.toString()'};", ); break; case 'header': @@ -1064,6 +1122,51 @@ class OpenAPIBuilder implements Builder { for (final statusCode in operation.responses!.keys) { final response = operation.responses![statusCode]!; code.write('if (response.statusCode == $statusCode) {'); + + String? headersType; + String? headersValue; + if (response.headers != null) { + final identifier = + '${tag != null ? _toDartName(tag.name, uppercaseFirstCharacter: true) : null}${_toDartName(operationId, uppercaseFirstCharacter: true)}Headers'; + final headerParseFunctions = {}; + for (final headerName in response.headers!.keys) { + final functionIdentifier = '_${_toDartName('${identifier}Parse$headerName')}'; + headerParseFunctions[headerName] = functionIdentifier; + final result = resolveType( + identifier, + response.headers![headerName]!.schema!, + ); + output.add( + '${result.typeName} $functionIdentifier(final Map data, final String key) => ${_parseFromStringFunctionForType('data[key]', result)};', + ); + } + final result = resolveType( + identifier, + Schema( + type: 'object', + properties: { + for (final headerName in response.headers!.keys) ...{ + headerName.toLowerCase(): response.headers![headerName]!.schema!, + }, + }, + ), + extraJsonSerializableValues: { + 'disallowUnrecognizedKeys': 'false', + }, + extraJsonKeyValues: { + for (final headerName in response.headers!.keys) ...{ + headerName.toLowerCase(): { + 'readValue': headerParseFunctions[headerName]!, + }, + }, + }, + ); + headersType = result.typeName; + headersValue = _deserializeFunctionForType('response.headers', result); + } + + String? dataType; + String? dataValue; if (response.content != null) { if (response.content!.length > 1) { throw Exception('Can not work with multiple mime types right now'); @@ -1077,26 +1180,38 @@ class OpenAPIBuilder implements Builder { ); switch (mimeType) { case 'application/json': - b.returns = refer('Future<${result.typeName}>'); - code.write('return ${_deserializeFunctionForType( + dataType = result.typeName; + dataValue = _deserializeFunctionForType( result.isBaseType ? 'utf8.decode(response.body)' : 'json.decode(utf8.decode(response.body))', result, - )};'); + ); break; case 'image/png': - b.returns = refer('Future'); - code.write('return response.body;'); + dataType = 'Uint8List'; + dataValue = 'response.body'; break; default: throw Exception('Can not parse mime type "$mimeType"'); } } + } + + if (headersType != null && dataType != null) { + b.returns = refer('Future>'); + code.write('return Response<$dataType, $headersType>($dataValue, $headersValue,);'); + } else if (headersType != null) { + b.returns = refer('Future<$headersType>'); + code.write('return $headersValue;'); + } else if (dataType != null) { + b.returns = refer('Future<$dataType>'); + code.write('return $dataValue;'); } else { - code.write('return;'); b.returns = refer('Future'); + code.write('return;'); } + code.write('}'); } code.write('throw ApiException.fromResponse(response); // coverage:ignore-line\n'); @@ -1373,6 +1488,24 @@ String _deserializeFunctionForType(final String object, final TypeResolveResult } } +String _parseFromStringFunctionForType(final String object, final TypeResolveResult result) { + final o = '$object as String'; + if (result.isBaseType) { + switch (result.typeName) { + case 'String': + return o; + case 'int': + return 'int.parse($o)'; + default: + throw Exception('Can not parse "${result.typeName}" from String'); + } + } else if (result.isEnum) { + return _parseFromStringFunctionForType(o, result.subType!); + } else { + return 'json.decode($o)'; + } +} + bool _isParameterNullable(final bool? required, final dynamic default_) => !(required ?? false) && default_ == null; String _valueToEscapedValue(final String type, final dynamic value) => type == 'String' ? "'$value'" : value.toString();