Skip to content

Commit

Permalink
fix(dynamite): object serialization in query parameters
Browse files Browse the repository at this point in the history
Signed-off-by: Nikolas Rimikis <[email protected]>
  • Loading branch information
Leptopoda committed Dec 12, 2023
1 parent 0396947 commit 6ac38a3
Show file tree
Hide file tree
Showing 29 changed files with 2,210 additions and 3,321 deletions.
45 changes: 29 additions & 16 deletions packages/dynamite/dynamite/lib/src/builder/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:dynamite/src/helpers/type_result.dart';
import 'package:dynamite/src/models/openapi.dart' as openapi;
import 'package:dynamite/src/models/type_result.dart';
import 'package:intersperse/intersperse.dart';
import 'package:uri/uri.dart';

Iterable<Class> generateClients(
final openapi.OpenAPI spec,
Expand Down Expand Up @@ -216,8 +217,7 @@ Iterable<Method> buildTags(
.join(',');

code.writeln('''
final _pathParameters = <String, dynamic>{};
final _queryParameters = <String, dynamic>{};
final _parameters = <String, dynamic>{};
final _headers = <String, String>{${acceptHeader.isNotEmpty ? "'Accept': '$acceptHeader'," : ''}};
Uint8List? _body;
''');
Expand Down Expand Up @@ -325,12 +325,30 @@ Iterable<Method> buildTags(
toDartName(identifierBuilder.toString(), uppercaseFirstCharacter: true),
);

code.writeln('''
var _uri = Uri.parse(UriTemplate('${pathEntry.key}').expand(_pathParameters));
if (_queryParameters.isNotEmpty) {
_uri = _uri.replace(queryParameters: _queryParameters);
}
''');
final queryParams = <String>[];
for (final parameter in parameters) {
if (parameter.$in != openapi.ParameterType.query) {
continue;
}

queryParams.add(parameter.uriTemplate(withPrefix: false));
}

final pathBuilder = StringBuffer()..write(pathEntry.key);

if (queryParams.isNotEmpty) {
pathBuilder
..write('{?')
..writeAll(queryParams, ',')
..write('}');
}

final path = pathBuilder.toString();

// Sanity check the uri at build time.
UriTemplate(path);

code.writeln("final _uri = Uri.parse(UriTemplate('$path').expand(_parameters));");

if (dataType != null) {
returnDataType = dataType.name;
Expand Down Expand Up @@ -420,19 +438,14 @@ Iterable<String> buildParameterSerialization(
final hasDefault = $default != null;
final defaultValueCode = valueToEscapedValue(result, $default.toString());
final dartName = toDartName(parameter.name);

final value = result.encode(
dartName,
onlyChildren: parameter.$in == openapi.ParameterType.query,
);
final value = parameter.$in == openapi.ParameterType.header ? result.encode(dartName) : result.serialize(dartName);

final mapName = switch (parameter.$in) {
openapi.ParameterType.path => '_pathParameters',
openapi.ParameterType.query => '_queryParameters',
openapi.ParameterType.path || openapi.ParameterType.query => '_parameters',
openapi.ParameterType.header => '_headers',
_ => throw UnsupportedError('Can not work with parameter "${parameter.name}" in "${parameter.$in}"'),
};
final assignment = "$mapName['${parameter.name}'] = $value;";
final assignment = "$mapName['${parameter.pctName}'] = $value;";

if (!parameter.required && (result.nullable || hasDefault)) {
yield 'if( ';
Expand Down
1 change: 1 addition & 0 deletions packages/dynamite/dynamite/lib/src/models/openapi.g.dart

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

131 changes: 128 additions & 3 deletions packages/dynamite/dynamite/lib/src/models/openapi/parameter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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';
import 'package:meta/meta.dart';

part 'parameter.g.dart';

Expand All @@ -26,16 +27,20 @@ abstract class Parameter implements Built<Parameter, ParameterBuilder> {

bool get required;

@Deprecated('Use [schema] instead which also automatically handles [content].')
@protected
@BuiltValueField(wireName: 'schema')
Schema? get $schema;

BuiltMap<String, MediaType>? get content;

bool get explode;

bool get allowReserved;

ParameterStyle get style;

Schema? get schema {
// ignore: deprecated_member_use_from_same_package
if ($schema != null) {
// ignore: deprecated_member_use_from_same_package
return $schema;
}

Expand All @@ -54,9 +59,111 @@ abstract class Parameter implements Built<Parameter, ParameterBuilder> {
return null;
}

/// Builds the uri template value for this parameter.
///
/// Throws a StateError if the parameter does not support a uri template.
String uriTemplate({
final bool isFirst = false,
final bool withPrefix = true,
}) {
final error = 'The parameter $name is not supported by RFC 6570 UriTemplates. Dynamite can not yet work with it.';
final buffer = StringBuffer();

final prefix = switch (style) {
ParameterStyle.simple => null,
ParameterStyle.label => '.',
ParameterStyle.matrix => ';',
ParameterStyle.form => isFirst ? '?' : '&',
ParameterStyle.spaceDelimited || ParameterStyle.pipeDelimited => explode
? isFirst
? '?'
: '&'
: throw UnsupportedError(error),
ParameterStyle.deepObject || _ => throw UnsupportedError(error),
};

if (prefix != null && withPrefix) {
buffer.write(prefix);
}

if (allowReserved) {
buffer.write('+');
}

buffer.write(pctName);

if (explode) {
buffer.write('*');
}

return buffer.toString();
}

/// The pct encoded name of this parameter.
String get pctName => Uri.encodeQueryComponent(name);

@BuiltValueHook(finalizeBuilder: true)
static void _defaults(final ParameterBuilder b) {
b.required ??= false;
b._allowReserved ??= false;
b._explode ??= switch (b.$in!) {
ParameterType.query || ParameterType.cookie => true,
ParameterType.path || ParameterType.header => false,
_ => throw StateError('invalid parameter type'),
};
b._style ??= switch (b.$in!) {
ParameterType.query => ParameterStyle.form,
ParameterType.path => ParameterStyle.simple,
ParameterType.header => ParameterStyle.simple,
ParameterType.cookie => ParameterStyle.form,
_ => throw StateError('invalid parameter type'),
};

switch (b.style) {
case ParameterStyle.matrix:
if (b._$in != ParameterType.path) {
throw OpenAPISpecError('ParameterStyle.matrix can only be used in path parameters.');
}
case ParameterStyle.label:
if (b._$in != ParameterType.path) {
throw OpenAPISpecError('ParameterStyle.label can only be used in path parameters.');
}

case ParameterStyle.form:
if (b._$in != ParameterType.query && b._$in != ParameterType.cookie) {
throw OpenAPISpecError('ParameterStyle.form can only be used in query or cookie parameters.');
}

case ParameterStyle.simple:
if (b._$in != ParameterType.path && b._$in != ParameterType.header) {
throw OpenAPISpecError('ParameterStyle.simple can only be used in path or header parameters.');
}

case ParameterStyle.spaceDelimited:
if (b._$schema?.type != SchemaType.array && b._$schema?.type != SchemaType.object) {
throw OpenAPISpecError('ParameterStyle.spaceDelimited can only be used with array or object schemas.');
}
if (b._$in != ParameterType.query) {
throw OpenAPISpecError('ParameterStyle.spaceDelimited can only be used in query parameters.');
}

case ParameterStyle.pipeDelimited:
if (b._$schema?.type != SchemaType.array && b._$schema?.type != SchemaType.object) {
throw OpenAPISpecError('ParameterStyle.pipeDelimited can only be used with array or object schemas.');
}
if (b._$in != ParameterType.query) {
throw OpenAPISpecError('ParameterStyle.pipeDelimited can only be used in query parameters.');
}

case ParameterStyle.deepObject:
if (b._$schema?.type != SchemaType.object) {
throw OpenAPISpecError('ParameterStyle.deepObject can only be used with object schemas.');
}
if (b._$in != ParameterType.query) {
throw OpenAPISpecError('ParameterStyle.deepObject can only be used in query parameters.');
}
}

if (b.$in == ParameterType.path && !b.required!) {
throw OpenAPISpecError('Path parameters must be required but ${b.name} is not.');
}
Expand Down Expand Up @@ -111,3 +218,21 @@ class ParameterType extends EnumClass {

static Serializer<ParameterType> get serializer => _$parameterTypeSerializer;
}

class ParameterStyle extends EnumClass {
const ParameterStyle._(super.name);

static const ParameterStyle matrix = _$parameterStyleMatrix;
static const ParameterStyle label = _$parameterStyleLabel;
static const ParameterStyle form = _$parameterStyleForm;
static const ParameterStyle simple = _$parameterStyleSimple;
static const ParameterStyle spaceDelimited = _$parameterStyleSpaceDelimited;
static const ParameterStyle pipeDelimited = _$parameterStylePipeDelimited;
static const ParameterStyle deepObject = _$parameterStyleDeepObject;

static BuiltSet<ParameterStyle> get values => _$parameterStyleValues;

static ParameterStyle valueOf(final String name) => _$parameterStyle(name);

static Serializer<ParameterStyle> get serializer => _$parameterStyleSerializer;
}
Loading

0 comments on commit 6ac38a3

Please sign in to comment.