Skip to content

Commit

Permalink
fix(dynamite): Fix nullable someOf parameters
Browse files Browse the repository at this point in the history
Signed-off-by: jld3103 <[email protected]>
Signed-off-by: Nikolas Rimikis <[email protected]>
  • Loading branch information
provokateurin authored and Leptopoda committed Dec 11, 2023
1 parent 6094e63 commit d819c88
Show file tree
Hide file tree
Showing 9 changed files with 254 additions and 8 deletions.
10 changes: 8 additions & 2 deletions packages/dynamite/dynamite/lib/src/builder/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ Iterable<Method> buildTags(
parameter.schema,
);

final result = resolveType(
var result = resolveType(
spec,
state,
toDartName(
Expand All @@ -253,7 +253,12 @@ Iterable<Method> buildTags(
),
parameter.schema!,
nullable: dartParameterNullable,
).dartType;
);

// The dartType of a some-of is a TypeResultBase breaking naming serialization
if (result is! TypeResultSomeOf) {
result = result.dartType;
}

operationParameters.add(
Parameter(
Expand Down Expand Up @@ -424,6 +429,7 @@ Iterable<String> buildParameterSerialization(
final value = result.encode(
dartName,
onlyChildren: parameter.$in == openapi.ParameterType.query,
mimeType: 'application/json',
);

final mapName = switch (parameter.$in) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ class TypeResultEnum extends TypeResult {
@override
String encode(
final String object, {
required final String mimeType,
final bool onlyChildren = false,
final String? mimeType,
}) {
if (subType.name == 'String') {
return '$object.name';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ class TypeResultList extends TypeResult {
@override
String encode(
final String object, {
required final String mimeType,
final bool onlyChildren = false,
final String? mimeType,
}) {
if (onlyChildren) {
return '$object.map((final e) => ${subType.encode('e', mimeType: mimeType)})';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ class TypeResultObject extends TypeResult {
@override
String encode(
final String object, {
required final String mimeType,
final bool onlyChildren = false,
final String? mimeType,
}) {
if (className == _contentString) {
assert(mimeType == 'application/json', '$_contentString should have a mimeType of application/json');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ abstract class TypeResultSomeOf extends TypeResult {
return '$dartType $fieldName';
});

return TypeResultBase('({${record.join(',')}})');
return TypeResultBase(
'({${record.join(',')}})',
nullable: nullable,
);
}

late final String typeName = _typeName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@ sealed class TypeResult {
/// Encodes the variable named [object].
String encode(
final String object, {
required final String mimeType,
final bool onlyChildren = false,
final String? mimeType,
}) {
final serialized = serialize(object);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// ignore_for_file: discarded_futures
// ignore_for_file: public_member_api_docs
// ignore_for_file: unreachable_switch_case
import 'dart:convert';
import 'dart:typed_data';

import 'package:built_collection/built_collection.dart';
Expand All @@ -11,6 +12,7 @@ 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:dynamite_runtime/utils.dart' as dynamite_utils;
import 'package:meta/meta.dart';
import 'package:universal_io/io.dart';
import 'package:uri/uri.dart';
Expand Down Expand Up @@ -172,6 +174,142 @@ class Client extends DynamiteClient {
serializers: _jsonSerializers,
);
}

/// 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:
/// * [oneOf]
/// * [anyOf]
///
/// Status codes:
/// * default
///
/// See:
/// * [getSomeOfRaw] for an experimental operation that returns a [DynamiteRawResponse] that can be serialized.
Future<DynamiteResponse<void, void>> getSomeOf({
final GetSomeOfOneOf? oneOf,
final GetSomeOfAnyOf? anyOf,
}) async {
final rawResponse = getSomeOfRaw(
oneOf: oneOf,
anyOf: anyOf,
);

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:
/// * [oneOf]
/// * [anyOf]
///
/// Status codes:
/// * default
///
/// See:
/// * [getSomeOf] for an operation that returns a [DynamiteResponse] with a stable API.
@experimental
DynamiteRawResponse<void, void> getSomeOfRaw({
final GetSomeOfOneOf? oneOf,
final GetSomeOfAnyOf? anyOf,
}) {
final pathParameters = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final headers = <String, String>{};
Uint8List? body;

if (oneOf != null) {
queryParameters['oneOf'] =
json.encode(_jsonSerializers.serialize(oneOf, specifiedType: const FullType(GetSomeOfOneOf)));
}
if (anyOf != null) {
queryParameters['anyOf'] =
json.encode(_jsonSerializers.serialize(anyOf, specifiedType: const FullType(GetSomeOfAnyOf)));
}
var uri = Uri.parse(UriTemplate('/some-of').expand(pathParameters));
if (queryParameters.isNotEmpty) {
uri = uri.replace(queryParameters: queryParameters);
}

return DynamiteRawResponse<void, void>(
response: executeRequest(
'get',
uri,
headers,
body,
null,
),
bodyType: null,
headersType: null,
serializers: _jsonSerializers,
);
}
}

typedef GetSomeOfOneOf = ({bool? $bool, String? string});

typedef GetSomeOfAnyOf = ({bool? $bool, String? string});

typedef $BoolString = ({bool? $bool, String? string});

extension $BoolStringExtension on $BoolString {
List<dynamic> get _values => [$bool, string];
void validateOneOf() => dynamite_utils.validateOneOf(_values);
void validateAnyOf() => dynamite_utils.validateAnyOf(_values);
static Serializer<$BoolString> get serializer => const _$BoolStringSerializer();
static $BoolString fromJson(final Object? json) => _jsonSerializers.deserializeWith(serializer, json)!;
Object? toJson() => _jsonSerializers.serializeWith(serializer, this);
}

class _$BoolStringSerializer implements PrimitiveSerializer<$BoolString> {
const _$BoolStringSerializer();

@override
Iterable<Type> get types => const [$BoolString];

@override
String get wireName => r'$BoolString';

@override
Object serialize(
final Serializers serializers,
final $BoolString object, {
final FullType specifiedType = FullType.unspecified,
}) {
dynamic value;
value = object.$bool;
if (value != null) {
return serializers.serialize(value, specifiedType: const FullType(bool))!;
}
value = object.string;
if (value != null) {
return serializers.serialize(value, specifiedType: const FullType(String))!;
}
// Should not be possible after validation.
throw StateError('Tried to serialize without any value.');
}

@override
$BoolString deserialize(
final Serializers serializers,
final Object data, {
final FullType specifiedType = FullType.unspecified,
}) {
bool? $bool;
try {
$bool = serializers.deserialize(data, specifiedType: const FullType(bool))! as bool;
} catch (_) {}
String? string;
try {
string = serializers.deserialize(data, specifiedType: const FullType(String))! as String;
} catch (_) {}
return ($bool: $bool, string: string);
}
}

// coverage:ignore-start
Expand All @@ -186,7 +324,8 @@ final Serializers _serializers = (Serializers().toBuilder()
]),
ContentStringBuilder<BuiltMap<String, JsonObject>>.new,
)
..add(ContentString.serializer))
..add(ContentString.serializer)
..add($BoolStringExtension.serializer))
.build();

final Serializers _jsonSerializers = (_serializers.toBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,45 @@
}
}
}
},
"/some-of": {
"get": {
"parameters": [
{
"name": "oneOf",
"in": "query",
"schema": {
"oneOf": [
{
"type": "string"
},
{
"type": "boolean"
}
]
}
},
{
"name": "anyOf",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "boolean"
}
]
}
}
],
"responses": {
"default": {
"description": ""
}
}
}
}
},
"tags": []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,63 @@ void main() {
);
});
});

group('getSomeOf', () {
test('oneOf', () async {
await client.getSomeOf(oneOf: ($bool: true, string: null));
var captured = verify(mockHttpClient.openUrl(captureAny, captureAny)).captured;
expect(
captured,
equals([
'get',
Uri.parse('example.com/some-of?oneOf=true'),
]),
);
resetMockitoState();

await client.getSomeOf(oneOf: ($bool: null, string: 'value'));
captured = verify(mockHttpClient.openUrl(captureAny, captureAny)).captured;
expect(
captured,
equals([
'get',
Uri.parse('example.com/some-of?oneOf=%22value%22'),
]),
);
});

test('anyOf', () async {
await client.getSomeOf(anyOf: ($bool: true, string: null));
var captured = verify(mockHttpClient.openUrl(captureAny, captureAny)).captured;
expect(
captured,
equals([
'get',
Uri.parse('example.com/some-of?anyOf=true'),
]),
);
resetMockitoState();

await client.getSomeOf(anyOf: ($bool: null, string: 'value'));
captured = verify(mockHttpClient.openUrl(captureAny, captureAny)).captured;
expect(
captured,
equals([
'get',
Uri.parse('example.com/some-of?anyOf=%22value%22'),
]),
);
resetMockitoState();

await client.getSomeOf(anyOf: ($bool: true, string: 'value'));
captured = verify(mockHttpClient.openUrl(captureAny, captureAny)).captured;
expect(
captured,
equals([
'get',
Uri.parse('example.com/some-of?anyOf=true'),
]),
);
});
});
}

0 comments on commit d819c88

Please sign in to comment.