From a792e0fb357172a77c660c18cb0723ba14c8c740 Mon Sep 17 00:00:00 2001 From: laiiihz Date: Thu, 21 Dec 2023 13:20:41 +0800 Subject: [PATCH] feat: rewrite dart http code gen with dart code builder & format --- lib/codegen/dart/dio.dart | 10 +- lib/codegen/dart/http.dart | 273 ++++++++++++----------- lib/codegen/dart/shared.dart | 34 ++- test/codegen/dart_http_codegen_test.dart | 223 ++++++++++-------- 4 files changed, 317 insertions(+), 223 deletions(-) diff --git a/lib/codegen/dart/dio.dart b/lib/codegen/dart/dio.dart index 7c603716..fcaa8314 100644 --- a/lib/codegen/dart/dio.dart +++ b/lib/codegen/dart/dio.dart @@ -17,7 +17,7 @@ class DartDioCodeGen { } final next = generatedDartCode( url: url, - method: requestModel.method.name, + method: requestModel.method, queryParams: requestModel.paramsMap, headers: requestModel.headersMap, body: requestModel.requestBody, @@ -31,7 +31,7 @@ class DartDioCodeGen { String generatedDartCode({ required String url, - required String method, + required HTTPVerb method, required Map queryParams, required Map headers, required String? body, @@ -56,9 +56,10 @@ class DartDioCodeGen { } Expression? dataExp; - if (body != null) { + if (kMethodsWithBody.contains(method) && (body?.isNotEmpty ?? false)) { final strContent = CodeExpression(Code('r\'\'\'$body\'\'\'')); switch (contentType) { + // dio dosen't need pass `content-type` header when body is json or plain text case ContentType.json: final convertImport = Directive.import('dart:convert', as: 'convert'); sbf.writeln(convertImport.accept(emitter)); @@ -66,6 +67,7 @@ class DartDioCodeGen { .assign(refer('convert.json.decode').call([strContent])); case ContentType.text: dataExp = declareFinal('data').assign(strContent); + // when add new type of [ContentType], need update [dataExp]. } } final responseExp = declareFinal('response').assign(InvokeExpression.newOf( @@ -81,7 +83,7 @@ class DartDioCodeGen { if (dataExp != null) 'data': refer('data'), }, [], - method, + method.name, ).awaited); final mainFunction = Method((m) { diff --git a/lib/codegen/dart/http.dart b/lib/codegen/dart/http.dart index 2c0e016c..6beccac8 100644 --- a/lib/codegen/dart/http.dart +++ b/lib/codegen/dart/http.dart @@ -1,147 +1,164 @@ 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 padMultilineString; import 'package:apidash/models/models.dart' show RequestModel; +import 'package:code_builder/code_builder.dart'; +import 'package:dart_style/dart_style.dart'; + +import 'shared.dart'; class DartHttpCodeGen { - String kTemplateStart = """import 'package:http/http.dart' as http; - -void main() async { - var uri = Uri.parse('{{url}}'); - -"""; - - String kTemplateParams = """ - - var queryParams = {{params}}; -"""; - int kParamsPadding = 20; - - String kStringUrlParams = """ - - var urlQueryParams = Map.from(uri.queryParameters); - urlQueryParams.addAll(queryParams); - uri = uri.replace(queryParameters: urlQueryParams); -"""; - - String kStringNoUrlParams = """ - - uri = uri.replace(queryParameters: queryParams); -"""; - - String kTemplateBody = """ - - String body = r'''{{body}}'''; - -"""; - - String kTemplateHeaders = """ - - var headers = {{headers}}; - -"""; - int kHeadersPadding = 16; - - String kTemplateRequest = """ - - final response = await http.{{method}}(uri"""; - - String kStringRequestHeaders = """, - headers: headers"""; - - String kStringRequestBody = """, - body: body"""; - String kStringRequestEnd = r"""); - - int statusCode = response.statusCode; - if (statusCode >= 200 && statusCode < 300) { - print('Status Code: $statusCode'); - print('Response Body: ${response.body}'); - } - else{ - print('Error Status Code: $statusCode'); - print('Error Response Body: ${response.body}'); - } -} -"""; - String? getCode( RequestModel requestModel, String defaultUriScheme, ) { try { - String result = ""; - bool hasHeaders = false; - bool hasBody = false; - String url = requestModel.url; if (!url.contains("://") && url.isNotEmpty) { url = "$defaultUriScheme://$url"; } - var templateStartUrl = jj.Template(kTemplateStart); - result += templateStartUrl.render({"url": url}); - - var paramsList = requestModel.requestParams; - if (paramsList != null) { - var params = requestModel.paramsMap; - if (params.isNotEmpty) { - var templateParams = jj.Template(kTemplateParams); - var paramsString = kEncoder.convert(params); - paramsString = padMultilineString(paramsString, kParamsPadding); - result += templateParams.render({"params": paramsString}); - Uri uri = Uri.parse(url); - if (uri.hasQuery) { - result += kStringUrlParams; - } else { - result += kStringNoUrlParams; - } - } - } - - var method = requestModel.method; - var requestBody = requestModel.requestBody; - if (kMethodsWithBody.contains(method) && requestBody != null) { - var contentLength = utf8.encode(requestBody).length; - if (contentLength > 0) { - hasBody = true; - var templateBody = jj.Template(kTemplateBody); - result += templateBody.render({"body": requestBody}); - } - } - - var headersList = requestModel.requestHeaders; - if (headersList != null || hasBody) { - var headers = requestModel.headersMap; - if (headers.isNotEmpty || hasBody) { - hasHeaders = true; - if (hasBody) { - headers[HttpHeaders.contentTypeHeader] = - kContentTypeMap[requestModel.requestBodyContentType] ?? ""; - } - var headersString = kEncoder.convert(headers); - headersString = padMultilineString(headersString, kHeadersPadding); - var templateHeaders = jj.Template(kTemplateHeaders); - result += templateHeaders.render({"headers": headersString}); - } - } - - var templateRequest = jj.Template(kTemplateRequest); - result += templateRequest.render({"method": method.name}); - - if (hasHeaders) { - result += kStringRequestHeaders; - } - - if (hasBody) { - result += kStringRequestBody; - } - - result += kStringRequestEnd; - return result; + final next = generatedDartCode( + url: url, + method: requestModel.method, + queryParams: requestModel.paramsMap, + headers: requestModel.headersMap, + body: requestModel.requestBody, + contentType: requestModel.requestBodyContentType, + ); + return next; } catch (e) { + print(e); return null; } } + + String generatedDartCode({ + required String url, + required HTTPVerb method, + required Map queryParams, + required Map headers, + required String? body, + required ContentType contentType, + }) { + final uri = Uri.parse(url); + + final sbf = StringBuffer(); + final emitter = DartEmitter(); + final dioImport = Directive.import('package:http/http.dart', as: 'http'); + sbf.writeln(dioImport.accept(emitter)); + + final uriExp = + declareVar('uri').assign(refer('Uri.parse').call([literalString(url)])); + + final composeHeaders = headers; + Expression? dataExp; + if (kMethodsWithBody.contains(method) && (body?.isNotEmpty ?? false)) { + final strContent = CodeExpression(Code('r\'\'\'$body\'\'\'')); + dataExp = declareVar('body', type: refer('String')).assign(strContent); + + composeHeaders.putIfAbsent(HttpHeaders.contentTypeHeader, + () => kContentTypeMap[contentType] ?? ''); + } + + Expression? queryParamExp; + List? uriReassignExps; + // var urlQueryParams = Map.from(uri.queryParameters); + // urlQueryParams.addAll(queryParams); + // uri = uri.replace(queryParameters: urlQueryParams); + + if (queryParams.isNotEmpty) { + queryParamExp = declareVar('queryParams').assign( + literalMap(queryParams.map((key, value) => MapEntry(key, value))), + ); + + uriReassignExps = [ + if (uri.hasQuery) + declareVar('urlQueryParams').assign( + InvokeExpression.newOf( + refer('Map'), + [refer('uri.queryParameters')], + {}, + [], + 'from', + ), + ), + if (uri.hasQuery) + refer('urlQueryParams') + .property('addAll') + .call([refer('queryParams')], {}), + refer('uri').assign(refer('uri').property('replace').call([], { + 'queryParameters': + uri.hasQuery ? refer('urlQueryParams') : refer('queryParams'), + })) + ]; + } + + Expression? headerExp; + if (headers.isNotEmpty) { + headerExp = declareVar('headers').assign( + literalMap(headers.map((key, value) => MapEntry(key, value))), + ); + } + final responseExp = declareFinal('response').assign(InvokeExpression.newOf( + refer('http.${method.name}'), + [refer('uri')], + { + if (headerExp != null) 'headers': refer('headers'), + if (dataExp != null) 'body': refer('body'), + }, + [], + ).awaited); + + final mainFunction = Method((m) { + final statusRef = refer('statusCode'); + m + ..name = 'main' + ..returns = refer('void') + ..modifier = MethodModifier.async + ..body = Block((b) { + b.statements.add(uriExp.statement); + if (queryParamExp != null) { + b.statements.add(const Code('\n')); + b.statements.add(queryParamExp.statement); + } + if (uriReassignExps != null) { + b.statements.addAll(uriReassignExps.map((e) => e.statement)); + } + if (dataExp != null) { + b.statements.add(const Code('\n')); + b.statements.add(dataExp.statement); + } + if (headerExp != null) { + b.statements.add(const Code('\n')); + 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); + 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}')]), + ], + elseBody: [ + refer('print') + .call([literalString(r'Error Status Code: $statusCode')]), + refer('print').call( + [literalString(r'Error Response Body: ${response.body}')]), + ], + )); + }); + }); + + sbf.writeln(mainFunction.accept(emitter)); + + return DartFormatter(pageWidth: 160).format(sbf.toString()); + } } diff --git a/lib/codegen/dart/shared.dart b/lib/codegen/dart/shared.dart index 9938750c..073112dd 100644 --- a/lib/codegen/dart/shared.dart +++ b/lib/codegen/dart/shared.dart @@ -1,14 +1,24 @@ import 'package:code_builder/code_builder.dart'; +Code _toStatement(Spec spec) { + if (spec is Expression) { + return spec.statement; + } else if (spec is Code) { + return spec; + } else { + throw UnimplementedError(); + } +} + Block declareTryCatch({ - required List body, - required Map> onError, + required List body, + required Map> onError, bool showStackStrace = false, }) { return Block((b) { b.statements.add(const Code('try')); b.statements.add(const Code('{')); - b.statements.addAll(body.map((e) => e.statement).toList()); + b.statements.addAll(body.map(_toStatement).toList()); final entries = onError.entries; for (var error in entries) { @@ -19,8 +29,24 @@ Block declareTryCatch({ b.statements.add(Code(showStackStrace ? 'catch(e,s)' : 'catch(e)')); b.statements.add(const Code('{')); - b.statements.addAll(error.value.map((e) => e.statement).toList()); + b.statements.addAll(error.value.map(_toStatement).toList()); if (entries.last.key == error.key) b.statements.add(const Code('}')); } }); } + +Block declareIfElse({ + required Expression condition, + required List body, + required List elseBody, +}) { + return Block.of([ + const Code('if('), + condition.code, + const Code('){'), + ...body.map(_toStatement), + const Code('} else {'), + ...elseBody.map(_toStatement), + const Code('}'), + ]); +} diff --git a/test/codegen/dart_http_codegen_test.dart b/test/codegen/dart_http_codegen_test.dart index e384b628..0bdc5f60 100644 --- a/test/codegen/dart_http_codegen_test.dart +++ b/test/codegen/dart_http_codegen_test.dart @@ -1,7 +1,17 @@ import 'package:apidash/codegen/dart/http.dart'; +import 'package:dart_style/dart_style.dart'; import 'package:test/test.dart'; import '../request_models.dart'; +final _formatter = DartFormatter(fixes: [ + StyleFix.singleCascadeStatements +]); + +extension on String { + // format code before compare + String get format => _formatter.format(this); +} + void main() { final dartHttpCodeGen = DartHttpCodeGen(); @@ -25,7 +35,10 @@ void main() async { } } """; - expect(dartHttpCodeGen.getCode(requestModelGet1, "https"), expectedCode); + expect( + dartHttpCodeGen.getCode(requestModelGet1, "https"), + expectedCode.format, + ); }); test('GET 2', () { @@ -34,9 +47,7 @@ void main() async { void main() async { var uri = Uri.parse('https://api.foss42.com/country/data'); - var queryParams = { - "code": "US" - }; + var queryParams = {'code': 'US'}; uri = uri.replace(queryParameters: queryParams); final response = await http.get(uri); @@ -52,18 +63,18 @@ void main() async { } } """; - expect(dartHttpCodeGen.getCode(requestModelGet2, "https"), expectedCode); + expect( + dartHttpCodeGen.getCode(requestModelGet2, "https"), + expectedCode.format, + ); }); test('GET 3', () { const expectedCode = r"""import 'package:http/http.dart' as http; - void main() async { var uri = Uri.parse('https://api.foss42.com/country/data?code=US'); - var queryParams = { - "code": "IND" - }; + var queryParams = {'code': 'IND'}; var urlQueryParams = Map.from(uri.queryParameters); urlQueryParams.addAll(queryParams); uri = uri.replace(queryParameters: urlQueryParams); @@ -81,7 +92,10 @@ void main() async { } } """; - expect(dartHttpCodeGen.getCode(requestModelGet3, "https"), expectedCode); + expect( + dartHttpCodeGen.getCode(requestModelGet3, "https"), + expectedCode.format, + ); }); test('GET 4', () { @@ -91,12 +105,12 @@ void main() async { var uri = Uri.parse('https://api.foss42.com/humanize/social'); var queryParams = { - "num": "8700000", - "digits": "3", - "system": "SS", - "add_space": "true", - "trailing_zeros": "true" - }; + 'num': '8700000', + 'digits': '3', + 'system': 'SS', + 'add_space': 'true', + 'trailing_zeros': 'true', + }; uri = uri.replace(queryParameters: queryParams); final response = await http.get(uri); @@ -112,7 +126,10 @@ void main() async { } } """; - expect(dartHttpCodeGen.getCode(requestModelGet4, "https"), expectedCode); + expect( + dartHttpCodeGen.getCode(requestModelGet4, "https"), + expectedCode.format, + ); }); test('GET 5', () { @@ -121,12 +138,12 @@ void main() async { void main() async { var uri = Uri.parse('https://api.github.com/repos/foss42/apidash'); - var headers = { - "User-Agent": "Test Agent" - }; + var headers = {'User-Agent': 'Test Agent'}; - final response = await http.get(uri, - headers: headers); + final response = await http.get( + uri, + headers: headers, + ); int statusCode = response.statusCode; if (statusCode >= 200 && statusCode < 300) { @@ -139,7 +156,10 @@ void main() async { } } """; - expect(dartHttpCodeGen.getCode(requestModelGet5, "https"), expectedCode); + expect( + dartHttpCodeGen.getCode(requestModelGet5, "https"), + expectedCode.format, + ); }); test('GET 6', () { @@ -148,17 +168,15 @@ void main() async { void main() async { var uri = Uri.parse('https://api.github.com/repos/foss42/apidash'); - var queryParams = { - "raw": "true" - }; + var queryParams = {'raw': 'true'}; uri = uri.replace(queryParameters: queryParams); - var headers = { - "User-Agent": "Test Agent" - }; + var headers = {'User-Agent': 'Test Agent'}; - final response = await http.get(uri, - headers: headers); + final response = await http.get( + uri, + headers: headers, + ); int statusCode = response.statusCode; if (statusCode >= 200 && statusCode < 300) { @@ -171,7 +189,10 @@ void main() async { } } """; - expect(dartHttpCodeGen.getCode(requestModelGet6, "https"), expectedCode); + expect( + dartHttpCodeGen.getCode(requestModelGet6, "https"), + expectedCode.format, + ); }); test('GET 7', () { @@ -193,7 +214,10 @@ void main() async { } } """; - expect(dartHttpCodeGen.getCode(requestModelGet7, "https"), expectedCode); + expect( + dartHttpCodeGen.getCode(requestModelGet7, "https"), + expectedCode.format, + ); }); test('GET 8', () { @@ -202,17 +226,15 @@ void main() async { void main() async { var uri = Uri.parse('https://api.github.com/repos/foss42/apidash'); - var queryParams = { - "raw": "true" - }; + var queryParams = {'raw': 'true'}; uri = uri.replace(queryParameters: queryParams); - var headers = { - "User-Agent": "Test Agent" - }; + var headers = {'User-Agent': 'Test Agent'}; - final response = await http.get(uri, - headers: headers); + final response = await http.get( + uri, + headers: headers, + ); int statusCode = response.statusCode; if (statusCode >= 200 && statusCode < 300) { @@ -225,7 +247,10 @@ void main() async { } } """; - expect(dartHttpCodeGen.getCode(requestModelGet8, "https"), expectedCode); + expect( + dartHttpCodeGen.getCode(requestModelGet8, "https"), + expectedCode.format, + ); }); }); @@ -249,7 +274,10 @@ void main() async { } } """; - expect(dartHttpCodeGen.getCode(requestModelHead1, "https"), expectedCode); + expect( + dartHttpCodeGen.getCode(requestModelHead1, "https"), + expectedCode.format, + ); }); test('HEAD 2', () { @@ -271,7 +299,8 @@ void main() async { } } """; - expect(dartHttpCodeGen.getCode(requestModelHead2, "http"), expectedCode); + expect(dartHttpCodeGen.getCode(requestModelHead2, "http"), + expectedCode.format); }); }); @@ -286,13 +315,13 @@ void main() async { "text": "I LOVE Flutter" }'''; - var headers = { - "content-type": "text/plain" - }; + var headers = {'content-type': 'text/plain'}; - final response = await http.post(uri, - headers: headers, - body: body); + final response = await http.post( + uri, + headers: headers, + body: body, + ); int statusCode = response.statusCode; if (statusCode >= 200 && statusCode < 300) { @@ -305,7 +334,10 @@ void main() async { } } """; - expect(dartHttpCodeGen.getCode(requestModelPost1, "https"), expectedCode); + expect( + dartHttpCodeGen.getCode(requestModelPost1, "https"), + expectedCode.format, + ); }); test('POST 2', () { @@ -318,13 +350,13 @@ void main() async { "text": "I LOVE Flutter" }'''; - var headers = { - "content-type": "application/json" - }; + var headers = {'content-type': 'application/json'}; - final response = await http.post(uri, - headers: headers, - body: body); + final response = await http.post( + uri, + headers: headers, + body: body, + ); int statusCode = response.statusCode; if (statusCode >= 200 && statusCode < 300) { @@ -337,7 +369,10 @@ void main() async { } } """; - expect(dartHttpCodeGen.getCode(requestModelPost2, "https"), expectedCode); + expect( + dartHttpCodeGen.getCode(requestModelPost2, "https"), + expectedCode.format, + ); }); test('POST 3', () { @@ -351,13 +386,15 @@ void main() async { }'''; var headers = { - "User-Agent": "Test Agent", - "content-type": "application/json" - }; + 'User-Agent': 'Test Agent', + 'content-type': 'application/json', + }; - final response = await http.post(uri, - headers: headers, - body: body); + final response = await http.post( + uri, + headers: headers, + body: body, + ); int statusCode = response.statusCode; if (statusCode >= 200 && statusCode < 300) { @@ -370,7 +407,10 @@ void main() async { } } """; - expect(dartHttpCodeGen.getCode(requestModelPost3, "https"), expectedCode); + expect( + dartHttpCodeGen.getCode(requestModelPost3, "https"), + expectedCode.format, + ); }); }); group('PUT Request', () { @@ -385,13 +425,13 @@ void main() async { "job": "zion resident" }'''; - var headers = { - "content-type": "application/json" - }; + var headers = {'content-type': 'application/json'}; - final response = await http.put(uri, - headers: headers, - body: body); + final response = await http.put( + uri, + headers: headers, + body: body, + ); int statusCode = response.statusCode; if (statusCode >= 200 && statusCode < 300) { @@ -404,7 +444,10 @@ void main() async { } } """; - expect(dartHttpCodeGen.getCode(requestModelPut1, "https"), expectedCode); + expect( + dartHttpCodeGen.getCode(requestModelPut1, "https"), + expectedCode.format, + ); }); }); @@ -420,13 +463,13 @@ void main() async { "job": "accountant" }'''; - var headers = { - "content-type": "application/json" - }; + var headers = {'content-type': 'application/json'}; - final response = await http.patch(uri, - headers: headers, - body: body); + final response = await http.patch( + uri, + headers: headers, + body: body, + ); int statusCode = response.statusCode; if (statusCode >= 200 && statusCode < 300) { @@ -440,7 +483,9 @@ void main() async { } """; expect( - dartHttpCodeGen.getCode(requestModelPatch1, "https"), expectedCode); + dartHttpCodeGen.getCode(requestModelPatch1, "https"), + expectedCode.format, + ); }); }); @@ -465,7 +510,9 @@ void main() async { } """; expect( - dartHttpCodeGen.getCode(requestModelDelete1, "https"), expectedCode); + dartHttpCodeGen.getCode(requestModelDelete1, "https"), + expectedCode.format, + ); }); test('DELETE 2', () { @@ -479,13 +526,13 @@ void main() async { "job": "accountant" }'''; - var headers = { - "content-type": "application/json" - }; + var headers = {'content-type': 'application/json'}; - final response = await http.delete(uri, - headers: headers, - body: body); + final response = await http.delete( + uri, + headers: headers, + body: body, + ); int statusCode = response.statusCode; if (statusCode >= 200 && statusCode < 300) { @@ -499,7 +546,9 @@ void main() async { } """; expect( - dartHttpCodeGen.getCode(requestModelDelete2, "https"), expectedCode); + dartHttpCodeGen.getCode(requestModelDelete2, "https"), + expectedCode.format, + ); }); }); }