Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(dynamite): object serialization in query parameters #1278

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 54 additions & 31 deletions packages/dynamite/dynamite/lib/src/builder/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import 'package:dynamite/src/builder/state.dart';
import 'package:dynamite/src/helpers/dart_helpers.dart';
import 'package:dynamite/src/helpers/dynamite.dart';
import 'package:dynamite/src/helpers/pattern_check.dart';
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 +216,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 @@ -317,12 +316,35 @@ 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;
}

// Default to a plain parameter without exploding.
queryParams.add(parameter.uriTemplate(withPrefix: false) ?? parameter.pctEncodedName);
}

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.
try {
UriTemplate(path);
} on ParseException catch (e) {
throw Exception('The resulting uri $path is not a valid uri template according to RFC 6570. $e');
}

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

if (dataType != null) {
returnDataType = dataType.name;
Expand Down Expand Up @@ -409,32 +431,33 @@ String buildParameterSerialization(
final openapi.Parameter parameter,
) {
final $default = parameter.schema?.$default;
final hasDefault = $default != null;
final defaultValueCode = valueToEscapedValue(result, $default.toString());
var defaultValueCode = $default?.value;
if ($default != null && $default.isString) {
defaultValueCode = "'${$default.asString}'";
}
final dartName = toDartName(parameter.name);
final serializedName = '\$$dartName';

final value = result.encode(
hasDefault ? '($dartName ?? $defaultValueCode)' : dartName,
onlyChildren: parameter.$in == openapi.ParameterType.query,
);
final buffer = StringBuffer()..write('var $serializedName = ${result.serialize(dartName)};');

if ($default != null) {
buffer.writeln('$serializedName ??= $defaultValueCode;');
}

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

final assignment = "$mapName['${parameter.name}'] = $value;";

final buffer = StringBuffer();
if (!parameter.required && result.nullable && !hasDefault) {
buffer
..write('if ($dartName != null) {')
..write(assignment)
..write('}');
if (parameter.$in == openapi.ParameterType.header) {
final assignment =
"_headers['${parameter.pctEncodedName}'] = ${result.encode(serializedName, onlyChildren: true)};";

if ($default == null) {
buffer
..writeln('if ($serializedName != null) {')
..writeln(assignment)
..writeln('}');
} else {
buffer.writeln(assignment);
}
} else {
buffer.write(assignment);
buffer.writeln("_parameters['${parameter.pctEncodedName}'] = $serializedName;");
}

return buffer.toString();
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.

132 changes: 129 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,112 @@ abstract class Parameter implements Built<Parameter, ParameterBuilder> {
return null;
}

/// Builds the uri template value for this parameter.
///
/// When the parameter is in a collection of parameters the prefix can be different.
/// Specify [isFirst] according to this. When [withPrefix] is `false` the prefix will be dropped entirely.
///
/// Returns `null` if the parameter does not support a uri template.
String? uriTemplate({
final bool isFirst = true,
final bool withPrefix = true,
}) {
final buffer = StringBuffer();

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

if (prefix == null && style != ParameterStyle.simple) {
return null;
}

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

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

buffer.write(pctEncodedName);

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

return buffer.toString();
}

/// The pct encoded name of this parameter.
String get pctEncodedName => 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 +219,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
Loading