diff --git a/lib/codegen/dart/dio.dart b/lib/codegen/dart/dio.dart index 1c6a42d3..c4a7ed8e 100644 --- a/lib/codegen/dart/dio.dart +++ b/lib/codegen/dart/dio.dart @@ -1,8 +1,8 @@ -import 'package:apidash/consts.dart'; -import 'package:apidash/models/request_model.dart' show RequestModel; +import 'dart:convert'; import 'package:code_builder/code_builder.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'; class DartDioCodeGen { @@ -22,6 +22,7 @@ class DartDioCodeGen { headers: requestModel.enabledHeadersMap, body: requestModel.requestBody, contentType: requestModel.requestBodyContentType, + formData: requestModel.formDataMapList, ); return next; } catch (e) { @@ -36,6 +37,7 @@ class DartDioCodeGen { required Map headers, required String? body, required ContentType contentType, + required List> formData, }) { final sbf = StringBuffer(); final emitter = DartEmitter(); @@ -54,9 +56,22 @@ class DartDioCodeGen { literalMap(headers.map((key, value) => MapEntry(key, value))), ); } - + final multiPartList = Code(''' + final List> 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; - if (kMethodsWithBody.contains(method) && (body?.isNotEmpty ?? false)) { + if ((kMethodsWithBody.contains(method) && (body?.isNotEmpty ?? false) || + contentType == ContentType.formdata)) { final strContent = CodeExpression(Code('r\'\'\'$body\'\'\'')); switch (contentType) { // dio dosen't need pass `content-type` header when body is json or plain text @@ -68,6 +83,8 @@ class DartDioCodeGen { case ContentType.text: dataExp = declareFinal('data').assign(strContent); // 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( @@ -93,6 +110,8 @@ class DartDioCodeGen { if (queryParamExp != null) queryParamExp, if (headerExp != null) headerExp, if (dataExp != null) dataExp, + if ((contentType == ContentType.formdata && formData.isNotEmpty)) + multiPartList, responseExp, refer('print').call([refer('response').property('statusCode')]), refer('print').call([refer('response').property('data')]), @@ -122,6 +141,8 @@ class DartDioCodeGen { sbf.writeln(mainFunction.accept(emitter)); - return DartFormatter(pageWidth: 160).format(sbf.toString()); + return DartFormatter( + pageWidth: contentType == ContentType.formdata ? 70 : 160) + .format(sbf.toString()); } } diff --git a/lib/codegen/dart/http.dart b/lib/codegen/dart/http.dart index 3b70db65..8c1f3c8f 100644 --- a/lib/codegen/dart/http.dart +++ b/lib/codegen/dart/http.dart @@ -1,10 +1,9 @@ +import 'dart:convert'; 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:dart_style/dart_style.dart'; - +import 'package:apidash/models/models.dart' show RequestModel; +import 'package:apidash/consts.dart'; import 'shared.dart'; class DartHttpCodeGen { @@ -22,9 +21,10 @@ class DartHttpCodeGen { method: requestModel.method, queryParams: requestModel.enabledParamsMap, headers: {...requestModel.enabledHeadersMap}, - body: requestModel.requestBody, contentType: requestModel.requestBodyContentType, - hasContentTypeHeader: requestModel.hasContentTypeHeader, + hasContentTypeHeader: requestModel.hasContentTypeHeader, + body: requestModel.requestBody, + formData: requestModel.formDataMapList, ); return next; } catch (e) { @@ -37,9 +37,10 @@ class DartHttpCodeGen { required HTTPVerb method, required Map queryParams, required Map headers, - required String? body, required ContentType contentType, + required String? body, required bool hasContentTypeHeader, + required List> formData, }) { final uri = Uri.parse(url); @@ -55,7 +56,6 @@ class DartHttpCodeGen { if (kMethodsWithBody.contains(method) && (body?.isNotEmpty ?? false)) { final strContent = CodeExpression(Code('r\'\'\'$body\'\'\'')); dataExp = declareVar('body', type: refer('String')).assign(strContent); - if (!hasContentTypeHeader) { headers.putIfAbsent(HttpHeaders.contentTypeHeader, () => kContentTypeMap[contentType] ?? ''); @@ -102,7 +102,9 @@ class DartHttpCodeGen { ); } final responseExp = declareFinal('response').assign(InvokeExpression.newOf( - refer('http.${method.name}'), + refer( + 'http.${method.name}', + ), [refer('uri')], { if (headerExp != null) 'headers': refer('headers'), @@ -110,7 +112,36 @@ class DartHttpCodeGen { }, [], ).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 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 statusRef = refer('statusCode'); m @@ -135,25 +166,49 @@ class DartHttpCodeGen { b.statements.add(headerExp.statement); } b.statements.add(const Code('\n')); - b.statements.add(responseExp.statement); - b.statements.add(const Code('\n')); - b.statements.add(declareVar('statusCode', type: refer('int')) - .assign(refer('response').property('statusCode')) - .statement); + 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(const Code('\n')); + b.statements.add(declareVar('statusCode', type: refer('int')) + .assign(refer('response').property('statusCode')) + .statement); + } + b.statements.add(declareIfElse( condition: statusRef .greaterOrEqualTo(literalNum(200)) .and(statusRef.lessThan(literalNum(300))), body: [ refer('print').call([literalString(r'Status Code: $statusCode')]), - refer('print') - .call([literalString(r'Response Body: ${response.body}')]), + refer('print').call([ + literalString( + 'Response Body: ${contentType == ContentType.formdata ? ':\$responseBody' : '\${response.body}'}') + ]), ], elseBody: [ refer('print') .call([literalString(r'Error Status Code: $statusCode')]), - refer('print').call( - [literalString(r'Error Response Body: ${response.body}')]), + refer('print').call([ + literalString( + 'Error Response Body: ${contentType == ContentType.formdata ? ':\$responseBody' : '\${response.body}'}') + ]), ], )); }); @@ -161,6 +216,8 @@ class DartHttpCodeGen { sbf.writeln(mainFunction.accept(emitter)); - return DartFormatter(pageWidth: 160).format(sbf.toString()); + return DartFormatter( + pageWidth: contentType == ContentType.formdata ? 70 : 160) + .format(sbf.toString()); } } diff --git a/lib/codegen/js/axios.dart b/lib/codegen/js/axios.dart index bc338945..1aeee285 100644 --- a/lib/codegen/js/axios.dart +++ b/lib/codegen/js/axios.dart @@ -1,16 +1,18 @@ -import 'package:apidash/consts.dart'; +import 'dart:convert'; import 'package:jinja/jinja.dart' as jj; 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/consts.dart'; class AxiosCodeGen { AxiosCodeGen({this.isNodeJs = false}); 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 = { @@ -46,13 +48,49 @@ axios(config) 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( RequestModel requestModel, String defaultUriScheme, ) { 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; if (!url.contains("://") && url.isNotEmpty) { @@ -80,18 +118,31 @@ axios(config) } var headers = harJson["headers"]; - if (headers.isNotEmpty) { + if (headers.isNotEmpty || requestModel.isFormDataRequest) { var templateHeader = jj.Template(kTemplateHeader); var m = {}; for (var i in headers) { m[i["name"]] = i["value"]; } + if (requestModel.isFormDataRequest) { + m['Content-Type'] = 'multipart/form-data'; + } result += templateHeader .render({"headers": padMultilineString(kEncoder.convert(m), 2)}); } + 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) { - var templateBody = jj.Template(kTemplateBody); result += templateBody .render({"body": kEncoder.convert(harJson["postData"]["text"])}); } diff --git a/lib/codegen/js/fetch.dart b/lib/codegen/js/fetch.dart index bc15b7dd..4ef2e551 100644 --- a/lib/codegen/js/fetch.dart +++ b/lib/codegen/js/fetch.dart @@ -1,15 +1,18 @@ -import 'package:apidash/consts.dart'; +import 'dart:convert'; import 'package:jinja/jinja.dart' as jj; import 'package:apidash/utils/utils.dart' - show requestModelToHARJsonRequest, padMultilineString; + show padMultilineString, requestModelToHARJsonRequest; import 'package:apidash/models/models.dart' show RequestModel; +import 'package:apidash/consts.dart'; class FetchCodeGen { FetchCodeGen({this.isNodeJs = false}); 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}} """; + 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 = """ }; @@ -53,8 +76,19 @@ fetch(url, options) String defaultUriScheme, ) { 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; if (!url.contains("://") && url.isNotEmpty) { url = "$defaultUriScheme://$url"; @@ -70,21 +104,33 @@ fetch(url, options) }); var headers = harJson["headers"]; + if (headers.isNotEmpty) { var templateHeader = jj.Template(kTemplateHeader); var m = {}; + if (requestModel.isFormDataRequest) { + m["Content-Type"] = "multipart/form-data"; + } for (var i in headers) { m[i["name"]] = i["value"]; } - result += templateHeader - .render({"headers": padMultilineString(kEncoder.convert(m), 2)}); + result += templateHeader.render({ + "headers": padMultilineString(kEncoder.convert(m), 2), + }); } if (harJson["postData"]?["text"] != null) { var templateBody = jj.Template(kTemplateBody); - result += templateBody - .render({"body": kEncoder.convert(harJson["postData"]["text"])}); + result += templateBody.render({ + "body": kEncoder.convert(harJson["postData"]["text"]), + }); + } else if (requestModel.isFormDataRequest) { + var templateBody = jj.Template(kTemplateBody); + result += templateBody.render({ + "body": 'payload', + }); } + result += kStringRequest; return result; } catch (e) { diff --git a/lib/codegen/kotlin/okhttp.dart b/lib/codegen/kotlin/okhttp.dart index 362e31a1..becfdbe4 100644 --- a/lib/codegen/kotlin/okhttp.dart +++ b/lib/codegen/kotlin/okhttp.dart @@ -1,9 +1,9 @@ import 'dart:convert'; -import 'package:apidash/consts.dart'; import 'package:jinja/jinja.dart' as jj; import 'package:apidash/utils/utils.dart' show getValidRequestUri, stripUriParams; import '../../models/request_model.dart'; +import 'package:apidash/consts.dart'; class KotlinOkHttpCodeGen { 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( RequestModel requestModel, @@ -68,7 +75,6 @@ import okhttp3.MediaType.Companion.toMediaType"""; ) { try { String result = ""; - bool hasHeaders = false; bool hasQuery = false; bool hasBody = false; @@ -102,7 +108,13 @@ import okhttp3.MediaType.Companion.toMediaType"""; var method = requestModel.method; 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; if (contentLength > 0) { hasBody = true; @@ -127,7 +139,6 @@ import okhttp3.MediaType.Companion.toMediaType"""; if (headersList != null) { var headers = requestModel.enabledHeadersMap; if (headers.isNotEmpty) { - hasHeaders = true; result += getHeaders(headers); } } @@ -135,7 +146,7 @@ import okhttp3.MediaType.Companion.toMediaType"""; var templateRequestEnd = jj.Template(kTemplateRequestEnd); result += templateRequestEnd.render({ "method": method.name.toLowerCase(), - "hasBody": hasBody ? "body" : "", + "hasBody": (hasBody || requestModel.isFormDataRequest) ? "body" : "", }); } return result; diff --git a/lib/codegen/others/curl.dart b/lib/codegen/others/curl.dart index 673014aa..4ae610de 100644 --- a/lib/codegen/others/curl.dart +++ b/lib/codegen/others/curl.dart @@ -10,6 +10,9 @@ class cURLCodeGen { String kTemplateHeader = """ \\ --header '{{name}}: {{value}}' """; + String kTemplateFormData = """ \\ + --form '{{name}}: {{value}}' +"""; String kTemplateBody = """ \\ --data '{{body}}' @@ -48,6 +51,23 @@ class cURLCodeGen { .render({"name": item["name"], "value": item["value"]}); } } + if (harJson['formData'] != null) { + var formDataList = harJson['formData'] as List>; + 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) { var templateBody = jj.Template(kTemplateBody); diff --git a/lib/codegen/python/http_client.dart b/lib/codegen/python/http_client.dart index 1c72f138..2d002ca7 100644 --- a/lib/codegen/python/http_client.dart +++ b/lib/codegen/python/http_client.dart @@ -1,13 +1,16 @@ import 'dart:io'; import 'dart:convert'; import 'package:jinja/jinja.dart' as jj; -import 'package:apidash/consts.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/consts.dart'; class PythonHttpClientCodeGen { final String kTemplateStart = """import http.client +{% if isFormDataRequest %}import mimetypes +from codecs import encode +{% endif %} """; String kTemplateParams = """ @@ -30,6 +33,8 @@ body = r'''{{body}}''' headers = {{headers}} """; + String kTemplateFormHeaderContentType = ''' +multipart/form-data; boundary={{boundary}}'''; int kHeadersPadding = 10; @@ -55,10 +60,38 @@ data = res.read() 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( RequestModel requestModel, String defaultUriScheme, ) { + String uuid = getNewUuid(); + try { String result = ""; bool hasHeaders = false; @@ -70,11 +103,17 @@ print(data.decode("utf-8")) url = "$defaultUriScheme://$url"; } - result += kTemplateStart; + var templateStartUrl = jj.Template(kTemplateStart); + result += templateStartUrl.render( + { + "isFormDataRequest": requestModel.isFormDataRequest, + }, + ); var rec = getValidRequestUri( url, requestModel.enabledRequestParams, ); + Uri? uri = rec.$1; if (uri != null) { @@ -103,6 +142,14 @@ print(data.decode("utf-8")) var headersList = requestModel.enabledRequestHeaders; if (headersList != null || hasBody) { var headers = requestModel.enabledHeadersMap; + if (requestModel.isFormDataRequest) { + var formHeaderTemplate = + jj.Template(kTemplateFormHeaderContentType); + headers[HttpHeaders.contentTypeHeader] = formHeaderTemplate.render({ + "boundary": uuid, + }); + } + if (headers.isNotEmpty || hasBody) { hasHeaders = true; if (hasBody && !requestModel.hasContentTypeHeader) { @@ -115,7 +162,15 @@ print(data.decode("utf-8")) 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); result += templateConnection.render({ "isHttps": uri.scheme == "https" ? "S" : "", @@ -129,11 +184,11 @@ print(data.decode("utf-8")) "queryParamsStr": hasQuery ? " + queryParamsStr" : "", }); - if (hasBody) { + if (hasBody || requestModel.isFormDataRequest) { result += kStringRequestBody; } - if (hasHeaders) { + if (hasHeaders || requestModel.isFormDataRequest) { result += kStringRequestHeaders; } diff --git a/lib/codegen/python/requests.dart b/lib/codegen/python/requests.dart index bb3c53c8..4d9523e4 100644 --- a/lib/codegen/python/requests.dart +++ b/lib/codegen/python/requests.dart @@ -3,12 +3,14 @@ import 'dart:convert'; import 'package:jinja/jinja.dart' as jj; import 'package:apidash/consts.dart'; import 'package:apidash/utils/utils.dart' - show getValidRequestUri, padMultilineString, stripUriParams; + show getNewUuid, getValidRequestUri, padMultilineString, stripUriParams; import 'package:apidash/models/models.dart' show RequestModel; class PythonRequestsCodeGen { final String kTemplateStart = """import requests - +{% if isFormDataRequest %}import mimetypes +from codecs import encode +{% endif %} url = '{{url}}' """; @@ -37,6 +39,8 @@ payload = {{body}} headers = {{headers}} """; + String kTemplateFormHeaderContentType = ''' +multipart/form-data; boundary={{boundary}}'''; int kHeadersPadding = 10; @@ -45,6 +49,34 @@ headers = {{headers}} 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 kStringRequestBody = """, data=payload"""; @@ -69,6 +101,7 @@ print('Response Body:', response.text) bool hasHeaders = false; bool hasBody = false; bool hasJsonBody = false; + String uuid = getNewUuid(); String url = requestModel.url; if (!url.contains("://") && url.isNotEmpty) { @@ -82,7 +115,10 @@ print('Response Body:', response.text) Uri? uri = rec.$1; if (uri != null) { var templateStartUrl = jj.Template(kTemplateStart); - result += templateStartUrl.render({"url": stripUriParams(uri)}); + result += templateStartUrl.render({ + "url": stripUriParams(uri), + 'isFormDataRequest': requestModel.isFormDataRequest + }); if (uri.hasQuery) { var params = uri.queryParameters; @@ -115,6 +151,13 @@ print('Response Body:', response.text) var headersList = requestModel.enabledRequestHeaders; if (headersList != null || hasBody) { var headers = requestModel.enabledHeadersMap; + if (requestModel.isFormDataRequest) { + var formHeaderTemplate = + jj.Template(kTemplateFormHeaderContentType); + headers[HttpHeaders.contentTypeHeader] = formHeaderTemplate.render({ + "boundary": uuid, + }); + } if (headers.isNotEmpty || hasBody) { hasHeaders = true; if (hasBody) { @@ -127,7 +170,15 @@ print('Response Body:', response.text) 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); result += templateRequest.render({ "method": method.name.toLowerCase(), @@ -137,15 +188,15 @@ print('Response Body:', response.text) result += kStringRequestParams; } - if (hasBody) { + if (hasBody || requestModel.isFormDataRequest) { result += kStringRequestBody; } - if (hasJsonBody) { + if (hasJsonBody || requestModel.isFormDataRequest) { result += kStringRequestJson; } - if (hasHeaders) { + if (hasHeaders || requestModel.isFormDataRequest) { result += kStringRequestHeaders; } diff --git a/lib/consts.dart b/lib/consts.dart index d881fb86..cc1ae394 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -48,6 +48,10 @@ const kHintOpacity = 0.6; const kForegroundOpacity = 0.05; const kTextStyleButton = TextStyle(fontWeight: FontWeight.bold); +const kFormDataButtonLabelTextStyle = TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, +); const kBorderRadius8 = BorderRadius.all(Radius.circular(8)); final kBorderRadius10 = BorderRadius.circular(10); @@ -57,6 +61,7 @@ const kP1 = EdgeInsets.all(1); const kP5 = EdgeInsets.all(5); const kP8 = EdgeInsets.all(8); const kPs8 = EdgeInsets.only(left: 8); +const kPs2 = EdgeInsets.only(left: 2); const kPh20v5 = EdgeInsets.symmetric(horizontal: 20, vertical: 5); const kPh20v10 = EdgeInsets.symmetric(horizontal: 20, vertical: 10); const kP10 = EdgeInsets.all(10); @@ -85,7 +90,7 @@ const kP8CollectionPane = EdgeInsets.only( // bottom: 8.0, ); const kPr8CollectionPane = EdgeInsets.only(right: 8.0); - +const kpsV5 = EdgeInsets.symmetric(vertical: 2); const kHSpacer4 = SizedBox(width: 4); const kHSpacer5 = SizedBox(width: 5); const kHSpacer10 = SizedBox(width: 10); @@ -237,7 +242,9 @@ enum RequestItemMenuOption { edit, delete, duplicate } 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 kDefaultUriScheme = "https"; @@ -308,6 +315,7 @@ const kSubTypeDefaultViewOptions = 'all'; const kContentTypeMap = { ContentType.json: "$kTypeApplication/$kSubTypeJson", ContentType.text: "$kTypeText/$kSubTypePlain", + ContentType.formdata: "multipart/form-data", }; enum ResponseBodyView { preview, code, raw, none } diff --git a/lib/models/form_data_model.dart b/lib/models/form_data_model.dart new file mode 100644 index 00000000..add20833 --- /dev/null +++ b/lib/models/form_data_model.dart @@ -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 json) => + _$FormDataModelFromJson(json); +} + +const kFormDataEmptyModel = FormDataModel( + name: "", + value: "", + type: FormDataType.text, +); diff --git a/lib/models/form_data_model.freezed.dart b/lib/models/form_data_model.freezed.dart new file mode 100644 index 00000000..0958bd4e --- /dev/null +++ b/lib/models/form_data_model.freezed.dart @@ -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 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 json) { + return _FormDataModel.fromJson(json); +} + +/// @nodoc +mixin _$FormDataModel { + String get name => throw _privateConstructorUsedError; + String get value => throw _privateConstructorUsedError; + FormDataType get type => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $FormDataModelCopyWith 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 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 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 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; +} diff --git a/lib/models/form_data_model.g.dart b/lib/models/form_data_model.g.dart new file mode 100644 index 00000000..f539458f --- /dev/null +++ b/lib/models/form_data_model.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'form_data_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$FormDataModelImpl _$$FormDataModelImplFromJson(Map json) => + _$FormDataModelImpl( + name: json['name'] as String, + value: json['value'] as String, + type: $enumDecode(_$FormDataTypeEnumMap, json['type']), + ); + +Map _$$FormDataModelImplToJson(_$FormDataModelImpl instance) => + { + 'name': instance.name, + 'value': instance.value, + 'type': _$FormDataTypeEnumMap[instance.type]!, + }; + +const _$FormDataTypeEnumMap = { + FormDataType.text: 'text', + FormDataType.file: 'file', +}; diff --git a/lib/models/models.dart b/lib/models/models.dart index 3820e3c2..66d6f6ce 100644 --- a/lib/models/models.dart +++ b/lib/models/models.dart @@ -2,3 +2,4 @@ export 'name_value_model.dart'; export 'request_model.dart'; export 'response_model.dart'; export 'settings_model.dart'; +export 'form_data_model.dart'; diff --git a/lib/models/request_model.dart b/lib/models/request_model.dart index 265d2c3f..9154e428 100644 --- a/lib/models/request_model.dart +++ b/lib/models/request_model.dart @@ -1,10 +1,14 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; -import 'package:apidash/consts.dart'; -import 'package:apidash/utils/utils.dart' - show mapToRows, rowsToMap, getEnabledRows; -import 'name_value_model.dart'; -import 'response_model.dart'; +import '../utils/utils.dart' + show + mapListToFormDataModelRows, + rowsToFormDataMapList, + mapToRows, + rowsToMap, + getEnabledRows; +import '../consts.dart'; +import 'models.dart'; @immutable class RequestModel { @@ -21,6 +25,7 @@ class RequestModel { this.isParamEnabledList, this.requestBodyContentType = ContentType.json, this.requestBody, + this.requestFormDataList, this.responseStatus, this.message, this.responseModel, @@ -38,6 +43,7 @@ class RequestModel { final List? isParamEnabledList; final ContentType requestBodyContentType; final String? requestBody; + final List? requestFormDataList; final int? responseStatus; final String? message; final ResponseModel? responseModel; @@ -54,6 +60,10 @@ class RequestModel { Map get headersMap => rowsToMap(requestHeaders) ?? {}; Map get paramsMap => rowsToMap(requestParams) ?? {}; + List> get formDataMapList => + rowsToFormDataMapList(requestFormDataList) ?? []; + bool get isFormDataRequest => requestBodyContentType == ContentType.formdata; + bool get hasContentTypeHeader => enabledHeadersMap.keys .any((k) => k.toLowerCase() == HttpHeaders.contentTypeHeader); @@ -74,6 +84,8 @@ class RequestModel { isParamEnabledList != null ? [...isParamEnabledList!] : null, requestBodyContentType: requestBodyContentType, requestBody: requestBody, + requestFormDataList: + requestFormDataList != null ? [...requestFormDataList!] : null, ); } @@ -90,6 +102,7 @@ class RequestModel { List? isParamEnabledList, ContentType? requestBodyContentType, String? requestBody, + List? requestFormDataList, int? responseStatus, String? message, ResponseModel? responseModel, @@ -112,6 +125,7 @@ class RequestModel { requestBodyContentType: requestBodyContentType ?? this.requestBodyContentType, requestBody: requestBody ?? this.requestBody, + requestFormDataList: requestFormDataList ?? this.requestFormDataList, responseStatus: responseStatus ?? this.responseStatus, message: message ?? this.message, responseModel: responseModel ?? this.responseModel, @@ -143,9 +157,11 @@ class RequestModel { requestBodyContentType = kDefaultContentType; } final requestBody = data["requestBody"] as String?; + final requestFormDataList = data["requestFormDataList"]; final responseStatus = data["responseStatus"] as int?; final message = data["message"] as String?; final responseModelJson = data["responseModel"]; + if (responseModelJson != null) { responseModel = ResponseModel.fromJson(Map.from(responseModelJson)); @@ -170,6 +186,9 @@ class RequestModel { isParamEnabledList: isParamEnabledList, requestBodyContentType: requestBodyContentType, requestBody: requestBody, + requestFormDataList: requestFormDataList != null + ? mapListToFormDataModelRows(List.from(requestFormDataList)) + : null, responseStatus: responseStatus, message: message, responseModel: responseModel, @@ -189,6 +208,7 @@ class RequestModel { "isParamEnabledList": isParamEnabledList, "requestBodyContentType": requestBodyContentType.name, "requestBody": requestBody, + "requestFormDataList": rowsToFormDataMapList(requestFormDataList), "responseStatus": includeResponse ? responseStatus : null, "message": includeResponse ? message : null, "responseModel": includeResponse ? responseModel?.toJson() : null, @@ -210,6 +230,7 @@ class RequestModel { "Enabled Params: ${isParamEnabledList.toString()}", "Request Body Content Type: ${requestBodyContentType.toString()}", "Request Body: ${requestBody.toString()}", + "Request FormData: ${requestFormDataList.toString()}", "Response Status: $responseStatus", "Response Message: $message", "Response: ${responseModel.toString()}" @@ -232,6 +253,7 @@ class RequestModel { listEquals(other.isParamEnabledList, isParamEnabledList) && other.requestBodyContentType == requestBodyContentType && other.requestBody == requestBody && + other.requestFormDataList == requestFormDataList && other.responseStatus == responseStatus && other.message == message && other.responseModel == responseModel; @@ -253,6 +275,7 @@ class RequestModel { isParamEnabledList, requestBodyContentType, requestBody, + requestFormDataList, responseStatus, message, responseModel, diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 8e5b305a..415c9715 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -5,6 +5,7 @@ import '../models/models.dart'; import '../services/services.dart' show hiveHandler, HiveHandler, request; import '../utils/utils.dart' show uuid, collectionToHAR; import '../consts.dart'; +import 'package:http/http.dart' as http; final activeIdStateProvider = StateProvider((ref) => null); @@ -127,6 +128,7 @@ class CollectionStateNotifier List? isParamEnabledList, ContentType? requestBodyContentType, String? requestBody, + List? requestFormDataList, int? responseStatus, String? message, ResponseModel? responseModel, @@ -143,9 +145,11 @@ class CollectionStateNotifier isParamEnabledList: isParamEnabledList, requestBodyContentType: requestBodyContentType, requestBody: requestBody, + requestFormDataList: requestFormDataList, responseStatus: responseStatus, message: message, responseModel: responseModel); + //print(newModel); var map = {...state!}; map[id] = newModel; state = map; @@ -156,10 +160,13 @@ class CollectionStateNotifier ref.read(codePaneVisibleStateProvider.notifier).state = false; final defaultUriScheme = ref.read(settingsProvider.select((value) => value.defaultUriScheme)); - RequestModel requestModel = state![id]!; - var responseRec = - await request(requestModel, defaultUriScheme: defaultUriScheme); + (http.Response?, Duration?, String?)? responseRec = await request( + requestModel, + defaultUriScheme: defaultUriScheme, + isMultiPartRequest: + requestModel.requestBodyContentType == ContentType.formdata, + ); late final RequestModel newRequestModel; if (responseRec.$1 == null) { newRequestModel = requestModel.copyWith( diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart index 07c5bf53..94ff858c 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart @@ -18,6 +18,10 @@ class _EditRequestBodyState extends ConsumerState { final requestModel = ref .read(collectionStateNotifierProvider.notifier) .getRequestModel(activeId!); + ContentType? requestBodyStateWatcher = (ref + .watch(collectionStateNotifierProvider)![activeId] + ?.requestBodyContentType) ?? + ContentType.values.first; return Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.background, @@ -38,16 +42,18 @@ class _EditRequestBodyState extends ConsumerState { ), ), Expanded( - child: TextFieldEditor( - key: Key("$activeId-body"), - fieldKey: "$activeId-body-editor", - initialValue: requestModel?.requestBody, - onChanged: (String value) { - ref - .read(collectionStateNotifierProvider.notifier) - .update(activeId, requestBody: value); - }, - ), + child: requestBodyStateWatcher == ContentType.formdata + ? const FormDataWidget() + : TextFieldEditor( + key: Key("$activeId-body"), + fieldKey: "$activeId-body-editor", + initialValue: requestModel?.requestBody, + onChanged: (String value) { + ref + .read(collectionStateNotifierProvider.notifier) + .update(activeId, requestBody: value); + }, + ), ) ], ), diff --git a/lib/services/http_service.dart b/lib/services/http_service.dart index 485ad862..46a66a91 100644 --- a/lib/services/http_service.dart +++ b/lib/services/http_service.dart @@ -9,6 +9,7 @@ import 'package:apidash/consts.dart'; Future<(http.Response?, Duration?, String?)> request( RequestModel requestModel, { String defaultUriScheme = kDefaultUriScheme, + bool isMultiPartRequest = false, }) async { (Uri?, String?) uriRec = getValidRequestUri( requestModel.url, @@ -35,6 +36,31 @@ Future<(http.Response?, Duration?, String?)> request( } } 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) { case HTTPVerb.get: response = await http.get(requestUrl, headers: headers); diff --git a/lib/utils/convert_utils.dart b/lib/utils/convert_utils.dart index 94e6c878..bc704af9 100644 --- a/lib/utils/convert_utils.dart +++ b/lib/utils/convert_utils.dart @@ -2,6 +2,7 @@ import 'dart:typed_data'; import 'dart:convert'; import '../models/models.dart'; import '../consts.dart'; +import 'package:http/http.dart' as http; String humanizeDuration(Duration? duration) { if (duration == null) { @@ -89,6 +90,43 @@ List? mapToRows(Map? kvMap) { return finalRows; } +List>? rowsToFormDataMapList( + List? kvRows, +) { + if (kvRows == null) { + return null; + } + List> finalMap = kvRows + .map((FormDataModel formData) => { + "name": formData.name, + "value": formData.value, + "type": formData.type.name, + }) + .toList(); + return finalMap; +} + +List? mapListToFormDataModelRows(List? kvMap) { + if (kvMap == null) { + return null; + } + List 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) { if (text == null) { return null; @@ -110,6 +148,23 @@ Uint8List jsonMapToBytes(Map? map) { } } +Future 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? getEnabledRows( List? rows, List? isRowEnabledList) { if (rows == null || isRowEnabledList == null) { diff --git a/lib/utils/har_utils.dart b/lib/utils/har_utils.dart index 87f9a3fb..f7491bc1 100644 --- a/lib/utils/har_utils.dart +++ b/lib/utils/har_utils.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'dart:io'; 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/models/models.dart' show RequestModel; import 'package:package_info_plus/package_info_plus.dart'; @@ -153,7 +155,9 @@ Map requestModelToHARJsonRequest( } } } - + if (requestModel.isFormDataRequest) { + json["formData"] = requestModel.formDataMapList; + } if (exportMode) { json["comment"] = ""; json["cookies"] = []; diff --git a/lib/widgets/dropdowns.dart b/lib/widgets/dropdowns.dart index 940ac34c..69008a6b 100644 --- a/lib/widgets/dropdowns.dart +++ b/lib/widgets/dropdowns.dart @@ -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 createState() => _DropdownButtonFormData(); +} + +class _DropdownButtonFormData extends State { + @override + Widget build(BuildContext context) { + final surfaceColor = Theme.of(context).colorScheme.surface; + return DropdownButton( + 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>((FormDataType value) { + return DropdownMenuItem( + value: value, + child: Padding( + padding: kPs8, + child: Text( + value.name, + style: kTextStyleButton, + ), + ), + ); + }).toList(), + ); + } +} + class DropdownButtonCodegenLanguage extends StatelessWidget { const DropdownButtonCodegenLanguage({ super.key, diff --git a/lib/widgets/form_data_field.dart b/lib/widgets/form_data_field.dart new file mode 100644 index 00000000..0353d033 --- /dev/null +++ b/lib/widgets/form_data_field.dart @@ -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 createState() => _FormDataFieldState(); +} + +class _FormDataFieldState extends State { + @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, + ), + ), + ], + ); + } +} diff --git a/lib/widgets/form_data_widget.dart b/lib/widgets/form_data_widget.dart new file mode 100644 index 00000000..bb4782d2 --- /dev/null +++ b/lib/widgets/form_data_widget.dart @@ -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 createState() => _FormDataBodyState(); +} + +class _FormDataBodyState extends ConsumerState { + late int seed; + final random = Random.secure(); + late List 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 daviModelRows = DaviModel( + 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(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, + ); + } +} diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 8c288953..c627f82a 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -22,3 +22,4 @@ export 'textfields.dart'; export 'texts.dart'; export 'uint8_audio_player.dart'; export 'window_caption.dart'; +export 'form_data_widget.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index a32322ee..33a4ad9f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,6 +48,7 @@ dependencies: url: https://github.com/foss42/json_data_explorer.git version: ^0.1.1 scrollable_positioned_list: ^0.2.3 + file_picker: ^6.1.1 flutter_svg: ^2.0.9 vector_graphics_compiler: ^1.1.9+1 code_builder: ^4.9.0 diff --git a/test/models/request_model_test.dart b/test/models/request_model_test.dart index b1b56bfd..66064d31 100644 --- a/test/models/request_model_test.dart +++ b/test/models/request_model_test.dart @@ -113,6 +113,7 @@ void main() { "requestBody": '''{ "text":"WORLD" }''', + 'requestFormDataList': null, 'responseStatus': null, 'message': null, 'responseModel': null @@ -147,6 +148,7 @@ void main() { "Enabled Params: null", "Request Body Content Type: ContentType.json", 'Request Body: {\n"text":"WORLD"\n}', + 'Request FormData: null', "Response Status: null", "Response Message: null", "Response: null"