forked from loafle/openapi-generator-original
[dart][dart-dio-next] Improve support for file uploads (#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 #6671 * fixes #9079 * Add and fix tests * Only use MultipartFile for body/multipart parameters * Fix test * Actually fix tests
This commit is contained in:
parent
ae430a8c14
commit
ab11acd634
@ -82,7 +82,6 @@ jobs:
|
||||
- ~/.bundle
|
||||
- ~/.go_workspace
|
||||
- ~/.gradle
|
||||
- ~/.pub-cache
|
||||
- ~/.cache/bower
|
||||
- ".git"
|
||||
- ~/.stack
|
||||
|
@ -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;
|
||||
@ -192,6 +193,7 @@ public class DartDioNextClientCodegen extends AbstractDartCodegen {
|
||||
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) {
|
||||
@ -257,7 +259,7 @@ public class DartDioNextClientCodegen extends AbstractDartCodegen {
|
||||
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;
|
||||
@ -302,7 +304,6 @@ public class DartDioNextClientCodegen extends AbstractDartCodegen {
|
||||
sb.append(")]");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Map<String, Object> postProcessOperationsWithModels(Map<String, Object> objs, List<Object> allModels) {
|
||||
super.postProcessOperationsWithModels(objs, allModels);
|
||||
@ -313,11 +314,15 @@ public class DartDioNextClientCodegen extends AbstractDartCodegen {
|
||||
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);
|
||||
@ -328,7 +333,12 @@ public class DartDioNextClientCodegen extends AbstractDartCodegen {
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
@ -349,11 +359,16 @@ public class DartDioNextClientCodegen extends AbstractDartCodegen {
|
||||
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");
|
||||
}
|
||||
|
@ -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}}
|
||||
|
@ -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);
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
@ -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
|
||||
|
@ -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)),
|
||||
|
@ -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';
|
||||
@ -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,
|
||||
@ -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) {
|
||||
@ -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,
|
||||
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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';
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user