Skip to content

Commit

Permalink
[dart][dart-dio-next] Improve support for file uploads (OpenAPITools#…
Browse files Browse the repository at this point in the history
…9542)

* [dart][dart-dio] Improve support for file uploads

* add support for filenames in multipart requests by using  `MultipartFile` from dio directly
* add support for binary/file body data
* fixes OpenAPITools#6671
* fixes OpenAPITools#9079

* Add and fix tests

* Only use MultipartFile for body/multipart parameters

* Fix test

* Actually fix tests
  • Loading branch information
kuhnroyal authored May 25, 2021
1 parent ae430a8 commit ab11acd
Show file tree
Hide file tree
Showing 10 changed files with 146 additions and 26 deletions.
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";
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) {
super.postProcessOperationsWithModels(objs, allModels);
Expand All @@ -313,11 +314,15 @@ public Map<String, Object> postProcessOperationsWithModels(Map<String, Object> o
Set<String> resultImports = new HashSet<>();

for (CodegenOperation op : operationList) {
for (CodegenParameter param : op.bodyParams) {
if (param.baseType != null && param.baseType.equalsIgnoreCase("Uint8List") && op.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 @@ -328,7 +333,12 @@ public Map<String, Object> postProcessOperationsWithModels(Map<String, Object> o
}
}

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 @@ -349,11 +359,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]
**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 @@ -851,7 +851,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 @@ -493,7 +492,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 @@ -528,7 +527,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, stackTrace) {
Expand Down Expand Up @@ -588,7 +587,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 @@ -624,7 +623,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, stackTrace) {
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]',
},
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),
);
}
}

0 comments on commit ab11acd

Please sign in to comment.