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

[dart][dart-dio-next] Improve support for file uploads #9542

Merged
merged 6 commits into from
May 25, 2021
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 0 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ jobs:
- ~/.bundle
- ~/.go_workspace
- ~/.gradle
- ~/.pub-cache
- ~/.cache/bower
- ".git"
- ~/.stack
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public class DartDioNextClientCodegen extends AbstractDartCodegen {
public static final String SERIALIZATION_LIBRARY_BUILT_VALUE = "built_value";
public static final String SERIALIZATION_LIBRARY_DEFAULT = SERIALIZATION_LIBRARY_BUILT_VALUE;

private static final String DIO_IMPORT = "package:dio/dio.dart";
kuhnroyal marked this conversation as resolved.
Show resolved Hide resolved
private static final String CLIENT_NAME = "clientName";

private String dateLibrary;
Expand Down Expand Up @@ -192,6 +193,7 @@ private void configureSerializationLibraryBuiltValue(String srcFolder) {
imports.put("BuiltMap", "package:built_collection/built_collection.dart");
imports.put("JsonObject", "package:built_value/json_object.dart");
imports.put("Uint8List", "dart:typed_data");
imports.put("MultipartFile", DIO_IMPORT);
}

private void configureDateLibrary(String srcFolder) {
Expand Down Expand Up @@ -257,7 +259,7 @@ public Map<String, Object> postProcessModels(Map<String, Object> objs) {
for (Object _mo : models) {
Map<String, Object> mo = (Map<String, Object>) _mo;
CodegenModel cm = (CodegenModel) mo.get("model");
cm.imports = rewriteImports(cm.imports);
cm.imports = rewriteImports(cm.imports, true);
cm.vendorExtensions.put("x-has-vars", !cm.vars.isEmpty());
}
return objs;
Expand Down Expand Up @@ -302,7 +304,6 @@ private void appendBuiltValueCollection(StringBuilder sb, CodegenProperty proper
sb.append(")]");
}


@Override
public Map<String, Object> postProcessOperationsWithModels(Map<String, Object> objs, List<Object> allModels) {
objs = super.postProcessOperationsWithModels(objs, allModels);
Expand All @@ -329,11 +330,15 @@ public Map<String, Object> postProcessOperationsWithModels(Map<String, Object> o
}
}

for (CodegenParameter param : op.bodyParams) {
if (param.baseType != null && param.baseType.equalsIgnoreCase("Uint8List") && isMultipart) {
for (CodegenParameter param : op.allParams) {
if (((op.isMultipart && param.isFormParam) || param.isBodyParam) && (param.isBinary || param.isFile)) {
param.baseType = "MultipartFile";
param.dataType = "MultipartFile";
op.imports.add("MultipartFile");
}
}

for (CodegenParameter param : op.bodyParams) {
if (param.isContainer) {
final Map<String, Object> serializer = new HashMap<>();
serializer.put("isArray", param.isArray);
Expand All @@ -348,7 +353,12 @@ public Map<String, Object> postProcessOperationsWithModels(Map<String, Object> o
op.vendorExtensions.put("x-is-form", isForm);
op.vendorExtensions.put("x-is-multipart", isMultipart);

resultImports.addAll(rewriteImports(op.imports));
if (op.allParams.stream().noneMatch(param -> param.dataType.equals("Uint8List"))) {
// Remove unused imports after processing
op.imports.remove("Uint8List");
}

resultImports.addAll(rewriteImports(op.imports, false));
if (op.getHasFormParams()) {
resultImports.add("package:" + pubName + "/src/api_util.dart");
}
Expand All @@ -369,11 +379,16 @@ public Map<String, Object> postProcessOperationsWithModels(Map<String, Object> o
return objs;
}

private Set<String> rewriteImports(Set<String> originalImports) {
private Set<String> rewriteImports(Set<String> originalImports, boolean isModel) {
Set<String> resultImports = Sets.newHashSet();
for (String modelImport : originalImports) {
if (imports.containsKey(modelImport)) {
resultImports.add(imports.get(modelImport));
String i = imports.get(modelImport);
if (Objects.equals(i, DIO_IMPORT) && !isModel) {
// Don't add imports to operations that are already imported
continue;
}
resultImports.add(i);
} else {
resultImports.add("package:" + pubName + "/src/model/" + underscore(modelImport) + ".dart");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{{#hasFormParams}}
_bodyData = {{#isMultipart}}FormData.fromMap({{/isMultipart}}<String, dynamic>{
{{#formParams}}
{{^required}}{{^isNullable}}if ({{{paramName}}} != null) {{/isNullable}}{{/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}},
{{^required}}{{^isNullable}}if ({{{paramName}}} != null) {{/isNullable}}{{/required}}r'{{{baseName}}}': {{#isFile}}{{{paramName}}}{{/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}}
}{{#isMultipart}}){{/isMultipart}};
{{/hasFormParams}}
{{#bodyParam}}
{{#isPrimitiveType}}
_bodyData = {{paramName}};
_bodyData = {{paramName}}{{#isFile}}.finalize(){{/isFile}};
{{/isPrimitiveType}}
{{^isPrimitiveType}}
{{#isContainer}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ import 'package:openapi/api.dart';
var api_instance = new PetApi();
var petId = 789; // int | ID of pet to update
var additionalMetadata = additionalMetadata_example; // String | Additional data to pass to server
var file = BINARY_DATA_HERE; // Uint8List | file to upload
var file = BINARY_DATA_HERE; // MultipartFile | file to upload

try {
var result = api_instance.uploadFile(petId, additionalMetadata, file);
Expand All @@ -361,7 +361,7 @@ Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**petId** | **int**| ID of pet to update |
**additionalMetadata** | **String**| Additional data to pass to server | [optional]
**file** | **Uint8List**| file to upload | [optional]
kuhnroyal marked this conversation as resolved.
Show resolved Hide resolved
**file** | **MultipartFile**| file to upload | [optional]

### Return type

Expand Down Expand Up @@ -391,7 +391,7 @@ import 'package:openapi/api.dart';

var api_instance = new PetApi();
var petId = 789; // int | ID of pet to update
var requiredFile = BINARY_DATA_HERE; // Uint8List | file to upload
var requiredFile = BINARY_DATA_HERE; // MultipartFile | file to upload
var additionalMetadata = additionalMetadata_example; // String | Additional data to pass to server

try {
Expand All @@ -407,7 +407,7 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**petId** | **int**| ID of pet to update |
**requiredFile** | **Uint8List**| file to upload |
**requiredFile** | **MultipartFile**| file to upload |
**additionalMetadata** | **String**| Additional data to pass to server | [optional]

### Return type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -875,7 +875,7 @@ class FakeApi {
if (string != null) r'string': encodeFormParameter(_serializers, string, const FullType(String)),
r'pattern_without_delimiter': encodeFormParameter(_serializers, patternWithoutDelimiter, const FullType(String)),
r'byte': encodeFormParameter(_serializers, byte, const FullType(String)),
if (binary != null) r'binary': MultipartFile.fromBytes(binary, filename: r'binary'),
if (binary != null) r'binary': binary,
if (date != null) r'date': encodeFormParameter(_serializers, date, const FullType(Date)),
if (dateTime != null) r'dateTime': encodeFormParameter(_serializers, dateTime, const FullType(DateTime)),
if (password != null) r'password': encodeFormParameter(_serializers, password, const FullType(String)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import 'dart:async';
import 'package:built_value/serializer.dart';
import 'package:dio/dio.dart';

import 'dart:typed_data';
import 'package:built_collection/built_collection.dart';
import 'package:openapi/src/api_util.dart';
import 'package:openapi/src/model/api_response.dart';
Expand Down Expand Up @@ -513,7 +512,7 @@ class PetApi {
Future<Response<ApiResponse>> uploadFile({
required int petId,
String? additionalMetadata,
Uint8List? file,
MultipartFile? file,
CancelToken? cancelToken,
Map<String, dynamic>? headers,
Map<String, dynamic>? extra,
Expand Down Expand Up @@ -550,7 +549,7 @@ class PetApi {
try {
_bodyData = FormData.fromMap(<String, dynamic>{
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': file,
});

} catch(error) {
Expand Down Expand Up @@ -610,7 +609,7 @@ class PetApi {
///
Future<Response<ApiResponse>> uploadFileWithRequiredFile({
required int petId,
required Uint8List requiredFile,
required MultipartFile requiredFile,
String? additionalMetadata,
CancelToken? cancelToken,
Map<String, dynamic>? headers,
Expand Down Expand Up @@ -648,7 +647,7 @@ class PetApi {
try {
_bodyData = FormData.fromMap(<String, dynamic>{
if (additionalMetadata != null) r'additionalMetadata': encodeFormParameter(_serializers, additionalMetadata, const FullType(String)),
r'requiredFile': MultipartFile.fromBytes(requiredFile, filename: r'requiredFile'),
r'requiredFile': requiredFile,
});

} catch(error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ environment:
sdk: '>=2.10.0 <3.0.0'

dev_dependencies:
built_collection: '5.0.0'
built_value: '8.0.4'
dio: '4.0.0'
built_collection: 5.0.0
built_value: 8.0.6
dio: 4.0.0
http_mock_adapter: 0.2.1
mockito: '5.0.3'
mockito: 5.0.8
openapi:
path: ../petstore_client_lib_fake
test: '1.16.8'
test: 1.17.4
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ void main() {
'int64': '9223372036854775807',
'date': '2020-08-11',
'dateTime': '2020-08-11T12:30:55.123Z',
'binary': "Instance of 'MultipartFile'",
'binary': '[0, 1, 2, 3, 4, 5]',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this makes sense to send over the wire like this, but better than before in any case.

},
headers: <String, dynamic>{
'content-type': 'application/x-www-form-urlencoded',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import 'package:built_collection/built_collection.dart';
import 'package:dio/dio.dart';
import 'package:http_mock_adapter/http_mock_adapter.dart';
import 'package:http_parser/http_parser.dart';
import 'package:openapi/openapi.dart';
import 'package:test/test.dart';

import '../matcher/form_data_matcher.dart';

void main() {
const photo1 = 'https://localhost/photo1.jpg';
const photo2 = 'https://localhost/photo2.jpg';
Expand Down Expand Up @@ -221,5 +224,83 @@ void main() {
expect(response.data[1].status, PetStatusEnum.available);
});
});

group('uploadFile', () {
test('uploadFileWithRequiredFile', () async {
final file = MultipartFile.fromBytes(
[1, 2, 3, 4],
filename: 'test.png',
contentType: MediaType.parse('image/png'),
);

server.onRoute(
'/fake/5/uploadImageWithRequiredFile',
(request) => request.reply(200, {
'code': 200,
'type': 'success',
'message': 'File uploaded',
}),
request: Request(
method: RequestMethods.post,
headers: <String, dynamic>{
Headers.contentTypeHeader:
Matchers.pattern('multipart/form-data'),
Headers.contentLengthHeader: Matchers.integer,
},
data: FormDataMatcher(
expected: FormData.fromMap(<String, dynamic>{
r'requiredFile': file,
}),
),
),
);
final response = await client.getPetApi().uploadFileWithRequiredFile(
petId: 5,
requiredFile: file,
);

expect(response.statusCode, 200);
expect(response.data.message, 'File uploaded');
});

test('uploadFileWithRequiredFile & additionalMetadata', () async {
final file = MultipartFile.fromBytes(
[1, 2, 3, 4],
filename: 'test.png',
contentType: MediaType.parse('image/png'),
);

server.onRoute(
'/fake/3/uploadImageWithRequiredFile',
(request) => request.reply(200, {
'code': 200,
'type': 'success',
'message': 'File uploaded',
}),
request: Request(
method: RequestMethods.post,
headers: <String, dynamic>{
Headers.contentTypeHeader:
Matchers.pattern('multipart/form-data'),
Headers.contentLengthHeader: Matchers.integer,
},
data: FormDataMatcher(
expected: FormData.fromMap(<String, dynamic>{
'additionalMetadata': 'foo',
r'requiredFile': file,
}),
),
),
);
final response = await client.getPetApi().uploadFileWithRequiredFile(
petId: 3,
requiredFile: file,
additionalMetadata: 'foo',
);

expect(response.statusCode, 200);
expect(response.data.message, 'File uploaded');
});
});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'package:dio/dio.dart';
import 'package:meta/meta.dart';
import 'package:collection/collection.dart';
import 'package:http_mock_adapter/src/matchers/matcher.dart';

class FormDataMatcher extends Matcher {
final FormData expected;

const FormDataMatcher({@required this.expected});

@override
bool matches(dynamic actual) {
if (actual is! FormData) {
return false;
}
final data = actual as FormData;
return MapEquality<String, String>().equals(
Map.fromEntries(expected.fields),
Map.fromEntries(data.fields),
) &&
MapEquality<String, MultipartFile>().equals(
Map.fromEntries(expected.files),
Map.fromEntries(data.files),
);
}
}