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 16, 2023
1 parent e73a27e commit 0c3de09
Show file tree
Hide file tree
Showing 52 changed files with 5,075 additions and 3,602 deletions.
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

0 comments on commit 0c3de09

Please sign in to comment.