Merge pull request #101 from vidya-hub/form_data_imp

Form-Data and multi-part request feature added
This commit is contained in:
Ashita Prasad
2024-01-07 20:57:46 +05:30
committed by GitHub
25 changed files with 1114 additions and 78 deletions

View File

@ -1,8 +1,8 @@
import 'package:apidash/consts.dart'; import 'dart:convert';
import 'package:apidash/models/request_model.dart' show RequestModel;
import 'package:code_builder/code_builder.dart'; import 'package:code_builder/code_builder.dart';
import 'package:dart_style/dart_style.dart'; import 'package:dart_style/dart_style.dart';
import 'package:apidash/models/request_model.dart' show RequestModel;
import 'package:apidash/consts.dart';
import 'shared.dart'; import 'shared.dart';
class DartDioCodeGen { class DartDioCodeGen {
@ -22,6 +22,7 @@ class DartDioCodeGen {
headers: requestModel.enabledHeadersMap, headers: requestModel.enabledHeadersMap,
body: requestModel.requestBody, body: requestModel.requestBody,
contentType: requestModel.requestBodyContentType, contentType: requestModel.requestBodyContentType,
formData: requestModel.formDataMapList,
); );
return next; return next;
} catch (e) { } catch (e) {
@ -36,6 +37,7 @@ class DartDioCodeGen {
required Map<String, String> headers, required Map<String, String> headers,
required String? body, required String? body,
required ContentType contentType, required ContentType contentType,
required List<Map<String, dynamic>> formData,
}) { }) {
final sbf = StringBuffer(); final sbf = StringBuffer();
final emitter = DartEmitter(); final emitter = DartEmitter();
@ -54,9 +56,22 @@ class DartDioCodeGen {
literalMap(headers.map((key, value) => MapEntry(key, value))), literalMap(headers.map((key, value) => MapEntry(key, value))),
); );
} }
final multiPartList = Code('''
final List<Map<String,String>> formDataList = ${json.encode(formData)};
for (var formField in formDataList) {
if (formField['type'] == 'file') {
formData.files.add(MapEntry(
formField['name'],
await MultipartFile.fromFile(formField['value'], filename: formField['value']),
));
} else {
formData.fields.add(MapEntry(formField['name'], formField['value']));
}
}
''');
Expression? dataExp; Expression? dataExp;
if (kMethodsWithBody.contains(method) && (body?.isNotEmpty ?? false)) { if ((kMethodsWithBody.contains(method) && (body?.isNotEmpty ?? false) ||
contentType == ContentType.formdata)) {
final strContent = CodeExpression(Code('r\'\'\'$body\'\'\'')); final strContent = CodeExpression(Code('r\'\'\'$body\'\'\''));
switch (contentType) { switch (contentType) {
// dio dosen't need pass `content-type` header when body is json or plain text // dio dosen't need pass `content-type` header when body is json or plain text
@ -68,6 +83,8 @@ class DartDioCodeGen {
case ContentType.text: case ContentType.text:
dataExp = declareFinal('data').assign(strContent); dataExp = declareFinal('data').assign(strContent);
// when add new type of [ContentType], need update [dataExp]. // when add new type of [ContentType], need update [dataExp].
case ContentType.formdata:
dataExp = declareFinal('data').assign(refer('FormData()'));
} }
} }
final responseExp = declareFinal('response').assign(InvokeExpression.newOf( final responseExp = declareFinal('response').assign(InvokeExpression.newOf(
@ -93,6 +110,8 @@ class DartDioCodeGen {
if (queryParamExp != null) queryParamExp, if (queryParamExp != null) queryParamExp,
if (headerExp != null) headerExp, if (headerExp != null) headerExp,
if (dataExp != null) dataExp, if (dataExp != null) dataExp,
if ((contentType == ContentType.formdata && formData.isNotEmpty))
multiPartList,
responseExp, responseExp,
refer('print').call([refer('response').property('statusCode')]), refer('print').call([refer('response').property('statusCode')]),
refer('print').call([refer('response').property('data')]), refer('print').call([refer('response').property('data')]),
@ -122,6 +141,8 @@ class DartDioCodeGen {
sbf.writeln(mainFunction.accept(emitter)); sbf.writeln(mainFunction.accept(emitter));
return DartFormatter(pageWidth: 160).format(sbf.toString()); return DartFormatter(
pageWidth: contentType == ContentType.formdata ? 70 : 160)
.format(sbf.toString());
} }
} }

View File

@ -1,10 +1,9 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:apidash/consts.dart';
import 'package:apidash/models/models.dart' show RequestModel;
import 'package:code_builder/code_builder.dart'; import 'package:code_builder/code_builder.dart';
import 'package:dart_style/dart_style.dart'; import 'package:dart_style/dart_style.dart';
import 'package:apidash/models/models.dart' show RequestModel;
import 'package:apidash/consts.dart';
import 'shared.dart'; import 'shared.dart';
class DartHttpCodeGen { class DartHttpCodeGen {
@ -22,9 +21,10 @@ class DartHttpCodeGen {
method: requestModel.method, method: requestModel.method,
queryParams: requestModel.enabledParamsMap, queryParams: requestModel.enabledParamsMap,
headers: {...requestModel.enabledHeadersMap}, headers: {...requestModel.enabledHeadersMap},
body: requestModel.requestBody,
contentType: requestModel.requestBodyContentType, contentType: requestModel.requestBodyContentType,
hasContentTypeHeader: requestModel.hasContentTypeHeader, hasContentTypeHeader: requestModel.hasContentTypeHeader,
body: requestModel.requestBody,
formData: requestModel.formDataMapList,
); );
return next; return next;
} catch (e) { } catch (e) {
@ -37,9 +37,10 @@ class DartHttpCodeGen {
required HTTPVerb method, required HTTPVerb method,
required Map<String, String> queryParams, required Map<String, String> queryParams,
required Map<String, String> headers, required Map<String, String> headers,
required String? body,
required ContentType contentType, required ContentType contentType,
required String? body,
required bool hasContentTypeHeader, required bool hasContentTypeHeader,
required List<Map<String, dynamic>> formData,
}) { }) {
final uri = Uri.parse(url); final uri = Uri.parse(url);
@ -55,7 +56,6 @@ class DartHttpCodeGen {
if (kMethodsWithBody.contains(method) && (body?.isNotEmpty ?? false)) { if (kMethodsWithBody.contains(method) && (body?.isNotEmpty ?? false)) {
final strContent = CodeExpression(Code('r\'\'\'$body\'\'\'')); final strContent = CodeExpression(Code('r\'\'\'$body\'\'\''));
dataExp = declareVar('body', type: refer('String')).assign(strContent); dataExp = declareVar('body', type: refer('String')).assign(strContent);
if (!hasContentTypeHeader) { if (!hasContentTypeHeader) {
headers.putIfAbsent(HttpHeaders.contentTypeHeader, headers.putIfAbsent(HttpHeaders.contentTypeHeader,
() => kContentTypeMap[contentType] ?? ''); () => kContentTypeMap[contentType] ?? '');
@ -102,7 +102,9 @@ class DartHttpCodeGen {
); );
} }
final responseExp = declareFinal('response').assign(InvokeExpression.newOf( final responseExp = declareFinal('response').assign(InvokeExpression.newOf(
refer('http.${method.name}'), refer(
'http.${method.name}',
),
[refer('uri')], [refer('uri')],
{ {
if (headerExp != null) 'headers': refer('headers'), if (headerExp != null) 'headers': refer('headers'),
@ -110,7 +112,36 @@ class DartHttpCodeGen {
}, },
[], [],
).awaited); ).awaited);
final multiPartRequest =
declareFinal('request').assign(InvokeExpression.newOf(
refer(
'http.MultipartRequest',
),
[refer(jsonEncode(method.name.toUpperCase())), refer('uri')],
));
final multiPartFiles = declareFinal('formDataList').assign(refer(
jsonEncode(formData),
));
final addHeaders = refer('request.headers.addAll').call([refer('headers')]);
const multiPartList = Code('''
for (Map<String, String> formData in formDataList){
if (formData['type'] == 'text') {
request.fields.addAll({formData['name']: formData['value']});
} else {
request.files.add(
await http.MultipartFile.fromPath(
formData['name'],
formData['value'],
),
);
}
}
''');
var multiPartRequestSend =
declareFinal('response').assign(refer('request.send()').awaited);
var multiPartResponseBody = declareFinal('responseBody')
.assign(refer('response.stream.bytesToString()').awaited);
final mainFunction = Method((m) { final mainFunction = Method((m) {
final statusRef = refer('statusCode'); final statusRef = refer('statusCode');
m m
@ -135,25 +166,49 @@ class DartHttpCodeGen {
b.statements.add(headerExp.statement); b.statements.add(headerExp.statement);
} }
b.statements.add(const Code('\n')); b.statements.add(const Code('\n'));
if (contentType == ContentType.formdata) {
if (formData.isNotEmpty) {
b.statements.add(multiPartFiles.statement);
}
b.statements.add(multiPartRequest.statement);
if (formData.isNotEmpty) {
b.statements.add(multiPartList);
}
if (headerExp != null) {
b.statements.add(addHeaders.statement);
}
b.statements.add(multiPartRequestSend.statement);
b.statements.add(multiPartResponseBody.statement);
b.statements.add(declareVar('statusCode', type: refer('int'))
.assign(refer('response').property('statusCode'))
.statement);
b.statements.add(const Code('\n'));
} else {
b.statements.add(responseExp.statement); b.statements.add(responseExp.statement);
b.statements.add(const Code('\n')); b.statements.add(const Code('\n'));
b.statements.add(declareVar('statusCode', type: refer('int')) b.statements.add(declareVar('statusCode', type: refer('int'))
.assign(refer('response').property('statusCode')) .assign(refer('response').property('statusCode'))
.statement); .statement);
}
b.statements.add(declareIfElse( b.statements.add(declareIfElse(
condition: statusRef condition: statusRef
.greaterOrEqualTo(literalNum(200)) .greaterOrEqualTo(literalNum(200))
.and(statusRef.lessThan(literalNum(300))), .and(statusRef.lessThan(literalNum(300))),
body: [ body: [
refer('print').call([literalString(r'Status Code: $statusCode')]), refer('print').call([literalString(r'Status Code: $statusCode')]),
refer('print') refer('print').call([
.call([literalString(r'Response Body: ${response.body}')]), literalString(
'Response Body: ${contentType == ContentType.formdata ? ':\$responseBody' : '\${response.body}'}')
]),
], ],
elseBody: [ elseBody: [
refer('print') refer('print')
.call([literalString(r'Error Status Code: $statusCode')]), .call([literalString(r'Error Status Code: $statusCode')]),
refer('print').call( refer('print').call([
[literalString(r'Error Response Body: ${response.body}')]), literalString(
'Error Response Body: ${contentType == ContentType.formdata ? ':\$responseBody' : '\${response.body}'}')
]),
], ],
)); ));
}); });
@ -161,6 +216,8 @@ class DartHttpCodeGen {
sbf.writeln(mainFunction.accept(emitter)); sbf.writeln(mainFunction.accept(emitter));
return DartFormatter(pageWidth: 160).format(sbf.toString()); return DartFormatter(
pageWidth: contentType == ContentType.formdata ? 70 : 160)
.format(sbf.toString());
} }
} }

View File

@ -1,16 +1,18 @@
import 'package:apidash/consts.dart'; import 'dart:convert';
import 'package:jinja/jinja.dart' as jj; import 'package:jinja/jinja.dart' as jj;
import 'package:apidash/utils/utils.dart' import 'package:apidash/utils/utils.dart'
show requestModelToHARJsonRequest, padMultilineString, stripUrlParams; show padMultilineString, requestModelToHARJsonRequest, stripUrlParams;
import 'package:apidash/models/models.dart' show RequestModel; import 'package:apidash/models/models.dart' show RequestModel;
import 'package:apidash/consts.dart';
class AxiosCodeGen { class AxiosCodeGen {
AxiosCodeGen({this.isNodeJs = false}); AxiosCodeGen({this.isNodeJs = false});
final bool isNodeJs; final bool isNodeJs;
String kStringImportNode = """import axios from 'axios'; String kStringImportNode = """{% if isNodeJs %}import axios from 'axios';
{% endif %}{% if isFormDataRequest and isNodeJs %}const fs = require('fs');{% endif %}
"""; """;
String kTemplateStart = """let config = { String kTemplateStart = """let config = {
@ -46,13 +48,49 @@ axios(config)
console.log(error); console.log(error);
}); });
"""; """;
String kMultiPartBodyTemplate = r'''
async function buildFormData(fields) {
var formdata = new FormData();
for (const field of fields) {
const name = field.name || '';
const value = field.value || '';
const type = field.type || 'text';
if (type === 'text') {
formdata.append(name, value);
} else if (type === 'file') {
formdata.append(name,{% if isNodeJs %} fs.createReadStream(value){% else %} fileInput.files[0],value{% endif %});
}
}
return formdata;
}
''';
var kGetFormDataTemplate = '''buildFormData({{fields_list}});
''';
String? getCode( String? getCode(
RequestModel requestModel, RequestModel requestModel,
String defaultUriScheme, String defaultUriScheme,
) { ) {
try { try {
String result = isNodeJs ? kStringImportNode : ""; jj.Template kNodejsImportTemplate = jj.Template(kStringImportNode);
String importsData = kNodejsImportTemplate.render({
"isFormDataRequest": requestModel.isFormDataRequest,
"isNodeJs": isNodeJs,
});
String result = importsData;
if (requestModel.isFormDataRequest &&
requestModel.formDataMapList.isNotEmpty) {
var templateMultiPartBody = jj.Template(kMultiPartBodyTemplate);
var renderedMultiPartBody = templateMultiPartBody.render({
"isNodeJs": isNodeJs,
});
result += renderedMultiPartBody;
}
String url = requestModel.url; String url = requestModel.url;
if (!url.contains("://") && url.isNotEmpty) { if (!url.contains("://") && url.isNotEmpty) {
@ -80,18 +118,31 @@ axios(config)
} }
var headers = harJson["headers"]; var headers = harJson["headers"];
if (headers.isNotEmpty) { if (headers.isNotEmpty || requestModel.isFormDataRequest) {
var templateHeader = jj.Template(kTemplateHeader); var templateHeader = jj.Template(kTemplateHeader);
var m = {}; var m = {};
for (var i in headers) { for (var i in headers) {
m[i["name"]] = i["value"]; m[i["name"]] = i["value"];
} }
if (requestModel.isFormDataRequest) {
m['Content-Type'] = 'multipart/form-data';
}
result += templateHeader result += templateHeader
.render({"headers": padMultilineString(kEncoder.convert(m), 2)}); .render({"headers": padMultilineString(kEncoder.convert(m), 2)});
} }
if (harJson["postData"]?["text"] != null) {
var templateBody = jj.Template(kTemplateBody); var templateBody = jj.Template(kTemplateBody);
if (requestModel.isFormDataRequest &&
requestModel.formDataMapList.isNotEmpty) {
var getFieldDataTemplate = jj.Template(kGetFormDataTemplate);
result += templateBody.render({
"body": getFieldDataTemplate.render({
"fields_list": json.encode(requestModel.formDataMapList),
})
});
}
if (harJson["postData"]?["text"] != null) {
result += templateBody result += templateBody
.render({"body": kEncoder.convert(harJson["postData"]["text"])}); .render({"body": kEncoder.convert(harJson["postData"]["text"])});
} }

View File

@ -1,15 +1,18 @@
import 'package:apidash/consts.dart'; import 'dart:convert';
import 'package:jinja/jinja.dart' as jj; import 'package:jinja/jinja.dart' as jj;
import 'package:apidash/utils/utils.dart' import 'package:apidash/utils/utils.dart'
show requestModelToHARJsonRequest, padMultilineString; show padMultilineString, requestModelToHARJsonRequest;
import 'package:apidash/models/models.dart' show RequestModel; import 'package:apidash/models/models.dart' show RequestModel;
import 'package:apidash/consts.dart';
class FetchCodeGen { class FetchCodeGen {
FetchCodeGen({this.isNodeJs = false}); FetchCodeGen({this.isNodeJs = false});
final bool isNodeJs; final bool isNodeJs;
String kStringImportNode = """import fetch from 'node-fetch'; String kStringImportNode = """
import fetch from 'node-fetch';
{% if isFormDataRequest %}const fs = require('fs');{% endif %}
"""; """;
@ -28,6 +31,26 @@ let options = {
{{body}} {{body}}
"""; """;
String kMultiPartBodyTemplate = r'''
async function buildDataList(fields) {
var formdata = new FormData();
for (const field of fields) {
const name = field.name || '';
const value = field.value || '';
const type = field.type || 'text';
if (type === 'text') {
formdata.append(name, value);
} else if (type === 'file') {
formdata.append(name,{% if isNodeJs %} fs.createReadStream(value){% else %} fileInput.files[0],value{% endif %});
}
}
return formdata;
}
const payload = buildDataList({{fields_list}});
''';
String kStringRequest = """ String kStringRequest = """
}; };
@ -53,8 +76,19 @@ fetch(url, options)
String defaultUriScheme, String defaultUriScheme,
) { ) {
try { try {
String result = isNodeJs ? kStringImportNode : ""; jj.Template kNodejsImportTemplate = jj.Template(kStringImportNode);
String importsData = kNodejsImportTemplate.render({
"isFormDataRequest": requestModel.isFormDataRequest,
});
String result = isNodeJs ? importsData : "";
if (requestModel.isFormDataRequest) {
var templateMultiPartBody = jj.Template(kMultiPartBodyTemplate);
result += templateMultiPartBody.render({
"isNodeJs": isNodeJs,
"fields_list": json.encode(requestModel.formDataMapList),
});
}
String url = requestModel.url; String url = requestModel.url;
if (!url.contains("://") && url.isNotEmpty) { if (!url.contains("://") && url.isNotEmpty) {
url = "$defaultUriScheme://$url"; url = "$defaultUriScheme://$url";
@ -70,21 +104,33 @@ fetch(url, options)
}); });
var headers = harJson["headers"]; var headers = harJson["headers"];
if (headers.isNotEmpty) { if (headers.isNotEmpty) {
var templateHeader = jj.Template(kTemplateHeader); var templateHeader = jj.Template(kTemplateHeader);
var m = {}; var m = {};
if (requestModel.isFormDataRequest) {
m["Content-Type"] = "multipart/form-data";
}
for (var i in headers) { for (var i in headers) {
m[i["name"]] = i["value"]; m[i["name"]] = i["value"];
} }
result += templateHeader result += templateHeader.render({
.render({"headers": padMultilineString(kEncoder.convert(m), 2)}); "headers": padMultilineString(kEncoder.convert(m), 2),
});
} }
if (harJson["postData"]?["text"] != null) { if (harJson["postData"]?["text"] != null) {
var templateBody = jj.Template(kTemplateBody); var templateBody = jj.Template(kTemplateBody);
result += templateBody result += templateBody.render({
.render({"body": kEncoder.convert(harJson["postData"]["text"])}); "body": kEncoder.convert(harJson["postData"]["text"]),
});
} else if (requestModel.isFormDataRequest) {
var templateBody = jj.Template(kTemplateBody);
result += templateBody.render({
"body": 'payload',
});
} }
result += kStringRequest; result += kStringRequest;
return result; return result;
} catch (e) { } catch (e) {

View File

@ -1,9 +1,9 @@
import 'dart:convert'; import 'dart:convert';
import 'package:apidash/consts.dart';
import 'package:jinja/jinja.dart' as jj; import 'package:jinja/jinja.dart' as jj;
import 'package:apidash/utils/utils.dart' import 'package:apidash/utils/utils.dart'
show getValidRequestUri, stripUriParams; show getValidRequestUri, stripUriParams;
import '../../models/request_model.dart'; import '../../models/request_model.dart';
import 'package:apidash/consts.dart';
class KotlinOkHttpCodeGen { class KotlinOkHttpCodeGen {
final String kTemplateStart = """import okhttp3.OkHttpClient final String kTemplateStart = """import okhttp3.OkHttpClient
@ -61,6 +61,13 @@ import okhttp3.MediaType.Companion.toMediaType""";
} }
"""; """;
// Converting list of form data objects to kolin multi part data
String kFormDataBody = '''
val body = MultipartBody.Builder().setType(MultipartBody.FORM){% for item in formDataList %}{% if item.type == 'file' %}
.addFormDataPart("{{item.name}}",null,File("{{item.value}}").asRequestBody("application/octet-stream".toMediaType()))
{% else %}.addFormDataPart("{{item.name}}","{{item.value}}")
{% endif %}{% endfor %}.build()
''';
String? getCode( String? getCode(
RequestModel requestModel, RequestModel requestModel,
@ -68,7 +75,6 @@ import okhttp3.MediaType.Companion.toMediaType""";
) { ) {
try { try {
String result = ""; String result = "";
bool hasHeaders = false;
bool hasQuery = false; bool hasQuery = false;
bool hasBody = false; bool hasBody = false;
@ -102,7 +108,13 @@ import okhttp3.MediaType.Companion.toMediaType""";
var method = requestModel.method; var method = requestModel.method;
var requestBody = requestModel.requestBody; var requestBody = requestModel.requestBody;
if (kMethodsWithBody.contains(method) && requestBody != null) { if (requestModel.isFormDataRequest) {
var formDataTemplate = jj.Template(kFormDataBody);
result += formDataTemplate.render({
"formDataList": requestModel.formDataMapList,
});
} else if (kMethodsWithBody.contains(method) && requestBody != null) {
var contentLength = utf8.encode(requestBody).length; var contentLength = utf8.encode(requestBody).length;
if (contentLength > 0) { if (contentLength > 0) {
hasBody = true; hasBody = true;
@ -127,7 +139,6 @@ import okhttp3.MediaType.Companion.toMediaType""";
if (headersList != null) { if (headersList != null) {
var headers = requestModel.enabledHeadersMap; var headers = requestModel.enabledHeadersMap;
if (headers.isNotEmpty) { if (headers.isNotEmpty) {
hasHeaders = true;
result += getHeaders(headers); result += getHeaders(headers);
} }
} }
@ -135,7 +146,7 @@ import okhttp3.MediaType.Companion.toMediaType""";
var templateRequestEnd = jj.Template(kTemplateRequestEnd); var templateRequestEnd = jj.Template(kTemplateRequestEnd);
result += templateRequestEnd.render({ result += templateRequestEnd.render({
"method": method.name.toLowerCase(), "method": method.name.toLowerCase(),
"hasBody": hasBody ? "body" : "", "hasBody": (hasBody || requestModel.isFormDataRequest) ? "body" : "",
}); });
} }
return result; return result;

View File

@ -10,6 +10,9 @@ class cURLCodeGen {
String kTemplateHeader = """ \\ String kTemplateHeader = """ \\
--header '{{name}}: {{value}}' --header '{{name}}: {{value}}'
"""; """;
String kTemplateFormData = """ \\
--form '{{name}}: {{value}}'
""";
String kTemplateBody = """ \\ String kTemplateBody = """ \\
--data '{{body}}' --data '{{body}}'
@ -48,6 +51,23 @@ class cURLCodeGen {
.render({"name": item["name"], "value": item["value"]}); .render({"name": item["name"], "value": item["value"]});
} }
} }
if (harJson['formData'] != null) {
var formDataList = harJson['formData'] as List<Map<String, dynamic>>;
for (var formData in formDataList) {
var templateFormData = jj.Template(kTemplateFormData);
if (formData['type'] != null &&
formData['name'] != null &&
formData['value'] != null &&
formData['name']!.isNotEmpty &&
formData['value']!.isNotEmpty) {
result += templateFormData.render({
"name": formData["name"],
"value":
"${formData['type'] == 'file' ? '@' : ''}${formData["value"]}",
});
}
}
}
if (harJson["postData"]?["text"] != null) { if (harJson["postData"]?["text"] != null) {
var templateBody = jj.Template(kTemplateBody); var templateBody = jj.Template(kTemplateBody);

View File

@ -1,13 +1,16 @@
import 'dart:io'; import 'dart:io';
import 'dart:convert'; import 'dart:convert';
import 'package:jinja/jinja.dart' as jj; import 'package:jinja/jinja.dart' as jj;
import 'package:apidash/consts.dart';
import 'package:apidash/utils/utils.dart' import 'package:apidash/utils/utils.dart'
show getValidRequestUri, padMultilineString; show getNewUuid, getValidRequestUri, padMultilineString;
import 'package:apidash/models/models.dart' show RequestModel; import 'package:apidash/models/models.dart' show RequestModel;
import 'package:apidash/consts.dart';
class PythonHttpClientCodeGen { class PythonHttpClientCodeGen {
final String kTemplateStart = """import http.client final String kTemplateStart = """import http.client
{% if isFormDataRequest %}import mimetypes
from codecs import encode
{% endif %}
"""; """;
String kTemplateParams = """ String kTemplateParams = """
@ -30,6 +33,8 @@ body = r'''{{body}}'''
headers = {{headers}} headers = {{headers}}
"""; """;
String kTemplateFormHeaderContentType = '''
multipart/form-data; boundary={{boundary}}''';
int kHeadersPadding = 10; int kHeadersPadding = 10;
@ -55,10 +60,38 @@ data = res.read()
print(data.decode("utf-8")) print(data.decode("utf-8"))
"""; """;
final String kStringFormDataBody = r'''
def build_data_list(fields):
dataList = []
for field in fields:
name = field.get('name', '')
value = field.get('value', '')
type_ = field.get('type', 'text')
dataList.append(encode('--{{boundary}}'))
if type_ == 'text':
dataList.append(encode(f'Content-Disposition: form-data; name="{name}"'))
dataList.append(encode('Content-Type: text/plain'))
dataList.append(encode(''))
dataList.append(encode(value))
elif type_ == 'file':
dataList.append(encode(f'Content-Disposition: form-data; name="{name}"; filename="{value}"'))
dataList.append(encode(f'Content-Type: {mimetypes.guess_type(value)[0] or "application/octet-stream"}'))
dataList.append(encode(''))
dataList.append(open(value, 'rb').read())
dataList.append(encode(f'--{{boundary}}--'))
dataList.append(encode(''))
return dataList
dataList = build_data_list({{fields_list}})
body = b'\r\n'.join(dataList)
''';
String? getCode( String? getCode(
RequestModel requestModel, RequestModel requestModel,
String defaultUriScheme, String defaultUriScheme,
) { ) {
String uuid = getNewUuid();
try { try {
String result = ""; String result = "";
bool hasHeaders = false; bool hasHeaders = false;
@ -70,11 +103,17 @@ print(data.decode("utf-8"))
url = "$defaultUriScheme://$url"; url = "$defaultUriScheme://$url";
} }
result += kTemplateStart; var templateStartUrl = jj.Template(kTemplateStart);
result += templateStartUrl.render(
{
"isFormDataRequest": requestModel.isFormDataRequest,
},
);
var rec = getValidRequestUri( var rec = getValidRequestUri(
url, url,
requestModel.enabledRequestParams, requestModel.enabledRequestParams,
); );
Uri? uri = rec.$1; Uri? uri = rec.$1;
if (uri != null) { if (uri != null) {
@ -103,6 +142,14 @@ print(data.decode("utf-8"))
var headersList = requestModel.enabledRequestHeaders; var headersList = requestModel.enabledRequestHeaders;
if (headersList != null || hasBody) { if (headersList != null || hasBody) {
var headers = requestModel.enabledHeadersMap; var headers = requestModel.enabledHeadersMap;
if (requestModel.isFormDataRequest) {
var formHeaderTemplate =
jj.Template(kTemplateFormHeaderContentType);
headers[HttpHeaders.contentTypeHeader] = formHeaderTemplate.render({
"boundary": uuid,
});
}
if (headers.isNotEmpty || hasBody) { if (headers.isNotEmpty || hasBody) {
hasHeaders = true; hasHeaders = true;
if (hasBody && !requestModel.hasContentTypeHeader) { if (hasBody && !requestModel.hasContentTypeHeader) {
@ -115,7 +162,15 @@ print(data.decode("utf-8"))
result += templateHeaders.render({"headers": headersString}); result += templateHeaders.render({"headers": headersString});
} }
} }
if (requestModel.isFormDataRequest) {
var formDataBodyData = jj.Template(kStringFormDataBody);
result += formDataBodyData.render(
{
"fields_list": json.encode(requestModel.formDataMapList),
"boundary": uuid,
},
);
}
var templateConnection = jj.Template(kTemplateConnection); var templateConnection = jj.Template(kTemplateConnection);
result += templateConnection.render({ result += templateConnection.render({
"isHttps": uri.scheme == "https" ? "S" : "", "isHttps": uri.scheme == "https" ? "S" : "",
@ -129,11 +184,11 @@ print(data.decode("utf-8"))
"queryParamsStr": hasQuery ? " + queryParamsStr" : "", "queryParamsStr": hasQuery ? " + queryParamsStr" : "",
}); });
if (hasBody) { if (hasBody || requestModel.isFormDataRequest) {
result += kStringRequestBody; result += kStringRequestBody;
} }
if (hasHeaders) { if (hasHeaders || requestModel.isFormDataRequest) {
result += kStringRequestHeaders; result += kStringRequestHeaders;
} }

View File

@ -3,12 +3,14 @@ import 'dart:convert';
import 'package:jinja/jinja.dart' as jj; import 'package:jinja/jinja.dart' as jj;
import 'package:apidash/consts.dart'; import 'package:apidash/consts.dart';
import 'package:apidash/utils/utils.dart' import 'package:apidash/utils/utils.dart'
show getValidRequestUri, padMultilineString, stripUriParams; show getNewUuid, getValidRequestUri, padMultilineString, stripUriParams;
import 'package:apidash/models/models.dart' show RequestModel; import 'package:apidash/models/models.dart' show RequestModel;
class PythonRequestsCodeGen { class PythonRequestsCodeGen {
final String kTemplateStart = """import requests final String kTemplateStart = """import requests
{% if isFormDataRequest %}import mimetypes
from codecs import encode
{% endif %}
url = '{{url}}' url = '{{url}}'
"""; """;
@ -37,6 +39,8 @@ payload = {{body}}
headers = {{headers}} headers = {{headers}}
"""; """;
String kTemplateFormHeaderContentType = '''
multipart/form-data; boundary={{boundary}}''';
int kHeadersPadding = 10; int kHeadersPadding = 10;
@ -45,6 +49,34 @@ headers = {{headers}}
response = requests.{{method}}(url response = requests.{{method}}(url
"""; """;
final String kStringFormDataBody = r'''
def build_data_list(fields):
dataList = []
for field in fields:
name = field.get('name', '')
value = field.get('value', '')
type_ = field.get('type', 'text')
dataList.append(encode('--{{boundary}}'))
if type_ == 'text':
dataList.append(encode(f'Content-Disposition: form-data; name="{name}"'))
dataList.append(encode('Content-Type: text/plain'))
dataList.append(encode(''))
dataList.append(encode(value))
elif type_ == 'file':
dataList.append(encode(f'Content-Disposition: form-data; name="{name}"; filename="{value}"'))
dataList.append(encode(f'Content-Type: {mimetypes.guess_type(value)[0] or "application/octet-stream"}'))
dataList.append(encode(''))
dataList.append(open(value, 'rb').read())
dataList.append(encode('--{{boundary}}--'))
dataList.append(encode(''))
return dataList
dataList = build_data_list({{fields_list}})
payload = b'\r\n'.join(dataList)
''';
String kStringRequestParams = """, params=params"""; String kStringRequestParams = """, params=params""";
String kStringRequestBody = """, data=payload"""; String kStringRequestBody = """, data=payload""";
@ -69,6 +101,7 @@ print('Response Body:', response.text)
bool hasHeaders = false; bool hasHeaders = false;
bool hasBody = false; bool hasBody = false;
bool hasJsonBody = false; bool hasJsonBody = false;
String uuid = getNewUuid();
String url = requestModel.url; String url = requestModel.url;
if (!url.contains("://") && url.isNotEmpty) { if (!url.contains("://") && url.isNotEmpty) {
@ -82,7 +115,10 @@ print('Response Body:', response.text)
Uri? uri = rec.$1; Uri? uri = rec.$1;
if (uri != null) { if (uri != null) {
var templateStartUrl = jj.Template(kTemplateStart); var templateStartUrl = jj.Template(kTemplateStart);
result += templateStartUrl.render({"url": stripUriParams(uri)}); result += templateStartUrl.render({
"url": stripUriParams(uri),
'isFormDataRequest': requestModel.isFormDataRequest
});
if (uri.hasQuery) { if (uri.hasQuery) {
var params = uri.queryParameters; var params = uri.queryParameters;
@ -115,6 +151,13 @@ print('Response Body:', response.text)
var headersList = requestModel.enabledRequestHeaders; var headersList = requestModel.enabledRequestHeaders;
if (headersList != null || hasBody) { if (headersList != null || hasBody) {
var headers = requestModel.enabledHeadersMap; var headers = requestModel.enabledHeadersMap;
if (requestModel.isFormDataRequest) {
var formHeaderTemplate =
jj.Template(kTemplateFormHeaderContentType);
headers[HttpHeaders.contentTypeHeader] = formHeaderTemplate.render({
"boundary": uuid,
});
}
if (headers.isNotEmpty || hasBody) { if (headers.isNotEmpty || hasBody) {
hasHeaders = true; hasHeaders = true;
if (hasBody) { if (hasBody) {
@ -127,7 +170,15 @@ print('Response Body:', response.text)
result += templateHeaders.render({"headers": headersString}); result += templateHeaders.render({"headers": headersString});
} }
} }
if (requestModel.isFormDataRequest) {
var formDataBodyData = jj.Template(kStringFormDataBody);
result += formDataBodyData.render(
{
"fields_list": json.encode(requestModel.formDataMapList),
"boundary": uuid,
},
);
}
var templateRequest = jj.Template(kTemplateRequest); var templateRequest = jj.Template(kTemplateRequest);
result += templateRequest.render({ result += templateRequest.render({
"method": method.name.toLowerCase(), "method": method.name.toLowerCase(),
@ -137,15 +188,15 @@ print('Response Body:', response.text)
result += kStringRequestParams; result += kStringRequestParams;
} }
if (hasBody) { if (hasBody || requestModel.isFormDataRequest) {
result += kStringRequestBody; result += kStringRequestBody;
} }
if (hasJsonBody) { if (hasJsonBody || requestModel.isFormDataRequest) {
result += kStringRequestJson; result += kStringRequestJson;
} }
if (hasHeaders) { if (hasHeaders || requestModel.isFormDataRequest) {
result += kStringRequestHeaders; result += kStringRequestHeaders;
} }

View File

@ -48,6 +48,10 @@ const kHintOpacity = 0.6;
const kForegroundOpacity = 0.05; const kForegroundOpacity = 0.05;
const kTextStyleButton = TextStyle(fontWeight: FontWeight.bold); const kTextStyleButton = TextStyle(fontWeight: FontWeight.bold);
const kFormDataButtonLabelTextStyle = TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
);
const kBorderRadius8 = BorderRadius.all(Radius.circular(8)); const kBorderRadius8 = BorderRadius.all(Radius.circular(8));
final kBorderRadius10 = BorderRadius.circular(10); final kBorderRadius10 = BorderRadius.circular(10);
@ -57,6 +61,7 @@ const kP1 = EdgeInsets.all(1);
const kP5 = EdgeInsets.all(5); const kP5 = EdgeInsets.all(5);
const kP8 = EdgeInsets.all(8); const kP8 = EdgeInsets.all(8);
const kPs8 = EdgeInsets.only(left: 8); const kPs8 = EdgeInsets.only(left: 8);
const kPs2 = EdgeInsets.only(left: 2);
const kPh20v5 = EdgeInsets.symmetric(horizontal: 20, vertical: 5); const kPh20v5 = EdgeInsets.symmetric(horizontal: 20, vertical: 5);
const kPh20v10 = EdgeInsets.symmetric(horizontal: 20, vertical: 10); const kPh20v10 = EdgeInsets.symmetric(horizontal: 20, vertical: 10);
const kP10 = EdgeInsets.all(10); const kP10 = EdgeInsets.all(10);
@ -85,7 +90,7 @@ const kP8CollectionPane = EdgeInsets.only(
// bottom: 8.0, // bottom: 8.0,
); );
const kPr8CollectionPane = EdgeInsets.only(right: 8.0); const kPr8CollectionPane = EdgeInsets.only(right: 8.0);
const kpsV5 = EdgeInsets.symmetric(vertical: 2);
const kHSpacer4 = SizedBox(width: 4); const kHSpacer4 = SizedBox(width: 4);
const kHSpacer5 = SizedBox(width: 5); const kHSpacer5 = SizedBox(width: 5);
const kHSpacer10 = SizedBox(width: 10); const kHSpacer10 = SizedBox(width: 10);
@ -237,7 +242,9 @@ enum RequestItemMenuOption { edit, delete, duplicate }
enum HTTPVerb { get, head, post, put, patch, delete } enum HTTPVerb { get, head, post, put, patch, delete }
enum ContentType { json, text } enum ContentType { json, text, formdata }
enum FormDataType { text, file }
const kSupportedUriSchemes = ["https", "http"]; const kSupportedUriSchemes = ["https", "http"];
const kDefaultUriScheme = "https"; const kDefaultUriScheme = "https";
@ -308,6 +315,7 @@ const kSubTypeDefaultViewOptions = 'all';
const kContentTypeMap = { const kContentTypeMap = {
ContentType.json: "$kTypeApplication/$kSubTypeJson", ContentType.json: "$kTypeApplication/$kSubTypeJson",
ContentType.text: "$kTypeText/$kSubTypePlain", ContentType.text: "$kTypeText/$kSubTypePlain",
ContentType.formdata: "multipart/form-data",
}; };
enum ResponseBodyView { preview, code, raw, none } enum ResponseBodyView { preview, code, raw, none }

View File

@ -0,0 +1,23 @@
import 'package:apidash/consts.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'form_data_model.freezed.dart';
part 'form_data_model.g.dart';
@freezed
class FormDataModel with _$FormDataModel {
const factory FormDataModel({
required String name,
required String value,
required FormDataType type,
}) = _FormDataModel;
factory FormDataModel.fromJson(Map<String, Object?> json) =>
_$FormDataModelFromJson(json);
}
const kFormDataEmptyModel = FormDataModel(
name: "",
value: "",
type: FormDataType.text,
);

View File

@ -0,0 +1,187 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'form_data_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
FormDataModel _$FormDataModelFromJson(Map<String, dynamic> json) {
return _FormDataModel.fromJson(json);
}
/// @nodoc
mixin _$FormDataModel {
String get name => throw _privateConstructorUsedError;
String get value => throw _privateConstructorUsedError;
FormDataType get type => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$FormDataModelCopyWith<FormDataModel> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $FormDataModelCopyWith<$Res> {
factory $FormDataModelCopyWith(
FormDataModel value, $Res Function(FormDataModel) then) =
_$FormDataModelCopyWithImpl<$Res, FormDataModel>;
@useResult
$Res call({String name, String value, FormDataType type});
}
/// @nodoc
class _$FormDataModelCopyWithImpl<$Res, $Val extends FormDataModel>
implements $FormDataModelCopyWith<$Res> {
_$FormDataModelCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? name = null,
Object? value = null,
Object? type = null,
}) {
return _then(_value.copyWith(
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
value: null == value
? _value.value
: value // ignore: cast_nullable_to_non_nullable
as String,
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as FormDataType,
) as $Val);
}
}
/// @nodoc
abstract class _$$FormDataModelImplCopyWith<$Res>
implements $FormDataModelCopyWith<$Res> {
factory _$$FormDataModelImplCopyWith(
_$FormDataModelImpl value, $Res Function(_$FormDataModelImpl) then) =
__$$FormDataModelImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String name, String value, FormDataType type});
}
/// @nodoc
class __$$FormDataModelImplCopyWithImpl<$Res>
extends _$FormDataModelCopyWithImpl<$Res, _$FormDataModelImpl>
implements _$$FormDataModelImplCopyWith<$Res> {
__$$FormDataModelImplCopyWithImpl(
_$FormDataModelImpl _value, $Res Function(_$FormDataModelImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? name = null,
Object? value = null,
Object? type = null,
}) {
return _then(_$FormDataModelImpl(
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
value: null == value
? _value.value
: value // ignore: cast_nullable_to_non_nullable
as String,
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as FormDataType,
));
}
}
/// @nodoc
@JsonSerializable()
class _$FormDataModelImpl implements _FormDataModel {
const _$FormDataModelImpl(
{required this.name, required this.value, required this.type});
factory _$FormDataModelImpl.fromJson(Map<String, dynamic> json) =>
_$$FormDataModelImplFromJson(json);
@override
final String name;
@override
final String value;
@override
final FormDataType type;
@override
String toString() {
return 'FormDataModel(name: $name, value: $value, type: $type)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$FormDataModelImpl &&
(identical(other.name, name) || other.name == name) &&
(identical(other.value, value) || other.value == value) &&
(identical(other.type, type) || other.type == type));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, name, value, type);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$FormDataModelImplCopyWith<_$FormDataModelImpl> get copyWith =>
__$$FormDataModelImplCopyWithImpl<_$FormDataModelImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$FormDataModelImplToJson(
this,
);
}
}
abstract class _FormDataModel implements FormDataModel {
const factory _FormDataModel(
{required final String name,
required final String value,
required final FormDataType type}) = _$FormDataModelImpl;
factory _FormDataModel.fromJson(Map<String, dynamic> json) =
_$FormDataModelImpl.fromJson;
@override
String get name;
@override
String get value;
@override
FormDataType get type;
@override
@JsonKey(ignore: true)
_$$FormDataModelImplCopyWith<_$FormDataModelImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,26 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'form_data_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$FormDataModelImpl _$$FormDataModelImplFromJson(Map<String, dynamic> json) =>
_$FormDataModelImpl(
name: json['name'] as String,
value: json['value'] as String,
type: $enumDecode(_$FormDataTypeEnumMap, json['type']),
);
Map<String, dynamic> _$$FormDataModelImplToJson(_$FormDataModelImpl instance) =>
<String, dynamic>{
'name': instance.name,
'value': instance.value,
'type': _$FormDataTypeEnumMap[instance.type]!,
};
const _$FormDataTypeEnumMap = {
FormDataType.text: 'text',
FormDataType.file: 'file',
};

View File

@ -2,3 +2,4 @@ export 'name_value_model.dart';
export 'request_model.dart'; export 'request_model.dart';
export 'response_model.dart'; export 'response_model.dart';
export 'settings_model.dart'; export 'settings_model.dart';
export 'form_data_model.dart';

View File

@ -1,10 +1,14 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:apidash/consts.dart'; import '../utils/utils.dart'
import 'package:apidash/utils/utils.dart' show
show mapToRows, rowsToMap, getEnabledRows; mapListToFormDataModelRows,
import 'name_value_model.dart'; rowsToFormDataMapList,
import 'response_model.dart'; mapToRows,
rowsToMap,
getEnabledRows;
import '../consts.dart';
import 'models.dart';
@immutable @immutable
class RequestModel { class RequestModel {
@ -21,6 +25,7 @@ class RequestModel {
this.isParamEnabledList, this.isParamEnabledList,
this.requestBodyContentType = ContentType.json, this.requestBodyContentType = ContentType.json,
this.requestBody, this.requestBody,
this.requestFormDataList,
this.responseStatus, this.responseStatus,
this.message, this.message,
this.responseModel, this.responseModel,
@ -38,6 +43,7 @@ class RequestModel {
final List<bool>? isParamEnabledList; final List<bool>? isParamEnabledList;
final ContentType requestBodyContentType; final ContentType requestBodyContentType;
final String? requestBody; final String? requestBody;
final List<FormDataModel>? requestFormDataList;
final int? responseStatus; final int? responseStatus;
final String? message; final String? message;
final ResponseModel? responseModel; final ResponseModel? responseModel;
@ -54,6 +60,10 @@ class RequestModel {
Map<String, String> get headersMap => rowsToMap(requestHeaders) ?? {}; Map<String, String> get headersMap => rowsToMap(requestHeaders) ?? {};
Map<String, String> get paramsMap => rowsToMap(requestParams) ?? {}; Map<String, String> get paramsMap => rowsToMap(requestParams) ?? {};
List<Map<String, dynamic>> get formDataMapList =>
rowsToFormDataMapList(requestFormDataList) ?? [];
bool get isFormDataRequest => requestBodyContentType == ContentType.formdata;
bool get hasContentTypeHeader => enabledHeadersMap.keys bool get hasContentTypeHeader => enabledHeadersMap.keys
.any((k) => k.toLowerCase() == HttpHeaders.contentTypeHeader); .any((k) => k.toLowerCase() == HttpHeaders.contentTypeHeader);
@ -74,6 +84,8 @@ class RequestModel {
isParamEnabledList != null ? [...isParamEnabledList!] : null, isParamEnabledList != null ? [...isParamEnabledList!] : null,
requestBodyContentType: requestBodyContentType, requestBodyContentType: requestBodyContentType,
requestBody: requestBody, requestBody: requestBody,
requestFormDataList:
requestFormDataList != null ? [...requestFormDataList!] : null,
); );
} }
@ -90,6 +102,7 @@ class RequestModel {
List<bool>? isParamEnabledList, List<bool>? isParamEnabledList,
ContentType? requestBodyContentType, ContentType? requestBodyContentType,
String? requestBody, String? requestBody,
List<FormDataModel>? requestFormDataList,
int? responseStatus, int? responseStatus,
String? message, String? message,
ResponseModel? responseModel, ResponseModel? responseModel,
@ -112,6 +125,7 @@ class RequestModel {
requestBodyContentType: requestBodyContentType:
requestBodyContentType ?? this.requestBodyContentType, requestBodyContentType ?? this.requestBodyContentType,
requestBody: requestBody ?? this.requestBody, requestBody: requestBody ?? this.requestBody,
requestFormDataList: requestFormDataList ?? this.requestFormDataList,
responseStatus: responseStatus ?? this.responseStatus, responseStatus: responseStatus ?? this.responseStatus,
message: message ?? this.message, message: message ?? this.message,
responseModel: responseModel ?? this.responseModel, responseModel: responseModel ?? this.responseModel,
@ -143,9 +157,11 @@ class RequestModel {
requestBodyContentType = kDefaultContentType; requestBodyContentType = kDefaultContentType;
} }
final requestBody = data["requestBody"] as String?; final requestBody = data["requestBody"] as String?;
final requestFormDataList = data["requestFormDataList"];
final responseStatus = data["responseStatus"] as int?; final responseStatus = data["responseStatus"] as int?;
final message = data["message"] as String?; final message = data["message"] as String?;
final responseModelJson = data["responseModel"]; final responseModelJson = data["responseModel"];
if (responseModelJson != null) { if (responseModelJson != null) {
responseModel = responseModel =
ResponseModel.fromJson(Map<String, dynamic>.from(responseModelJson)); ResponseModel.fromJson(Map<String, dynamic>.from(responseModelJson));
@ -170,6 +186,9 @@ class RequestModel {
isParamEnabledList: isParamEnabledList, isParamEnabledList: isParamEnabledList,
requestBodyContentType: requestBodyContentType, requestBodyContentType: requestBodyContentType,
requestBody: requestBody, requestBody: requestBody,
requestFormDataList: requestFormDataList != null
? mapListToFormDataModelRows(List<Map>.from(requestFormDataList))
: null,
responseStatus: responseStatus, responseStatus: responseStatus,
message: message, message: message,
responseModel: responseModel, responseModel: responseModel,
@ -189,6 +208,7 @@ class RequestModel {
"isParamEnabledList": isParamEnabledList, "isParamEnabledList": isParamEnabledList,
"requestBodyContentType": requestBodyContentType.name, "requestBodyContentType": requestBodyContentType.name,
"requestBody": requestBody, "requestBody": requestBody,
"requestFormDataList": rowsToFormDataMapList(requestFormDataList),
"responseStatus": includeResponse ? responseStatus : null, "responseStatus": includeResponse ? responseStatus : null,
"message": includeResponse ? message : null, "message": includeResponse ? message : null,
"responseModel": includeResponse ? responseModel?.toJson() : null, "responseModel": includeResponse ? responseModel?.toJson() : null,
@ -210,6 +230,7 @@ class RequestModel {
"Enabled Params: ${isParamEnabledList.toString()}", "Enabled Params: ${isParamEnabledList.toString()}",
"Request Body Content Type: ${requestBodyContentType.toString()}", "Request Body Content Type: ${requestBodyContentType.toString()}",
"Request Body: ${requestBody.toString()}", "Request Body: ${requestBody.toString()}",
"Request FormData: ${requestFormDataList.toString()}",
"Response Status: $responseStatus", "Response Status: $responseStatus",
"Response Message: $message", "Response Message: $message",
"Response: ${responseModel.toString()}" "Response: ${responseModel.toString()}"
@ -232,6 +253,7 @@ class RequestModel {
listEquals(other.isParamEnabledList, isParamEnabledList) && listEquals(other.isParamEnabledList, isParamEnabledList) &&
other.requestBodyContentType == requestBodyContentType && other.requestBodyContentType == requestBodyContentType &&
other.requestBody == requestBody && other.requestBody == requestBody &&
other.requestFormDataList == requestFormDataList &&
other.responseStatus == responseStatus && other.responseStatus == responseStatus &&
other.message == message && other.message == message &&
other.responseModel == responseModel; other.responseModel == responseModel;
@ -253,6 +275,7 @@ class RequestModel {
isParamEnabledList, isParamEnabledList,
requestBodyContentType, requestBodyContentType,
requestBody, requestBody,
requestFormDataList,
responseStatus, responseStatus,
message, message,
responseModel, responseModel,

View File

@ -5,6 +5,7 @@ import '../models/models.dart';
import '../services/services.dart' show hiveHandler, HiveHandler, request; import '../services/services.dart' show hiveHandler, HiveHandler, request;
import '../utils/utils.dart' show uuid, collectionToHAR; import '../utils/utils.dart' show uuid, collectionToHAR;
import '../consts.dart'; import '../consts.dart';
import 'package:http/http.dart' as http;
final activeIdStateProvider = StateProvider<String?>((ref) => null); final activeIdStateProvider = StateProvider<String?>((ref) => null);
@ -127,6 +128,7 @@ class CollectionStateNotifier
List<bool>? isParamEnabledList, List<bool>? isParamEnabledList,
ContentType? requestBodyContentType, ContentType? requestBodyContentType,
String? requestBody, String? requestBody,
List<FormDataModel>? requestFormDataList,
int? responseStatus, int? responseStatus,
String? message, String? message,
ResponseModel? responseModel, ResponseModel? responseModel,
@ -143,9 +145,11 @@ class CollectionStateNotifier
isParamEnabledList: isParamEnabledList, isParamEnabledList: isParamEnabledList,
requestBodyContentType: requestBodyContentType, requestBodyContentType: requestBodyContentType,
requestBody: requestBody, requestBody: requestBody,
requestFormDataList: requestFormDataList,
responseStatus: responseStatus, responseStatus: responseStatus,
message: message, message: message,
responseModel: responseModel); responseModel: responseModel);
//print(newModel);
var map = {...state!}; var map = {...state!};
map[id] = newModel; map[id] = newModel;
state = map; state = map;
@ -156,10 +160,13 @@ class CollectionStateNotifier
ref.read(codePaneVisibleStateProvider.notifier).state = false; ref.read(codePaneVisibleStateProvider.notifier).state = false;
final defaultUriScheme = final defaultUriScheme =
ref.read(settingsProvider.select((value) => value.defaultUriScheme)); ref.read(settingsProvider.select((value) => value.defaultUriScheme));
RequestModel requestModel = state![id]!; RequestModel requestModel = state![id]!;
var responseRec = (http.Response?, Duration?, String?)? responseRec = await request(
await request(requestModel, defaultUriScheme: defaultUriScheme); requestModel,
defaultUriScheme: defaultUriScheme,
isMultiPartRequest:
requestModel.requestBodyContentType == ContentType.formdata,
);
late final RequestModel newRequestModel; late final RequestModel newRequestModel;
if (responseRec.$1 == null) { if (responseRec.$1 == null) {
newRequestModel = requestModel.copyWith( newRequestModel = requestModel.copyWith(

View File

@ -18,6 +18,10 @@ class _EditRequestBodyState extends ConsumerState<EditRequestBody> {
final requestModel = ref final requestModel = ref
.read(collectionStateNotifierProvider.notifier) .read(collectionStateNotifierProvider.notifier)
.getRequestModel(activeId!); .getRequestModel(activeId!);
ContentType? requestBodyStateWatcher = (ref
.watch(collectionStateNotifierProvider)![activeId]
?.requestBodyContentType) ??
ContentType.values.first;
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
@ -38,7 +42,9 @@ class _EditRequestBodyState extends ConsumerState<EditRequestBody> {
), ),
), ),
Expanded( Expanded(
child: TextFieldEditor( child: requestBodyStateWatcher == ContentType.formdata
? const FormDataWidget()
: TextFieldEditor(
key: Key("$activeId-body"), key: Key("$activeId-body"),
fieldKey: "$activeId-body-editor", fieldKey: "$activeId-body-editor",
initialValue: requestModel?.requestBody, initialValue: requestModel?.requestBody,

View File

@ -9,6 +9,7 @@ import 'package:apidash/consts.dart';
Future<(http.Response?, Duration?, String?)> request( Future<(http.Response?, Duration?, String?)> request(
RequestModel requestModel, { RequestModel requestModel, {
String defaultUriScheme = kDefaultUriScheme, String defaultUriScheme = kDefaultUriScheme,
bool isMultiPartRequest = false,
}) async { }) async {
(Uri?, String?) uriRec = getValidRequestUri( (Uri?, String?) uriRec = getValidRequestUri(
requestModel.url, requestModel.url,
@ -35,6 +36,31 @@ Future<(http.Response?, Duration?, String?)> request(
} }
} }
Stopwatch stopwatch = Stopwatch()..start(); Stopwatch stopwatch = Stopwatch()..start();
if (isMultiPartRequest) {
var multiPartRequest = http.MultipartRequest(
requestModel.method.name.toUpperCase(),
requestUrl,
);
multiPartRequest.headers.addAll(headers);
for (FormDataModel formData
in (requestModel.requestFormDataList ?? [])) {
if (formData.type == FormDataType.text) {
multiPartRequest.fields.addAll({formData.name: formData.value});
} else {
multiPartRequest.files.add(
await http.MultipartFile.fromPath(
formData.name,
formData.value,
),
);
}
}
http.StreamedResponse multiPartResponse = await multiPartRequest.send();
stopwatch.stop();
http.Response convertedMultiPartResponse =
await convertStreamedResponse(multiPartResponse);
return (convertedMultiPartResponse, stopwatch.elapsed, null);
}
switch (requestModel.method) { switch (requestModel.method) {
case HTTPVerb.get: case HTTPVerb.get:
response = await http.get(requestUrl, headers: headers); response = await http.get(requestUrl, headers: headers);

View File

@ -2,6 +2,7 @@ import 'dart:typed_data';
import 'dart:convert'; import 'dart:convert';
import '../models/models.dart'; import '../models/models.dart';
import '../consts.dart'; import '../consts.dart';
import 'package:http/http.dart' as http;
String humanizeDuration(Duration? duration) { String humanizeDuration(Duration? duration) {
if (duration == null) { if (duration == null) {
@ -89,6 +90,43 @@ List<NameValueModel>? mapToRows(Map<String, String>? kvMap) {
return finalRows; return finalRows;
} }
List<Map<String, dynamic>>? rowsToFormDataMapList(
List<FormDataModel>? kvRows,
) {
if (kvRows == null) {
return null;
}
List<Map<String, dynamic>> finalMap = kvRows
.map((FormDataModel formData) => {
"name": formData.name,
"value": formData.value,
"type": formData.type.name,
})
.toList();
return finalMap;
}
List<FormDataModel>? mapListToFormDataModelRows(List<Map>? kvMap) {
if (kvMap == null) {
return null;
}
List<FormDataModel> finalRows = kvMap.map(
(formData) {
return FormDataModel(
name: formData["name"],
value: formData["value"],
type: getFormDataType(formData["type"]),
);
},
).toList();
return finalRows;
}
FormDataType getFormDataType(String? type) {
return FormDataType.values.firstWhere((element) => element.name == type,
orElse: () => FormDataType.text);
}
Uint8List? stringToBytes(String? text) { Uint8List? stringToBytes(String? text) {
if (text == null) { if (text == null) {
return null; return null;
@ -110,6 +148,23 @@ Uint8List jsonMapToBytes(Map<String, dynamic>? map) {
} }
} }
Future<http.Response> convertStreamedResponse(
http.StreamedResponse streamedResponse,
) async {
Uint8List bodyBytes = await streamedResponse.stream.toBytes();
http.Response response = http.Response.bytes(
bodyBytes,
streamedResponse.statusCode,
headers: streamedResponse.headers,
persistentConnection: streamedResponse.persistentConnection,
reasonPhrase: streamedResponse.reasonPhrase,
request: streamedResponse.request,
);
return response;
}
List<NameValueModel>? getEnabledRows( List<NameValueModel>? getEnabledRows(
List<NameValueModel>? rows, List<bool>? isRowEnabledList) { List<NameValueModel>? rows, List<bool>? isRowEnabledList) {
if (rows == null || isRowEnabledList == null) { if (rows == null || isRowEnabledList == null) {

View File

@ -1,6 +1,8 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:apidash/consts.dart'; import 'package:apidash/consts.dart';
import 'package:apidash/models/form_data_model.dart';
import 'package:apidash/utils/convert_utils.dart';
import 'package:apidash/utils/utils.dart' show getValidRequestUri; import 'package:apidash/utils/utils.dart' show getValidRequestUri;
import 'package:apidash/models/models.dart' show RequestModel; import 'package:apidash/models/models.dart' show RequestModel;
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
@ -153,7 +155,9 @@ Map<String, dynamic> requestModelToHARJsonRequest(
} }
} }
} }
if (requestModel.isFormDataRequest) {
json["formData"] = requestModel.formDataMapList;
}
if (exportMode) { if (exportMode) {
json["comment"] = ""; json["comment"] = "";
json["cookies"] = []; json["cookies"] = [];

View File

@ -93,6 +93,56 @@ class DropdownButtonContentType extends StatelessWidget {
} }
} }
class DropdownButtonFormData extends StatefulWidget {
const DropdownButtonFormData({
super.key,
this.formDataType,
this.onChanged,
});
final FormDataType? formDataType;
final void Function(FormDataType?)? onChanged;
@override
State<DropdownButtonFormData> createState() => _DropdownButtonFormData();
}
class _DropdownButtonFormData extends State<DropdownButtonFormData> {
@override
Widget build(BuildContext context) {
final surfaceColor = Theme.of(context).colorScheme.surface;
return DropdownButton<FormDataType>(
dropdownColor: surfaceColor,
focusColor: surfaceColor,
value: widget.formDataType,
icon: const Icon(
Icons.unfold_more_rounded,
size: 16,
),
elevation: 4,
style: kCodeStyle.copyWith(
color: Theme.of(context).colorScheme.primary,
),
underline: const IgnorePointer(),
onChanged: widget.onChanged,
borderRadius: kBorderRadius12,
items: FormDataType.values
.map<DropdownMenuItem<FormDataType>>((FormDataType value) {
return DropdownMenuItem<FormDataType>(
value: value,
child: Padding(
padding: kPs8,
child: Text(
value.name,
style: kTextStyleButton,
),
),
);
}).toList(),
);
}
}
class DropdownButtonCodegenLanguage extends StatelessWidget { class DropdownButtonCodegenLanguage extends StatelessWidget {
const DropdownButtonCodegenLanguage({ const DropdownButtonCodegenLanguage({
super.key, super.key,

View File

@ -0,0 +1,81 @@
import 'package:apidash/consts.dart';
import 'package:apidash/widgets/widgets.dart';
import 'package:flutter/material.dart';
class FormDataField extends StatefulWidget {
const FormDataField({
super.key,
required this.keyId,
this.initialValue,
this.hintText,
this.onChanged,
this.colorScheme,
this.formDataType,
this.onFormDataTypeChanged,
});
final String keyId;
final String? initialValue;
final String? hintText;
final void Function(String)? onChanged;
final ColorScheme? colorScheme;
final FormDataType? formDataType;
final void Function(FormDataType?)? onFormDataTypeChanged;
@override
State<FormDataField> createState() => _FormDataFieldState();
}
class _FormDataFieldState extends State<FormDataField> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
var colorScheme = widget.colorScheme ?? Theme.of(context).colorScheme;
return Row(
children: [
Expanded(
flex: 1,
child: TextFormField(
initialValue: widget.initialValue,
key: Key(widget.keyId),
style: kCodeStyle.copyWith(
color: colorScheme.onSurface,
),
decoration: InputDecoration(
hintStyle: kCodeStyle.copyWith(
color: colorScheme.outline.withOpacity(
kHintOpacity,
),
),
hintText: widget.hintText,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: colorScheme.primary.withOpacity(
kHintOpacity,
),
),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: colorScheme.surfaceVariant,
),
),
suffixIcon: DropdownButtonFormData(
formDataType: widget.formDataType,
onChanged: (p0) {
if (widget.onFormDataTypeChanged != null) {
widget.onFormDataTypeChanged!(p0);
}
},
)),
onChanged: widget.onChanged,
),
),
],
);
}
}

View File

@ -0,0 +1,223 @@
import 'dart:math';
import 'package:apidash/consts.dart';
import 'package:apidash/models/form_data_model.dart';
import 'package:apidash/models/models.dart';
import 'package:apidash/providers/collection_providers.dart';
import 'package:apidash/widgets/form_data_field.dart';
import 'package:apidash/widgets/textfields.dart';
import 'package:davi/davi.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class FormDataWidget extends ConsumerStatefulWidget {
const FormDataWidget({super.key});
@override
ConsumerState<FormDataWidget> createState() => _FormDataBodyState();
}
class _FormDataBodyState extends ConsumerState<FormDataWidget> {
late int seed;
final random = Random.secure();
late List<FormDataModel> rows;
@override
void initState() {
super.initState();
seed = random.nextInt(kRandMax);
}
@override
Widget build(BuildContext context) {
final activeId = ref.watch(activeIdStateProvider);
var formRows = ref.read(activeRequestModelProvider)?.requestFormDataList;
rows =
formRows == null || formRows.isEmpty ? [kFormDataEmptyModel] : formRows;
DaviModel<FormDataModel> daviModelRows = DaviModel<FormDataModel>(
rows: rows,
columns: [
DaviColumn(
cellPadding: kpsV5,
name: 'Key',
grow: 4,
cellBuilder: (_, row) {
int idx = row.index;
return Theme(
data: Theme.of(context),
child: FormDataField(
keyId: "$activeId-$idx-form-v-$seed",
initialValue: rows[idx].name,
hintText: " Add Key",
onChanged: (value) {
rows[idx] = rows[idx].copyWith(
name: value,
);
_onFieldChange(activeId!);
},
colorScheme: Theme.of(context).colorScheme,
formDataType: rows[idx].type,
onFormDataTypeChanged: (value) {
rows[idx] = rows[idx].copyWith(
type: value ?? FormDataType.text,
);
rows[idx] = rows[idx].copyWith(value: "");
setState(() {});
_onFieldChange(activeId!);
},
),
);
},
sortable: false,
),
DaviColumn(
width: 40,
cellPadding: kpsV5,
cellAlignment: Alignment.center,
cellBuilder: (_, row) {
return Text(
"=",
style: kCodeStyle,
);
},
),
DaviColumn(
name: 'Value',
grow: 4,
cellPadding: kpsV5,
cellBuilder: (_, row) {
int idx = row.index;
return rows[idx].type == FormDataType.file
? Align(
alignment: Alignment.centerLeft,
child: Row(
children: [
Expanded(
child: Theme(
data: Theme.of(context),
child: ElevatedButton.icon(
icon: const Icon(
Icons.snippet_folder_rounded,
size: 20,
),
style: ButtonStyle(
shape: MaterialStatePropertyAll(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
),
),
onPressed: () async {
FilePickerResult? pickedResult =
await FilePicker.platform.pickFiles();
if (pickedResult != null &&
pickedResult.files.isNotEmpty &&
pickedResult.files.first.path != null) {
rows[idx] = rows[idx].copyWith(
value: pickedResult.files.first.path!,
);
setState(() {});
_onFieldChange(activeId!);
}
},
label: Text(
(rows[idx].type == FormDataType.file &&
rows[idx].value.isNotEmpty)
? rows[idx].value.toString()
: "Select File",
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
style: kFormDataButtonLabelTextStyle,
),
),
),
),
],
),
)
: CellField(
keyId: "$activeId-$idx-form-v-$seed",
initialValue: rows[idx].value,
hintText: " Add Value",
onChanged: (value) {
rows[idx] = rows[idx].copyWith(value: value);
_onFieldChange(activeId!);
},
colorScheme: Theme.of(context).colorScheme,
);
},
sortable: false,
),
DaviColumn(
pinStatus: PinStatus.none,
width: 30,
cellBuilder: (_, row) {
return InkWell(
child: Theme.of(context).brightness == Brightness.dark
? kIconRemoveDark
: kIconRemoveLight,
onTap: () {
seed = random.nextInt(kRandMax);
if (rows.length == 1) {
setState(() {
rows = [kFormDataEmptyModel];
});
} else {
rows.removeAt(row.index);
}
_onFieldChange(activeId!);
setState(() {});
},
);
},
),
],
);
return Stack(
children: [
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: kBorderRadius12,
),
margin: kP10,
child: Column(
children: [
Expanded(
child: DaviTheme(
data: kTableThemeData,
child: Davi<FormDataModel>(daviModelRows),
),
),
],
),
),
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(bottom: 30),
child: ElevatedButton.icon(
onPressed: () {
setState(() {
rows.add(kFormDataEmptyModel);
});
_onFieldChange(activeId!);
},
icon: const Icon(Icons.add),
label: const Text(
"Add Form Data",
style: kTextStyleButton,
),
),
),
),
],
);
}
void _onFieldChange(String activeId) {
ref.read(collectionStateNotifierProvider.notifier).update(
activeId,
requestFormDataList: rows,
);
}
}

View File

@ -22,3 +22,4 @@ export 'textfields.dart';
export 'texts.dart'; export 'texts.dart';
export 'uint8_audio_player.dart'; export 'uint8_audio_player.dart';
export 'window_caption.dart'; export 'window_caption.dart';
export 'form_data_widget.dart';

View File

@ -48,6 +48,7 @@ dependencies:
url: https://github.com/foss42/json_data_explorer.git url: https://github.com/foss42/json_data_explorer.git
version: ^0.1.1 version: ^0.1.1
scrollable_positioned_list: ^0.2.3 scrollable_positioned_list: ^0.2.3
file_picker: ^6.1.1
flutter_svg: ^2.0.9 flutter_svg: ^2.0.9
vector_graphics_compiler: ^1.1.9+1 vector_graphics_compiler: ^1.1.9+1
code_builder: ^4.9.0 code_builder: ^4.9.0

View File

@ -113,6 +113,7 @@ void main() {
"requestBody": '''{ "requestBody": '''{
"text":"WORLD" "text":"WORLD"
}''', }''',
'requestFormDataList': null,
'responseStatus': null, 'responseStatus': null,
'message': null, 'message': null,
'responseModel': null 'responseModel': null
@ -147,6 +148,7 @@ void main() {
"Enabled Params: null", "Enabled Params: null",
"Request Body Content Type: ContentType.json", "Request Body Content Type: ContentType.json",
'Request Body: {\n"text":"WORLD"\n}', 'Request Body: {\n"text":"WORLD"\n}',
'Request FormData: null',
"Response Status: null", "Response Status: null",
"Response Message: null", "Response Message: null",
"Response: null" "Response: null"