[bug][dart][dart-dio] Improve parameterToString handling (#8372)

* [dart][dart-dio] Improve parameterToString handling

* add tests
* not sure this is complete but it is better than before and can serve as a baseline with the test cases

* Add a map parameter to FormData test

* Rename method to clarify what it actually does

* Couple more tests
This commit is contained in:
Peter Leibiger 2021-03-02 04:08:47 +01:00 committed by GitHub
parent 07f8bde6c1
commit 2ed702b339
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 457 additions and 68 deletions

View File

@ -77,7 +77,7 @@ class {{classname}} {
_bodyData = {{#isMultipart}}FormData.fromMap({{/isMultipart}}<String, dynamic>{ _bodyData = {{#isMultipart}}FormData.fromMap({{/isMultipart}}<String, dynamic>{
{{#formParams}} {{#formParams}}
{{^required}}{{^nullable}}if ({{{paramName}}} != null) {{/nullable}}{{/required}}r'{{{baseName}}}': {{#isFile}}MultipartFile.fromBytes({{{paramName}}}, filename: r'{{{baseName}}}'){{/isFile}}{{^isFile}}parameterToString(_serializers, {{{paramName}}}){{/isFile}}, {{^required}}{{^nullable}}if ({{{paramName}}} != null) {{/nullable}}{{/required}}r'{{{baseName}}}': {{#isFile}}MultipartFile.fromBytes({{{paramName}}}, filename: r'{{{baseName}}}'){{/isFile}}{{^isFile}}encodeFormParameter(_serializers, {{{paramName}}}, const FullType({{^isContainer}}{{{baseType}}}{{/isContainer}}{{#isContainer}}Built{{#isMap}}Map{{/isMap}}{{#isArray}}{{#uniqueItems}}Set{{/uniqueItems}}{{^uniqueItems}}List{{/uniqueItems}}{{/isArray}}, [{{#isMap}}FullType(String), {{/isMap}}FullType({{{baseType}}})]{{/isContainer}})){{/isFile}},
{{/formParams}} {{/formParams}}
}{{#isMultipart}}){{/isMultipart}}; }{{#isMultipart}}){{/isMultipart}};
{{/hasFormParams}} {{/hasFormParams}}

View File

@ -1,15 +1,25 @@
{{>header}} {{>header}}
import 'dart:convert'; import 'dart:convert';
import 'package:built_collection/built_collection.dart';
import 'package:built_value/serializer.dart'; import 'package:built_value/serializer.dart';
/// Format the given parameter object into string. /// Format the given form parameter object into something that Dio can handle.
String parameterToString(Serializers serializers, dynamic value) { /// Returns primitive or String.
if (value == null) { /// Returns List/Map if the value is BuildList/BuiltMap.
return ''; dynamic encodeFormParameter(Serializers serializers, dynamic value, FullType type) {
} else if (value is String || value is num) { if (value == null) {
return value.toString(); return '';
} else { }
return json.encode(serializers.serialize(value)); if (value is String || value is num || value is bool) {
} return value;
}
final serialized = serializers.serialize(value, specifiedType: type);
if (serialized is String) {
return serialized;
}
if (value is BuiltList || value is BuiltMap) {
return serialized;
}
return json.encode(serialized);
} }

View File

@ -419,8 +419,8 @@ class PetApi {
dynamic _bodyData; dynamic _bodyData;
_bodyData = <String, dynamic>{ _bodyData = <String, dynamic>{
if (name != null) r'name': parameterToString(_serializers, name), if (name != null) r'name': encodeFormParameter(_serializers, name, const FullType(String)),
if (status != null) r'status': parameterToString(_serializers, status), if (status != null) r'status': encodeFormParameter(_serializers, status, const FullType(String)),
}; };
final _response = await _dio.request<dynamic>( final _response = await _dio.request<dynamic>(
@ -475,7 +475,7 @@ class PetApi {
dynamic _bodyData; dynamic _bodyData;
_bodyData = FormData.fromMap(<String, dynamic>{ _bodyData = FormData.fromMap(<String, dynamic>{
if (additionalMetadata != null) r'additionalMetadata': parameterToString(_serializers, additionalMetadata), if (additionalMetadata != null) r'additionalMetadata': encodeFormParameter(_serializers, additionalMetadata, const FullType(String)),
if (file != null) r'file': MultipartFile.fromBytes(file, filename: r'file'), if (file != null) r'file': MultipartFile.fromBytes(file, filename: r'file'),
}); });

View File

@ -7,15 +7,25 @@
import 'dart:convert'; import 'dart:convert';
import 'package:built_collection/built_collection.dart';
import 'package:built_value/serializer.dart'; import 'package:built_value/serializer.dart';
/// Format the given parameter object into string. /// Format the given form parameter object into something that Dio can handle.
String parameterToString(Serializers serializers, dynamic value) { /// Returns primitive or String.
if (value == null) { /// Returns List/Map if the value is BuildList/BuiltMap.
return ''; dynamic encodeFormParameter(Serializers serializers, dynamic value, FullType type) {
} else if (value is String || value is num) { if (value == null) {
return value.toString(); return '';
} else { }
return json.encode(serializers.serialize(value)); if (value is String || value is num || value is bool) {
} return value;
}
final serialized = serializers.serialize(value, specifiedType: type);
if (serialized is String) {
return serialized;
}
if (value is BuiltList || value is BuiltMap) {
return serialized;
}
return json.encode(serialized);
} }

View File

@ -449,8 +449,8 @@ class PetApi {
dynamic _bodyData; dynamic _bodyData;
_bodyData = <String, dynamic>{ _bodyData = <String, dynamic>{
if (name != null) r'name': parameterToString(_serializers, name), if (name != null) r'name': encodeFormParameter(_serializers, name, const FullType(String)),
if (status != null) r'status': parameterToString(_serializers, status), if (status != null) r'status': encodeFormParameter(_serializers, status, const FullType(String)),
}; };
final _response = await _dio.request<dynamic>( final _response = await _dio.request<dynamic>(
@ -505,7 +505,7 @@ class PetApi {
dynamic _bodyData; dynamic _bodyData;
_bodyData = FormData.fromMap(<String, dynamic>{ _bodyData = FormData.fromMap(<String, dynamic>{
if (additionalMetadata != null) r'additionalMetadata': parameterToString(_serializers, additionalMetadata), if (additionalMetadata != null) r'additionalMetadata': encodeFormParameter(_serializers, additionalMetadata, const FullType(String)),
if (file != null) r'file': MultipartFile.fromBytes(file, filename: r'file'), if (file != null) r'file': MultipartFile.fromBytes(file, filename: r'file'),
}); });

View File

@ -7,15 +7,25 @@
import 'dart:convert'; import 'dart:convert';
import 'package:built_collection/built_collection.dart';
import 'package:built_value/serializer.dart'; import 'package:built_value/serializer.dart';
/// Format the given parameter object into string. /// Format the given form parameter object into something that Dio can handle.
String parameterToString(Serializers serializers, dynamic value) { /// Returns primitive or String.
if (value == null) { /// Returns List/Map if the value is BuildList/BuiltMap.
return ''; dynamic encodeFormParameter(Serializers serializers, dynamic value, FullType type) {
} else if (value is String || value is num) { if (value == null) {
return value.toString(); return '';
} else { }
return json.encode(serializers.serialize(value)); if (value is String || value is num || value is bool) {
} return value;
}
final serialized = serializers.serialize(value, specifiedType: type);
if (serialized is String) {
return serialized;
}
if (value is BuiltList || value is BuiltMap) {
return serialized;
}
return json.encode(serialized);
} }

View File

@ -587,20 +587,20 @@ class FakeApi {
dynamic _bodyData; dynamic _bodyData;
_bodyData = <String, dynamic>{ _bodyData = <String, dynamic>{
if (integer != null) r'integer': parameterToString(_serializers, integer), if (integer != null) r'integer': encodeFormParameter(_serializers, integer, const FullType(int)),
if (int32 != null) r'int32': parameterToString(_serializers, int32), if (int32 != null) r'int32': encodeFormParameter(_serializers, int32, const FullType(int)),
if (int64 != null) r'int64': parameterToString(_serializers, int64), if (int64 != null) r'int64': encodeFormParameter(_serializers, int64, const FullType(int)),
r'number': parameterToString(_serializers, number), r'number': encodeFormParameter(_serializers, number, const FullType(num)),
if (float != null) r'float': parameterToString(_serializers, float), if (float != null) r'float': encodeFormParameter(_serializers, float, const FullType(double)),
r'double': parameterToString(_serializers, double_), r'double': encodeFormParameter(_serializers, double_, const FullType(double)),
if (string != null) r'string': parameterToString(_serializers, string), if (string != null) r'string': encodeFormParameter(_serializers, string, const FullType(String)),
r'pattern_without_delimiter': parameterToString(_serializers, patternWithoutDelimiter), r'pattern_without_delimiter': encodeFormParameter(_serializers, patternWithoutDelimiter, const FullType(String)),
r'byte': parameterToString(_serializers, byte), r'byte': encodeFormParameter(_serializers, byte, const FullType(String)),
if (binary != null) r'binary': MultipartFile.fromBytes(binary, filename: r'binary'), if (binary != null) r'binary': MultipartFile.fromBytes(binary, filename: r'binary'),
if (date != null) r'date': parameterToString(_serializers, date), if (date != null) r'date': encodeFormParameter(_serializers, date, const FullType(DateTime)),
if (dateTime != null) r'dateTime': parameterToString(_serializers, dateTime), if (dateTime != null) r'dateTime': encodeFormParameter(_serializers, dateTime, const FullType(DateTime)),
if (password != null) r'password': parameterToString(_serializers, password), if (password != null) r'password': encodeFormParameter(_serializers, password, const FullType(String)),
if (callback != null) r'callback': parameterToString(_serializers, callback), if (callback != null) r'callback': encodeFormParameter(_serializers, callback, const FullType(String)),
}; };
final _response = await _dio.request<dynamic>( final _response = await _dio.request<dynamic>(
@ -661,8 +661,8 @@ class FakeApi {
dynamic _bodyData; dynamic _bodyData;
_bodyData = <String, dynamic>{ _bodyData = <String, dynamic>{
if (enumFormStringArray != null) r'enum_form_string_array': parameterToString(_serializers, enumFormStringArray), if (enumFormStringArray != null) r'enum_form_string_array': encodeFormParameter(_serializers, enumFormStringArray, const FullType(BuiltList, [FullType(String)])),
if (enumFormString != null) r'enum_form_string': parameterToString(_serializers, enumFormString), if (enumFormString != null) r'enum_form_string': encodeFormParameter(_serializers, enumFormString, const FullType(String)),
}; };
final _response = await _dio.request<dynamic>( final _response = await _dio.request<dynamic>(
@ -818,8 +818,8 @@ class FakeApi {
dynamic _bodyData; dynamic _bodyData;
_bodyData = <String, dynamic>{ _bodyData = <String, dynamic>{
r'param': parameterToString(_serializers, param), r'param': encodeFormParameter(_serializers, param, const FullType(String)),
r'param2': parameterToString(_serializers, param2), r'param2': encodeFormParameter(_serializers, param2, const FullType(String)),
}; };
final _response = await _dio.request<dynamic>( final _response = await _dio.request<dynamic>(

View File

@ -419,8 +419,8 @@ class PetApi {
dynamic _bodyData; dynamic _bodyData;
_bodyData = <String, dynamic>{ _bodyData = <String, dynamic>{
if (name != null) r'name': parameterToString(_serializers, name), if (name != null) r'name': encodeFormParameter(_serializers, name, const FullType(String)),
if (status != null) r'status': parameterToString(_serializers, status), if (status != null) r'status': encodeFormParameter(_serializers, status, const FullType(String)),
}; };
final _response = await _dio.request<dynamic>( final _response = await _dio.request<dynamic>(
@ -475,7 +475,7 @@ class PetApi {
dynamic _bodyData; dynamic _bodyData;
_bodyData = FormData.fromMap(<String, dynamic>{ _bodyData = FormData.fromMap(<String, dynamic>{
if (additionalMetadata != null) r'additionalMetadata': parameterToString(_serializers, additionalMetadata), if (additionalMetadata != null) r'additionalMetadata': encodeFormParameter(_serializers, additionalMetadata, const FullType(String)),
if (file != null) r'file': MultipartFile.fromBytes(file, filename: r'file'), if (file != null) r'file': MultipartFile.fromBytes(file, filename: r'file'),
}); });
@ -546,7 +546,7 @@ class PetApi {
dynamic _bodyData; dynamic _bodyData;
_bodyData = FormData.fromMap(<String, dynamic>{ _bodyData = FormData.fromMap(<String, dynamic>{
if (additionalMetadata != null) r'additionalMetadata': parameterToString(_serializers, additionalMetadata), if (additionalMetadata != null) r'additionalMetadata': encodeFormParameter(_serializers, additionalMetadata, const FullType(String)),
r'requiredFile': MultipartFile.fromBytes(requiredFile, filename: r'requiredFile'), r'requiredFile': MultipartFile.fromBytes(requiredFile, filename: r'requiredFile'),
}); });

View File

@ -7,15 +7,25 @@
import 'dart:convert'; import 'dart:convert';
import 'package:built_collection/built_collection.dart';
import 'package:built_value/serializer.dart'; import 'package:built_value/serializer.dart';
/// Format the given parameter object into string. /// Format the given form parameter object into something that Dio can handle.
String parameterToString(Serializers serializers, dynamic value) { /// Returns primitive or String.
if (value == null) { /// Returns List/Map if the value is BuildList/BuiltMap.
return ''; dynamic encodeFormParameter(Serializers serializers, dynamic value, FullType type) {
} else if (value is String || value is num) { if (value == null) {
return value.toString(); return '';
} else { }
return json.encode(serializers.serialize(value)); if (value is String || value is num || value is bool) {
} return value;
}
final serialized = serializers.serialize(value, specifiedType: type);
if (serialized is String) {
return serialized;
}
if (value is BuiltList || value is BuiltMap) {
return serialized;
}
return json.encode(serialized);
} }

View File

@ -0,0 +1,119 @@
import 'dart:typed_data';
import 'package:built_collection/built_collection.dart';
import 'package:dio/dio.dart';
import 'package:http_mock_adapter/http_mock_adapter.dart';
import 'package:openapi/api.dart';
import 'package:openapi/api/fake_api.dart';
import 'package:test/test.dart';
void main() {
Openapi client;
DioAdapter server;
setUp(() {
server = DioAdapter();
client = Openapi(dio: Dio()..httpClientAdapter = server);
});
tearDown(() {
server.close();
});
group(FakeApi, () {
group('testEndpointParameters', () {
test('complete', () async {
server.onPost(
'/fake',
data: {
'number': '3',
'double': '-13.57',
'pattern_without_delimiter': 'patternWithoutDelimiter',
'byte': '0',
'float': '1.23',
'integer': '45',
'int32': '2147483647',
'int64': '9223372036854775807',
'date': '2020-08-11T00:00:00.000Z',
'dateTime': '2020-08-11T12:30:55.123Z',
'binary': "Instance of 'MultipartFile'",
},
headers: <String, dynamic>{
'content-type': 'application/x-www-form-urlencoded',
'content-length': 255,
},
handler: (response) => response.reply(200, null),
);
final response = await client.getFakeApi().testEndpointParameters(
3,
-13.57,
'patternWithoutDelimiter',
'0',
float: 1.23,
integer: 45,
int32: 2147483647,
int64: 9223372036854775807,
date: DateTime.utc(2020, 8, 11),
dateTime: DateTime.utc(2020, 8, 11, 12, 30, 55, 123),
binary: Uint8List.fromList([0, 1, 2, 3, 4, 5]),
);
expect(response.statusCode, 200);
});
test('minimal', () async {
server.onPost(
'/fake',
data: {
'byte': '0',
'double': '-13.57',
'number': '3',
'pattern_without_delimiter': 'patternWithoutDelimiter',
},
headers: <String, dynamic>{
'content-type': 'application/x-www-form-urlencoded',
'content-length': 79,
},
handler: (response) => response.reply(200, null),
);
final response = await client.getFakeApi().testEndpointParameters(
3,
-13.57,
'patternWithoutDelimiter',
'0',
);
expect(response.statusCode, 200);
});
});
group('testEnumParameters', () {
test('in body data', () async {
// Not sure if this is correct, we are not sending
// form data in the body but some weird map
server.onGet(
'/fake',
data: {
'enum_form_string': 'formString',
'enum_form_string_array': '[foo, bar]',
},
headers: <String, dynamic>{
'content-type': 'application/x-www-form-urlencoded',
},
handler: (response) => response.reply(200, null),
);
final response = await client.getFakeApi().testEnumParameters(
enumFormString: 'formString',
enumFormStringArray: ListBuilder<String>(
<String>['foo', 'bar'],
).build(),
);
expect(response.statusCode, 200);
});
});
});
}

View File

@ -0,0 +1,230 @@
import 'package:built_collection/built_collection.dart';
import 'package:built_value/serializer.dart';
import 'package:dio/dio.dart';
import 'package:openapi/api_util.dart';
import 'package:openapi/model/cat.dart';
import 'package:openapi/serializers.dart';
import 'package:test/test.dart';
void main() {
group('api_utils', () {
group('encodeFormParameter should return', () {
test('empty String for null', () {
expect(
encodeFormParameter(
standardSerializers,
null,
const FullType(Cat),
),
'',
);
});
test('String for String', () {
expect(
encodeFormParameter(
standardSerializers,
'foo',
const FullType(String),
),
'foo',
);
});
test('List<String> for BuiltList<String>', () {
expect(
encodeFormParameter(
standardSerializers,
ListBuilder<String>(['foo', 'bar', 'baz']).build(),
const FullType(BuiltList, [FullType(String)]),
),
['foo', 'bar', 'baz'],
);
});
test('Map<String, String> for BuiltList<String, String>', () {
expect(
encodeFormParameter(
standardSerializers,
MapBuilder<String, String>({
'foo': 'foo-value',
'bar': 'bar-value',
'baz': 'baz-value',
}).build(),
const FullType(BuiltMap, [FullType(String), FullType(String)]),
),
{
'foo': 'foo-value',
'bar': 'bar-value',
'baz': 'baz-value',
},
);
});
test('num for num', () {
expect(
encodeFormParameter(standardSerializers, 0, const FullType(int)),
0,
);
expect(
encodeFormParameter(standardSerializers, 1, const FullType(int)),
1,
);
expect(
encodeFormParameter(standardSerializers, 1.0, const FullType(num)),
1.0,
);
expect(
encodeFormParameter(
standardSerializers, 1.234, const FullType(double)),
1.234,
);
});
test('List<num> for BuiltList<num>', () {
expect(
encodeFormParameter(
standardSerializers,
ListBuilder<num>([0, 1, 2, 3, 4.5, -123.456]).build(),
const FullType(BuiltList, [FullType(num)]),
),
[0, 1, 2, 3, 4.5, -123.456],
);
});
test('bool for bool', () {
expect(
encodeFormParameter(
standardSerializers,
true,
const FullType(bool),
),
true,
);
expect(
encodeFormParameter(
standardSerializers,
false,
const FullType(bool),
),
false,
);
});
test('String for Date', () {
expect(
encodeFormParameter(
standardSerializers,
DateTime.utc(2020, 8, 11),
const FullType(DateTime),
),
'2020-08-11T00:00:00.000Z',
);
});
test('String for DateTime', () {
expect(
encodeFormParameter(
standardSerializers,
DateTime.utc(2020, 8, 11, 12, 30, 55, 123),
const FullType(DateTime),
),
'2020-08-11T12:30:55.123Z',
);
});
test('JSON String for Cat', () {
// Not sure that is even a valid case,
// sending complex objects via FormData may not work as expected
expect(
encodeFormParameter(
standardSerializers,
(CatBuilder()
..color = 'black'
..className = 'cat'
..declawed = false)
.build(),
const FullType(Cat),
),
'{"className":"cat","color":"black","declawed":false}',
);
});
});
test('encodes FormData correctly', () {
final data = FormData.fromMap({
'null': encodeFormParameter(
standardSerializers,
null,
const FullType(num),
),
'empty': encodeFormParameter(
standardSerializers,
'',
const FullType(String),
),
'string_list': encodeFormParameter(
standardSerializers,
ListBuilder<String>(['foo', 'bar', 'baz']).build(),
const FullType(BuiltList, [FullType(String)]),
),
'num_list': encodeFormParameter(
standardSerializers,
ListBuilder<num>([0, 1, 2, 3, 4.5, -123.456]).build(),
const FullType(BuiltList, [FullType(num)]),
),
'string_map': encodeFormParameter(
standardSerializers,
MapBuilder<String, String>({
'foo': 'foo-value',
'bar': 'bar-value',
'baz': 'baz-value',
}).build(),
const FullType(BuiltMap, [FullType(String), FullType(String)]),
),
'bool': encodeFormParameter(
standardSerializers,
true,
const FullType(bool),
),
'double': encodeFormParameter(
standardSerializers,
-123.456,
const FullType(double),
),
'date_time': encodeFormParameter(
standardSerializers,
DateTime.utc(2020, 8, 11, 12, 30, 55, 123),
const FullType(DateTime),
),
});
expect(
data.fields,
pairwiseCompare<MapEntry<String, String>, MapEntry<String, String>>(
<MapEntry<String, String>>[
MapEntry('null', ''),
MapEntry('empty', ''),
MapEntry('string_list[]', 'foo'),
MapEntry('string_list[]', 'bar'),
MapEntry('string_list[]', 'baz'),
MapEntry('num_list[]', '0'),
MapEntry('num_list[]', '1'),
MapEntry('num_list[]', '2'),
MapEntry('num_list[]', '3'),
MapEntry('num_list[]', '4.5'),
MapEntry('num_list[]', '-123.456'),
MapEntry('string_map[foo]', 'foo-value'),
MapEntry('string_map[bar]', 'bar-value'),
MapEntry('string_map[baz]', 'baz-value'),
MapEntry('bool', 'true'),
MapEntry('double', '-123.456'),
MapEntry('date_time', '2020-08-11T12:30:55.123Z'),
],
(e, a) => e.key == a.key && e.value == a.value,
'Compares map entries by key and value',
),
);
});
});
}