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

feat(dynamite): always send parameters even when being the default #1292

Merged
merged 3 commits into from
Dec 17, 2023
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
110 changes: 60 additions & 50 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 All @@ -235,10 +234,6 @@ Iterable<Method> buildTags(
var returnHeadersType = 'void';

for (final parameter in parameters) {
final dartParameterNullable = isDartParameterNullable(
parameter.required,
parameter.schema,
);
final parameterRequired = isRequired(
parameter.required,
parameter.schema,
Expand All @@ -252,8 +247,8 @@ Iterable<Method> buildTags(
uppercaseFirstCharacter: true,
),
parameter.schema!,
nullable: dartParameterNullable,
).dartType;
nullable: !parameterRequired,
);

operationParameters.add(
Parameter(
Expand All @@ -263,16 +258,12 @@ Iterable<Method> buildTags(
..name = toDartName(parameter.name)
..required = parameterRequired
..type = refer(result.nullableName);

if (parameter.schema!.$default != null) {
b.defaultTo = Code(valueToEscapedValue(result, parameter.schema!.$default!.toString()));
}
provokateurin marked this conversation as resolved.
Show resolved Hide resolved
},
),
);

buildParameterPatternCheck(parameter).forEach(code.writeln);
buildParameterSerialization(result, parameter).forEach(code.writeln);
code.writeln(buildParameterSerialization(result, parameter));
}
resolveMimeTypeEncode(operation, spec, state, operationName, operationParameters).forEach(code.writeln);

Expand Down Expand Up @@ -325,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 @@ -412,45 +426,41 @@ return rawResponse.future;
}
}

Iterable<String> buildParameterSerialization(
String buildParameterSerialization(
final TypeResult result,
final openapi.Parameter parameter,
) sync* {
) {
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(
dartName,
onlyChildren: parameter.$in == openapi.ParameterType.query,
);
final buffer = StringBuffer()..write('var $serializedName = ${result.serialize(dartName)};');

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;";

if (!parameter.required && (result.nullable || hasDefault)) {
yield 'if( ';
if (result.nullable) {
yield '$dartName != null ';
}
if (result.nullable && hasDefault) {
yield '&&';
}
if (hasDefault) {
yield '$dartName != $defaultValueCode';
if ($default != null) {
buffer.writeln('$serializedName ??= $defaultValueCode;');
}

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);
}
yield ') {';
yield assignment;
yield '}';
} else {
yield assignment;
buffer.writeln("_parameters['${parameter.pctEncodedName}'] = $serializedName;");
}

return buffer.toString();
}

Iterable<String> buildParameterPatternCheck(
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