From 1429cdece5f0ec3ab297b9f9549b2774828f82c3 Mon Sep 17 00:00:00 2001 From: jld3103 Date: Tue, 24 Oct 2023 19:35:26 +0200 Subject: [PATCH 1/3] feat(dynamite): Support content in parameters Signed-off-by: jld3103 --- .../dynamite/lib/src/models/openapi.g.dart | 2 + .../lib/src/models/openapi/parameter.dart | 35 +++++++++++- .../lib/src/models/openapi/parameter.g.dart | 53 ++++++++++++++----- 3 files changed, 74 insertions(+), 16 deletions(-) diff --git a/packages/dynamite/dynamite/lib/src/models/openapi.g.dart b/packages/dynamite/dynamite/lib/src/models/openapi.g.dart index 7138f5f5..fa202117 100644 --- a/packages/dynamite/dynamite/lib/src/models/openapi.g.dart +++ b/packages/dynamite/dynamite/lib/src/models/openapi.g.dart @@ -63,6 +63,8 @@ Serializers _$serializers = (Serializers().toBuilder() const FullType(BuiltMap, [FullType(String), FullType(MediaType)]), () => MapBuilder()) ..addBuilderFactory( const FullType(BuiltMap, [FullType(String), FullType(MediaType)]), () => MapBuilder()) + ..addBuilderFactory( + const FullType(BuiltMap, [FullType(String), FullType(MediaType)]), () => MapBuilder()) ..addBuilderFactory( const FullType(BuiltMap, [FullType(String), FullType(Header)]), () => MapBuilder()) ..addBuilderFactory(const FullType(BuiltMap, [FullType(String), FullType(SecurityScheme)]), diff --git a/packages/dynamite/dynamite/lib/src/models/openapi/parameter.dart b/packages/dynamite/dynamite/lib/src/models/openapi/parameter.dart index 755d6443..35ff7490 100644 --- a/packages/dynamite/dynamite/lib/src/models/openapi/parameter.dart +++ b/packages/dynamite/dynamite/lib/src/models/openapi/parameter.dart @@ -4,6 +4,7 @@ import 'package:built_value/serializer.dart'; import 'package:dynamite/src/helpers/dart_helpers.dart'; import 'package:dynamite/src/helpers/docs.dart'; import 'package:dynamite/src/models/exceptions.dart'; +import 'package:dynamite/src/models/openapi/media_type.dart'; import 'package:dynamite/src/models/openapi/schema.dart'; part 'parameter.g.dart'; @@ -25,7 +26,33 @@ abstract class Parameter implements Built { bool get required; - Schema? get schema; + @Deprecated('Use [schema] instead which also automatically handles [content].') + @BuiltValueField(wireName: 'schema') + Schema? get $schema; + + BuiltMap? get content; + + Schema? get schema { + // ignore: deprecated_member_use_from_same_package + if ($schema != null) { + // ignore: deprecated_member_use_from_same_package + return $schema; + } + + if (content != null && content!.isNotEmpty) { + if (content!.length > 1) { + print('Can not work with multiple mime types right now. Using the first supported.'); + } + return Schema( + (final b) => b + ..type = SchemaType.string + ..contentMediaType = content!.entries.first.key + ..contentSchema = content!.entries.first.value.schema!.toBuilder(), + ); + } + + return null; + } @BuiltValueHook(finalizeBuilder: true) static void _defaults(final ParameterBuilder b) { @@ -34,9 +61,13 @@ abstract class Parameter implements Built { throw OpenAPISpecError('Path parameters must be required but ${b.name} is not.'); } - if (b.required! && b.schema.$default != null) { + if (b.required! && b._$schema != null && b.$schema.$default != null) { print('Required parameters should not specify default values.'); } + + if (b._$schema != null && b._content != null) { + throw OpenAPISpecError('Only one of schema or content must be set in parameter ${b.name}.'); + } } String get formattedDescription { diff --git a/packages/dynamite/dynamite/lib/src/models/openapi/parameter.g.dart b/packages/dynamite/dynamite/lib/src/models/openapi/parameter.g.dart index bf432d92..28c1a7b5 100644 --- a/packages/dynamite/dynamite/lib/src/models/openapi/parameter.g.dart +++ b/packages/dynamite/dynamite/lib/src/models/openapi/parameter.g.dart @@ -60,12 +60,19 @@ class _$ParameterSerializer implements StructuredSerializer { ..add('description') ..add(serializers.serialize(value, specifiedType: const FullType(String))); } - value = object.schema; + value = object.$schema; if (value != null) { result ..add('schema') ..add(serializers.serialize(value, specifiedType: const FullType(Schema))); } + value = object.content; + if (value != null) { + result + ..add('content') + ..add(serializers.serialize(value, + specifiedType: const FullType(BuiltMap, [FullType(String), FullType(MediaType)]))); + } return result; } @@ -93,7 +100,11 @@ class _$ParameterSerializer implements StructuredSerializer { result.required = serializers.deserialize(value, specifiedType: const FullType(bool))! as bool; break; case 'schema': - result.schema.replace(serializers.deserialize(value, specifiedType: const FullType(Schema))! as Schema); + result.$schema.replace(serializers.deserialize(value, specifiedType: const FullType(Schema))! as Schema); + break; + case 'content': + result.content.replace(serializers.deserialize(value, + specifiedType: const FullType(BuiltMap, [FullType(String), FullType(MediaType)]))!); break; } } @@ -128,11 +139,14 @@ class _$Parameter extends Parameter { @override final bool required; @override - final Schema? schema; + final Schema? $schema; + @override + final BuiltMap? content; factory _$Parameter([void Function(ParameterBuilder)? updates]) => (ParameterBuilder()..update(updates))._build(); - _$Parameter._({required this.name, required this.$in, this.description, required this.required, this.schema}) + _$Parameter._( + {required this.name, required this.$in, this.description, required this.required, this.$schema, this.content}) : super._() { BuiltValueNullFieldError.checkNotNull(name, r'Parameter', 'name'); BuiltValueNullFieldError.checkNotNull($in, r'Parameter', '\$in'); @@ -152,7 +166,8 @@ class _$Parameter extends Parameter { name == other.name && $in == other.$in && required == other.required && - schema == other.schema; + $schema == other.$schema && + content == other.content; } @override @@ -161,7 +176,8 @@ class _$Parameter extends Parameter { _$hash = $jc(_$hash, name.hashCode); _$hash = $jc(_$hash, $in.hashCode); _$hash = $jc(_$hash, required.hashCode); - _$hash = $jc(_$hash, schema.hashCode); + _$hash = $jc(_$hash, $schema.hashCode); + _$hash = $jc(_$hash, content.hashCode); _$hash = $jf(_$hash); return _$hash; } @@ -173,7 +189,8 @@ class _$Parameter extends Parameter { ..add('\$in', $in) ..add('description', description) ..add('required', required) - ..add('schema', schema)) + ..add('\$schema', $schema) + ..add('content', content)) .toString(); } } @@ -197,9 +214,13 @@ class ParameterBuilder implements Builder { bool? get required => _$this._required; set required(bool? required) => _$this._required = required; - SchemaBuilder? _schema; - SchemaBuilder get schema => _$this._schema ??= SchemaBuilder(); - set schema(SchemaBuilder? schema) => _$this._schema = schema; + SchemaBuilder? _$schema; + SchemaBuilder get $schema => _$this._$schema ??= SchemaBuilder(); + set $schema(SchemaBuilder? $schema) => _$this._$schema = $schema; + + MapBuilder? _content; + MapBuilder get content => _$this._content ??= MapBuilder(); + set content(MapBuilder? content) => _$this._content = content; ParameterBuilder(); @@ -210,7 +231,8 @@ class ParameterBuilder implements Builder { _$in = $v.$in; _description = $v.description; _required = $v.required; - _schema = $v.schema?.toBuilder(); + _$schema = $v.$schema?.toBuilder(); + _content = $v.content?.toBuilder(); _$v = null; } return this; @@ -240,12 +262,15 @@ class ParameterBuilder implements Builder { $in: BuiltValueNullFieldError.checkNotNull($in, r'Parameter', '\$in'), description: description, required: BuiltValueNullFieldError.checkNotNull(required, r'Parameter', 'required'), - schema: _schema?.build()); + $schema: _$schema?.build(), + content: _content?.build()); } catch (_) { late String _$failedField; try { - _$failedField = 'schema'; - _schema?.build(); + _$failedField = '\$schema'; + _$schema?.build(); + _$failedField = 'content'; + _content?.build(); } catch (e) { throw BuiltValueNestedFieldError(r'Parameter', _$failedField, e.toString()); } From 5b2cff8cd71848d84bc626a5bb57f948ba0369e7 Mon Sep 17 00:00:00 2001 From: jld3103 Date: Tue, 24 Oct 2023 20:23:24 +0200 Subject: [PATCH 2/3] fix(dynamite): Fix generating part directive and serializers conditionally Signed-off-by: jld3103 --- .../dynamite/dynamite/lib/src/builder/imports.dart | 9 ++++++--- .../dynamite/lib/src/builder/serializer.dart | 8 +------- .../dynamite/dynamite/lib/src/openapi_builder.dart | 14 ++++++++++---- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/dynamite/dynamite/lib/src/builder/imports.dart b/packages/dynamite/dynamite/lib/src/builder/imports.dart index c3a37d54..9e4f5e51 100644 --- a/packages/dynamite/dynamite/lib/src/builder/imports.dart +++ b/packages/dynamite/dynamite/lib/src/builder/imports.dart @@ -1,8 +1,9 @@ import 'package:build/build.dart'; import 'package:code_builder/code_builder.dart'; +import 'package:dynamite/src/builder/state.dart'; import 'package:path/path.dart' as p; -List generateImports(final AssetId outputId) => [ +List generateImports(final AssetId outputId, final State state) => [ const Code('// ignore_for_file: camel_case_types'), const Code('// ignore_for_file: discarded_futures'), const Code('// ignore_for_file: public_member_api_docs'), @@ -23,6 +24,8 @@ List generateImports(final AssetId outputId) => [ Directive.import('package:meta/meta.dart'), Directive.import('package:universal_io/io.dart'), const Code(''), - Directive.part(p.basename(outputId.changeExtension('.g.dart').path)), - const Code(''), + if (state.resolvedTypes.isNotEmpty) ...[ + Directive.part(p.basename(outputId.changeExtension('.g.dart').path)), + const Code(''), + ], ]; diff --git a/packages/dynamite/dynamite/lib/src/builder/serializer.dart b/packages/dynamite/dynamite/lib/src/builder/serializer.dart index 04674180..6118511c 100644 --- a/packages/dynamite/dynamite/lib/src/builder/serializer.dart +++ b/packages/dynamite/dynamite/lib/src/builder/serializer.dart @@ -1,9 +1,7 @@ import 'package:code_builder/code_builder.dart'; import 'package:dynamite/src/builder/state.dart'; -List buildSerializer(final State state) { - if (state.resolvedTypes.isNotEmpty) { - return [ +List buildSerializer(final State state) => [ const Code('// coverage:ignore-start'), const Code('final Serializers _serializers = (Serializers().toBuilder()'), ...state.resolvedTypes @@ -18,7 +16,3 @@ List buildSerializer(final State state) { ), const Code('// coverage:ignore-end'), ]; - } - - return []; -} diff --git a/packages/dynamite/dynamite/lib/src/openapi_builder.dart b/packages/dynamite/dynamite/lib/src/openapi_builder.dart index 1e46af25..5e42b4d7 100644 --- a/packages/dynamite/dynamite/lib/src/openapi_builder.dart +++ b/packages/dynamite/dynamite/lib/src/openapi_builder.dart @@ -51,9 +51,7 @@ class OpenAPIBuilder implements Builder { final state = State(); - final output = ListBuilder() - ..addAll(generateImports(outputId)) - ..addAll(generateClients(spec, state)); + final output = ListBuilder(); if (spec.components?.schemas != null) { for (final schema in spec.components!.schemas!.entries) { @@ -86,9 +84,17 @@ class OpenAPIBuilder implements Builder { } } + // Imports need to be generated after everything else so we know if we need the local part directive, + // but they need to be added to the beginning of the output. + final clients = generateClients(spec, state); + final serializer = buildSerializer(state); + final imports = generateImports(outputId, state); + output + ..addAll(imports) + ..addAll(clients) ..addAll(state.output) - ..addAll(buildSerializer(state)); + ..addAll(serializer); final patterns = [ RegExp( From 79b39f915357cd99e3865432a1bf902c5e9af9c5 Mon Sep 17 00:00:00 2001 From: jld3103 Date: Tue, 24 Oct 2023 20:25:30 +0200 Subject: [PATCH 3/3] feat(dynamite_end_to_end_test): Add parameters spec Signed-off-by: jld3103 --- .../lib/parameters.openapi.dart | 126 ++++++++++++++++++ .../lib/parameters.openapi.json | 51 +++++++ .../dynamite_end_to_end_test/pubspec.yaml | 8 +- 3 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 packages/dynamite/dynamite_end_to_end_test/lib/parameters.openapi.dart create mode 100644 packages/dynamite/dynamite_end_to_end_test/lib/parameters.openapi.json diff --git a/packages/dynamite/dynamite_end_to_end_test/lib/parameters.openapi.dart b/packages/dynamite/dynamite_end_to_end_test/lib/parameters.openapi.dart new file mode 100644 index 00000000..81b0baba --- /dev/null +++ b/packages/dynamite/dynamite_end_to_end_test/lib/parameters.openapi.dart @@ -0,0 +1,126 @@ +// ignore_for_file: camel_case_types +// ignore_for_file: discarded_futures +// ignore_for_file: public_member_api_docs +// ignore_for_file: unreachable_switch_case +import 'dart:typed_data'; + +import 'package:built_collection/built_collection.dart'; +import 'package:built_value/json_object.dart'; +import 'package:built_value/serializer.dart'; +import 'package:built_value/standard_json_plugin.dart'; +import 'package:dynamite_runtime/built_value.dart'; +import 'package:dynamite_runtime/http_client.dart'; +import 'package:dynamite_runtime/models.dart'; +import 'package:meta/meta.dart'; +import 'package:universal_io/io.dart'; + +class Client extends DynamiteClient { + Client( + super.baseURL, { + super.baseHeaders, + super.userAgent, + super.httpClient, + super.cookieJar, + }); + + Client.fromClient(final DynamiteClient client) + : super( + client.baseURL, + baseHeaders: client.baseHeaders, + httpClient: client.httpClient, + cookieJar: client.cookieJar, + authentications: client.authentications, + ); + + /// Returns a [Future] containing a [DynamiteResponse] with the status code, deserialized body and headers. + /// Throws a [DynamiteApiException] if the API call does not return an expected status code. + /// + /// Parameters: + /// * [contentString] + /// * [contentParameter] + /// + /// Status codes: + /// * 200 + /// + /// See: + /// * [$getRaw] for an experimental operation that returns a [DynamiteRawResponse] that can be serialized. + Future> $get({ + final ContentString>? contentString, + final ContentString>? contentParameter, + }) async { + final rawResponse = $getRaw( + contentString: contentString, + contentParameter: contentParameter, + ); + + return rawResponse.future; + } + + /// This method and the response it returns is experimental. The API might change without a major version bump. + /// + /// Returns a [Future] containing a [DynamiteRawResponse] with the raw [HttpClientResponse] and serialization helpers. + /// Throws a [DynamiteApiException] if the API call does not return an expected status code. + /// + /// Parameters: + /// * [contentString] + /// * [contentParameter] + /// + /// Status codes: + /// * 200 + /// + /// See: + /// * [$get] for an operation that returns a [DynamiteResponse] with a stable API. + @experimental + DynamiteRawResponse $getRaw({ + final ContentString>? contentString, + final ContentString>? contentParameter, + }) { + final queryParameters = {}; + final headers = { + 'Accept': 'application/json', + }; + Uint8List? body; + + if (contentString != null) { + queryParameters['content-string'] = _jsonSerializers.serialize( + contentString, + specifiedType: const FullType(ContentString, [ + FullType(BuiltMap, [FullType(String), FullType(JsonObject)]), + ]), + ); + } + if (contentParameter != null) { + queryParameters['content-parameter'] = _jsonSerializers.serialize( + contentParameter, + specifiedType: const FullType(ContentString, [ + FullType(BuiltMap, [FullType(String), FullType(JsonObject)]), + ]), + ); + } + const path = '/'; + final uri = Uri(path: path, queryParameters: queryParameters.isNotEmpty ? queryParameters : null); + + return DynamiteRawResponse( + response: executeRequest( + 'get', + uri, + headers, + body, + const {200}, + ), + bodyType: const FullType(JsonObject), + headersType: null, + serializers: _jsonSerializers, + ); + } +} + +// coverage:ignore-start +final Serializers _serializers = Serializers().toBuilder().build(); + +final Serializers _jsonSerializers = (_serializers.toBuilder() + ..add(DynamiteDoubleSerializer()) + ..addPlugin(StandardJsonPlugin()) + ..addPlugin(const ContentStringPlugin())) + .build(); +// coverage:ignore-end diff --git a/packages/dynamite/dynamite_end_to_end_test/lib/parameters.openapi.json b/packages/dynamite/dynamite_end_to_end_test/lib/parameters.openapi.json new file mode 100644 index 00000000..5b51e1cb --- /dev/null +++ b/packages/dynamite/dynamite_end_to_end_test/lib/parameters.openapi.json @@ -0,0 +1,51 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "parameters test", + "version": "0.0.1" + }, + "paths": { + "/": { + "get": { + "parameters": [ + { + "name": "content-string", + "in": "query", + "schema": { + "type": "string", + "contentMediaType": "application/json", + "contentSchema": { + "type": "object", + "additionalProperties": {} + } + } + }, + { + "name": "content-parameter", + "in": "query", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": {} + } + } + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + } + } + } + } + } + } + } + }, + "tags": [] +} diff --git a/packages/dynamite/dynamite_end_to_end_test/pubspec.yaml b/packages/dynamite/dynamite_end_to_end_test/pubspec.yaml index c4b317ec..05e044c0 100644 --- a/packages/dynamite/dynamite_end_to_end_test/pubspec.yaml +++ b/packages/dynamite/dynamite_end_to_end_test/pubspec.yaml @@ -4,14 +4,18 @@ description: Tests for dynamite. Not meant for publishing. version: 1.0.0 environment: - sdk: '>=3.1.1 <4.0.0' + sdk: '>=3.1.0 <4.0.0' dependencies: - built_value: ^8.6.2 + built_collection: ^5.0.0 + built_value: ^8.0.0 dynamite_runtime: git: url: https://github.com/nextcloud/neon path: packages/dynamite/dynamite_runtime + meta: ^1.0.0 + universal_io: ^2.0.0 + dev_dependencies: build_runner: ^2.4.6 build_verify: ^3.1.0