Browse Source

Merge pull request #1020 from nextcloud/feature/dynamite/parameter-content

feat(dynamite): Support content in parameters
pull/1021/head
Kate 1 year ago committed by GitHub
parent
commit
50f14665d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      packages/dynamite/dynamite/lib/src/builder/imports.dart
  2. 8
      packages/dynamite/dynamite/lib/src/builder/serializer.dart
  3. 2
      packages/dynamite/dynamite/lib/src/models/openapi.g.dart
  4. 35
      packages/dynamite/dynamite/lib/src/models/openapi/parameter.dart
  5. 53
      packages/dynamite/dynamite/lib/src/models/openapi/parameter.g.dart
  6. 14
      packages/dynamite/dynamite/lib/src/openapi_builder.dart
  7. 126
      packages/dynamite/dynamite_end_to_end_test/lib/parameters.openapi.dart
  8. 51
      packages/dynamite/dynamite_end_to_end_test/lib/parameters.openapi.json
  9. 8
      packages/dynamite/dynamite_end_to_end_test/pubspec.yaml

5
packages/dynamite/dynamite/lib/src/builder/imports.dart

@ -1,8 +1,9 @@
import 'package:build/build.dart'; import 'package:build/build.dart';
import 'package:code_builder/code_builder.dart'; import 'package:code_builder/code_builder.dart';
import 'package:dynamite/src/builder/state.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
List<Spec> generateImports(final AssetId outputId) => [ List<Spec> generateImports(final AssetId outputId, final State state) => [
const Code('// ignore_for_file: camel_case_types'), const Code('// ignore_for_file: camel_case_types'),
const Code('// ignore_for_file: discarded_futures'), const Code('// ignore_for_file: discarded_futures'),
const Code('// ignore_for_file: public_member_api_docs'), const Code('// ignore_for_file: public_member_api_docs'),
@ -23,6 +24,8 @@ List<Spec> generateImports(final AssetId outputId) => [
Directive.import('package:meta/meta.dart'), Directive.import('package:meta/meta.dart'),
Directive.import('package:universal_io/io.dart'), Directive.import('package:universal_io/io.dart'),
const Code(''), const Code(''),
if (state.resolvedTypes.isNotEmpty) ...[
Directive.part(p.basename(outputId.changeExtension('.g.dart').path)), Directive.part(p.basename(outputId.changeExtension('.g.dart').path)),
const Code(''), const Code(''),
],
]; ];

8
packages/dynamite/dynamite/lib/src/builder/serializer.dart

@ -1,9 +1,7 @@
import 'package:code_builder/code_builder.dart'; import 'package:code_builder/code_builder.dart';
import 'package:dynamite/src/builder/state.dart'; import 'package:dynamite/src/builder/state.dart';
List<Spec> buildSerializer(final State state) { List<Spec> buildSerializer(final State state) => [
if (state.resolvedTypes.isNotEmpty) {
return [
const Code('// coverage:ignore-start'), const Code('// coverage:ignore-start'),
const Code('final Serializers _serializers = (Serializers().toBuilder()'), const Code('final Serializers _serializers = (Serializers().toBuilder()'),
...state.resolvedTypes ...state.resolvedTypes
@ -18,7 +16,3 @@ List<Spec> buildSerializer(final State state) {
), ),
const Code('// coverage:ignore-end'), const Code('// coverage:ignore-end'),
]; ];
}
return [];
}

2
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<String, MediaType>()) const FullType(BuiltMap, [FullType(String), FullType(MediaType)]), () => MapBuilder<String, MediaType>())
..addBuilderFactory( ..addBuilderFactory(
const FullType(BuiltMap, [FullType(String), FullType(MediaType)]), () => MapBuilder<String, MediaType>()) const FullType(BuiltMap, [FullType(String), FullType(MediaType)]), () => MapBuilder<String, MediaType>())
..addBuilderFactory(
const FullType(BuiltMap, [FullType(String), FullType(MediaType)]), () => MapBuilder<String, MediaType>())
..addBuilderFactory( ..addBuilderFactory(
const FullType(BuiltMap, [FullType(String), FullType(Header)]), () => MapBuilder<String, Header>()) const FullType(BuiltMap, [FullType(String), FullType(Header)]), () => MapBuilder<String, Header>())
..addBuilderFactory(const FullType(BuiltMap, [FullType(String), FullType(SecurityScheme)]), ..addBuilderFactory(const FullType(BuiltMap, [FullType(String), FullType(SecurityScheme)]),

35
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/dart_helpers.dart';
import 'package:dynamite/src/helpers/docs.dart'; import 'package:dynamite/src/helpers/docs.dart';
import 'package:dynamite/src/models/exceptions.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'; import 'package:dynamite/src/models/openapi/schema.dart';
part 'parameter.g.dart'; part 'parameter.g.dart';
@ -25,7 +26,33 @@ abstract class Parameter implements Built<Parameter, ParameterBuilder> {
bool get required; bool get required;
Schema? get schema; @Deprecated('Use [schema] instead which also automatically handles [content].')
@BuiltValueField(wireName: 'schema')
Schema? get $schema;
BuiltMap<String, MediaType>? 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) @BuiltValueHook(finalizeBuilder: true)
static void _defaults(final ParameterBuilder b) { static void _defaults(final ParameterBuilder b) {
@ -34,9 +61,13 @@ abstract class Parameter implements Built<Parameter, ParameterBuilder> {
throw OpenAPISpecError('Path parameters must be required but ${b.name} is not.'); 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.'); 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 { String get formattedDescription {

53
packages/dynamite/dynamite/lib/src/models/openapi/parameter.g.dart

@ -60,12 +60,19 @@ class _$ParameterSerializer implements StructuredSerializer<Parameter> {
..add('description') ..add('description')
..add(serializers.serialize(value, specifiedType: const FullType(String))); ..add(serializers.serialize(value, specifiedType: const FullType(String)));
} }
value = object.schema; value = object.$schema;
if (value != null) { if (value != null) {
result result
..add('schema') ..add('schema')
..add(serializers.serialize(value, specifiedType: const FullType(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; return result;
} }
@ -93,7 +100,11 @@ class _$ParameterSerializer implements StructuredSerializer<Parameter> {
result.required = serializers.deserialize(value, specifiedType: const FullType(bool))! as bool; result.required = serializers.deserialize(value, specifiedType: const FullType(bool))! as bool;
break; break;
case 'schema': 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; break;
} }
} }
@ -128,11 +139,14 @@ class _$Parameter extends Parameter {
@override @override
final bool required; final bool required;
@override @override
final Schema? schema; final Schema? $schema;
@override
final BuiltMap<String, MediaType>? content;
factory _$Parameter([void Function(ParameterBuilder)? updates]) => (ParameterBuilder()..update(updates))._build(); 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._() { : super._() {
BuiltValueNullFieldError.checkNotNull(name, r'Parameter', 'name'); BuiltValueNullFieldError.checkNotNull(name, r'Parameter', 'name');
BuiltValueNullFieldError.checkNotNull($in, r'Parameter', '\$in'); BuiltValueNullFieldError.checkNotNull($in, r'Parameter', '\$in');
@ -152,7 +166,8 @@ class _$Parameter extends Parameter {
name == other.name && name == other.name &&
$in == other.$in && $in == other.$in &&
required == other.required && required == other.required &&
schema == other.schema; $schema == other.$schema &&
content == other.content;
} }
@override @override
@ -161,7 +176,8 @@ class _$Parameter extends Parameter {
_$hash = $jc(_$hash, name.hashCode); _$hash = $jc(_$hash, name.hashCode);
_$hash = $jc(_$hash, $in.hashCode); _$hash = $jc(_$hash, $in.hashCode);
_$hash = $jc(_$hash, required.hashCode); _$hash = $jc(_$hash, required.hashCode);
_$hash = $jc(_$hash, schema.hashCode); _$hash = $jc(_$hash, $schema.hashCode);
_$hash = $jc(_$hash, content.hashCode);
_$hash = $jf(_$hash); _$hash = $jf(_$hash);
return _$hash; return _$hash;
} }
@ -173,7 +189,8 @@ class _$Parameter extends Parameter {
..add('\$in', $in) ..add('\$in', $in)
..add('description', description) ..add('description', description)
..add('required', required) ..add('required', required)
..add('schema', schema)) ..add('\$schema', $schema)
..add('content', content))
.toString(); .toString();
} }
} }
@ -197,9 +214,13 @@ class ParameterBuilder implements Builder<Parameter, ParameterBuilder> {
bool? get required => _$this._required; bool? get required => _$this._required;
set required(bool? required) => _$this._required = required; set required(bool? required) => _$this._required = required;
SchemaBuilder? _schema; SchemaBuilder? _$schema;
SchemaBuilder get schema => _$this._schema ??= SchemaBuilder(); SchemaBuilder get $schema => _$this._$schema ??= SchemaBuilder();
set schema(SchemaBuilder? schema) => _$this._schema = schema; set $schema(SchemaBuilder? $schema) => _$this._$schema = $schema;
MapBuilder<String, MediaType>? _content;
MapBuilder<String, MediaType> get content => _$this._content ??= MapBuilder<String, MediaType>();
set content(MapBuilder<String, MediaType>? content) => _$this._content = content;
ParameterBuilder(); ParameterBuilder();
@ -210,7 +231,8 @@ class ParameterBuilder implements Builder<Parameter, ParameterBuilder> {
_$in = $v.$in; _$in = $v.$in;
_description = $v.description; _description = $v.description;
_required = $v.required; _required = $v.required;
_schema = $v.schema?.toBuilder(); _$schema = $v.$schema?.toBuilder();
_content = $v.content?.toBuilder();
_$v = null; _$v = null;
} }
return this; return this;
@ -240,12 +262,15 @@ class ParameterBuilder implements Builder<Parameter, ParameterBuilder> {
$in: BuiltValueNullFieldError.checkNotNull($in, r'Parameter', '\$in'), $in: BuiltValueNullFieldError.checkNotNull($in, r'Parameter', '\$in'),
description: description, description: description,
required: BuiltValueNullFieldError.checkNotNull(required, r'Parameter', 'required'), required: BuiltValueNullFieldError.checkNotNull(required, r'Parameter', 'required'),
schema: _schema?.build()); $schema: _$schema?.build(),
content: _content?.build());
} catch (_) { } catch (_) {
late String _$failedField; late String _$failedField;
try { try {
_$failedField = 'schema'; _$failedField = '\$schema';
_schema?.build(); _$schema?.build();
_$failedField = 'content';
_content?.build();
} catch (e) { } catch (e) {
throw BuiltValueNestedFieldError(r'Parameter', _$failedField, e.toString()); throw BuiltValueNestedFieldError(r'Parameter', _$failedField, e.toString());
} }

14
packages/dynamite/dynamite/lib/src/openapi_builder.dart

@ -51,9 +51,7 @@ class OpenAPIBuilder implements Builder {
final state = State(); final state = State();
final output = ListBuilder<Spec>() final output = ListBuilder<Spec>();
..addAll(generateImports(outputId))
..addAll(generateClients(spec, state));
if (spec.components?.schemas != null) { if (spec.components?.schemas != null) {
for (final schema in spec.components!.schemas!.entries) { 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 output
..addAll(imports)
..addAll(clients)
..addAll(state.output) ..addAll(state.output)
..addAll(buildSerializer(state)); ..addAll(serializer);
final patterns = [ final patterns = [
RegExp( RegExp(

126
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<DynamiteResponse<JsonObject, void>> $get({
final ContentString<BuiltMap<String, JsonObject>>? contentString,
final ContentString<BuiltMap<String, JsonObject>>? 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<JsonObject, void> $getRaw({
final ContentString<BuiltMap<String, JsonObject>>? contentString,
final ContentString<BuiltMap<String, JsonObject>>? contentParameter,
}) {
final queryParameters = <String, dynamic>{};
final headers = <String, String>{
'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<JsonObject, void>(
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

51
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": []
}

8
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 version: 1.0.0
environment: environment:
sdk: '>=3.1.1 <4.0.0' sdk: '>=3.1.0 <4.0.0'
dependencies: dependencies:
built_value: ^8.6.2 built_collection: ^5.0.0
built_value: ^8.0.0
dynamite_runtime: dynamite_runtime:
git: git:
url: https://github.com/nextcloud/neon url: https://github.com/nextcloud/neon
path: packages/dynamite/dynamite_runtime path: packages/dynamite/dynamite_runtime
meta: ^1.0.0
universal_io: ^2.0.0
dev_dependencies: dev_dependencies:
build_runner: ^2.4.6 build_runner: ^2.4.6
build_verify: ^3.1.0 build_verify: ^3.1.0

Loading…
Cancel
Save