diff --git a/lib/codegen/codegen.dart b/lib/codegen/codegen.dart index 8883a098..eb5bb0ef 100644 --- a/lib/codegen/codegen.dart +++ b/lib/codegen/codegen.dart @@ -1 +1,22 @@ -export 'dart/pkg_http.dart'; +import 'package:apidash/codegen/kotlin/pkg_okhttp.dart'; +import 'package:apidash/consts.dart'; + +import 'package:apidash/models/models.dart' show RequestModel; +import 'dart/pkg_http.dart'; + +class Codegen { + String? getCode( + CodegenLanguage codegenLanguage, + RequestModel requestModel, + String defaultUriScheme, + ) { + switch (codegenLanguage) { + case CodegenLanguage.dartHttp: + return DartHttpCodeGen().getCode(requestModel, defaultUriScheme); + case CodegenLanguage.kotlinOkHttp: + return KotlinOkHttpCodeGen().getCode(requestModel); + default: + throw ArgumentError('Invalid codegenLanguage'); + } + } +} diff --git a/lib/codegen/kotlin/pkg_okhttp.dart b/lib/codegen/kotlin/pkg_okhttp.dart new file mode 100644 index 00000000..260b00d2 --- /dev/null +++ b/lib/codegen/kotlin/pkg_okhttp.dart @@ -0,0 +1,80 @@ +import 'package:apidash/consts.dart'; + +import '../../models/request_model.dart'; + +class KotlinOkHttpCodeGen { + final String headerSnippet = """import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.RequestBody.Companion.asRequestBody +import java.io.File +import java.util.concurrent.TimeUnit + +val client = OkHttpClient() +"""; + + final String footerSnippet = """ .build() +val response = client.newCall(request).execute() + +println(response.body!!.string()) +"""; + String getCode(RequestModel requestModel) { + String result = ""; + result = result + headerSnippet; + if (requestModel.method != HTTPVerb.get && + requestModel.method != HTTPVerb.head) { + result = + """${result}val mediaType = "${requestModel.requestBodyContentType == ContentType.json ? "application/json" : "text/plain"}".toMediaType() +val body = "${requestModel.requestBody}".toRequestBody(mediaType)\n"""; + } + result = "${result}val request = Request.Builder()\n"; + + result = "$result .url(\"${requestModel.url}\")\n"; + result = result + addQueryParams(requestModel); + result = result + addRequestMethod(requestModel); + result = result + addHeaders(requestModel); + result = result + footerSnippet; + + return result; + } + + String addQueryParams(RequestModel requestModel) { + String result = ""; + if (requestModel.requestParams == null) { + return result; + } + for (final queryParam in requestModel.requestParams!) { + result = + """$result .addQueryParameter("${queryParam.k}", "${queryParam.v}")\n"""; + } + return result; + } + + String addHeaders(RequestModel requestModel) { + String result = ""; + if (requestModel.requestHeaders == null) { + return result; + } + for (final header in requestModel.requestHeaders!) { + result = """$result .addHeader("${header.k}", "${header.v}")\n"""; + } + return result; + } + + String addRequestMethod(RequestModel requestModel) { + String result = ""; + if (requestModel.method != HTTPVerb.get && + requestModel.method != HTTPVerb.head && + requestModel.method != HTTPVerb.delete) { + result = """$result .${requestModel.method.name}(body)\n"""; + } else if (requestModel.method == HTTPVerb.head) { + result = """$result .${requestModel.method.name}()\n"""; + } + if (requestModel.method == HTTPVerb.delete) { + result = """$result .method("DELETE", body)\n"""; + } + return result; + } +} diff --git a/lib/consts.dart b/lib/consts.dart index f6b812a9..f1390a3c 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -228,6 +228,14 @@ const kMethodsWithBody = [ const kDefaultHttpMethod = HTTPVerb.get; const kDefaultContentType = ContentType.json; +enum CodegenLanguage { + dartHttp("Dart (http)"), + kotlinOkHttp("Kotlin (OkHttp)"); + + const CodegenLanguage(this.label); + final String label; +} + const JsonEncoder kEncoder = JsonEncoder.withIndent(' '); const LineSplitter kSplitter = LineSplitter(); diff --git a/lib/providers/ui_providers.dart b/lib/providers/ui_providers.dart index da817bb9..a9732df8 100644 --- a/lib/providers/ui_providers.dart +++ b/lib/providers/ui_providers.dart @@ -1,3 +1,4 @@ +import 'package:apidash/consts.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; final navRailIndexStateProvider = StateProvider((ref) => 0); @@ -7,3 +8,4 @@ final sentRequestIdStateProvider = StateProvider((ref) => null); final codePaneVisibleStateProvider = StateProvider((ref) => false); final saveDataStateProvider = StateProvider((ref) => false); final clearDataStateProvider = StateProvider((ref) => false); +final codegenLanguageStateProvider = StateProvider((ref) => CodegenLanguage.dartHttp); \ No newline at end of file diff --git a/lib/screens/home_page/editor_pane/details_card/code_pane.dart b/lib/screens/home_page/editor_pane/details_card/code_pane.dart index 4317e3b4..ee54c8da 100644 --- a/lib/screens/home_page/editor_pane/details_card/code_pane.dart +++ b/lib/screens/home_page/editor_pane/details_card/code_pane.dart @@ -13,7 +13,7 @@ class CodePane extends ConsumerStatefulWidget { } class _CodePaneState extends ConsumerState { - final DartHttpCodeGen dartHttpCodeGen = DartHttpCodeGen(); + final Codegen codegen = Codegen(); @override void initState() { @@ -22,10 +22,15 @@ class _CodePaneState extends ConsumerState { @override Widget build(BuildContext context) { + final CodegenLanguage codegenLanguage = + ref.watch(codegenLanguageStateProvider); + final activeRequestModel = ref.watch(activeRequestModelProvider); final defaultUriScheme = ref.watch(settingsProvider.select((value) => value.defaultUriScheme)); - final code = dartHttpCodeGen.getCode(activeRequestModel!, defaultUriScheme); + + final code = + codegen.getCode(codegenLanguage, activeRequestModel!, defaultUriScheme); if (code == null) { return const ErrorMessage( message: "An error was encountered while generating code. $kRaiseIssue", @@ -33,6 +38,12 @@ class _CodePaneState extends ConsumerState { } return ViewCodePane( code: code, + codegenLanguage: codegenLanguage, + onChangedCodegenLanguage: (CodegenLanguage? value) { + ref + .read(codegenLanguageStateProvider.notifier) + .update((state) => value!); + }, ); } } diff --git a/lib/widgets/codegen_previewer.dart b/lib/widgets/codegen_previewer.dart index 8edd624c..5833d547 100644 --- a/lib/widgets/codegen_previewer.dart +++ b/lib/widgets/codegen_previewer.dart @@ -2,8 +2,9 @@ import 'package:flutter/material.dart'; import 'package:highlighter/highlighter.dart' show highlight; import 'package:apidash/consts.dart'; import 'package:apidash/utils/utils.dart'; -import 'code_previewer.dart' show convert; -import 'buttons.dart'; +import 'code_previewer.dart'; +import 'widgets.dart' + show CopyButton, DropdownButtonCodegenLanguage, SaveInDownloadsButton; class CodeGenPreviewer extends StatefulWidget { const CodeGenPreviewer({ @@ -105,9 +106,13 @@ class ViewCodePane extends StatefulWidget { const ViewCodePane({ super.key, required this.code, + required this.codegenLanguage, + required this.onChangedCodegenLanguage, }); final String code; + final CodegenLanguage codegenLanguage; + final Function(CodegenLanguage?) onChangedCodegenLanguage; @override State createState() => _ViewCodePaneState(); @@ -144,6 +149,10 @@ class _ViewCodePaneState extends State { style: Theme.of(context).textTheme.titleMedium, ), ), + DropdownButtonCodegenLanguage( + codegenLanguage: widget.codegenLanguage, + onChanged: widget.onChangedCodegenLanguage, + ), CopyButton(toCopy: widget.code), SaveInDownloadsButton( content: stringToBytes(widget.code), diff --git a/lib/widgets/dropdowns.dart b/lib/widgets/dropdowns.dart index 92c7dcfb..20e4a078 100644 --- a/lib/widgets/dropdowns.dart +++ b/lib/widgets/dropdowns.dart @@ -109,3 +109,55 @@ class _DropdownButtonContentTypeState extends State { ); } } + +class DropdownButtonCodegenLanguage extends StatefulWidget { + const DropdownButtonCodegenLanguage({ + Key? key, + this.codegenLanguage, + this.onChanged, + }) : super(key: key); + + @override + State createState() => + _DropdownButtonCodegenLanguageState(); + final CodegenLanguage? codegenLanguage; + final void Function(CodegenLanguage?)? onChanged; +} + +class _DropdownButtonCodegenLanguageState + extends State { + @override + Widget build(BuildContext context) { + final surfaceColor = Theme.of(context).colorScheme.surface; + return DropdownButton( + focusColor: surfaceColor, + value: widget.codegenLanguage, + icon: const Icon( + Icons.unfold_more_rounded, + size: 16, + ), + elevation: 4, + style: kCodeStyle.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + underline: Container( + height: 0, + ), + onChanged: widget.onChanged, + borderRadius: kBorderRadius12, + items: CodegenLanguage.values + .map>((CodegenLanguage value) { + return DropdownMenuItem( + value: value, + child: Padding( + padding: kPs8, + child: Text( + value.label, + style: kTextStyleButton, + ), + ), + ); + }).toList(), + ); + } +} \ No newline at end of file diff --git a/test/codegen/kotlin_okhttp_codegen_test.dart b/test/codegen/kotlin_okhttp_codegen_test.dart new file mode 100644 index 00000000..2582c4ae --- /dev/null +++ b/test/codegen/kotlin_okhttp_codegen_test.dart @@ -0,0 +1,163 @@ +import 'package:apidash/codegen/kotlin/pkg_okhttp.dart'; +import 'package:apidash/models/models.dart' show KVRow, RequestModel; +import 'package:test/test.dart'; +import 'package:apidash/consts.dart'; + +void main() { + group('KotlinOkHttpCodeGen', () { + final kotlinOkHttpCodeGen = KotlinOkHttpCodeGen(); + + test('getCode returns valid code for GET request', () { + const requestModel = RequestModel( + url: 'https://jsonplaceholder.typicode.com/todos/1', + method: HTTPVerb.get, + id: '', + ); + const expectedCode = """import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.RequestBody.Companion.asRequestBody +import java.io.File +import java.util.concurrent.TimeUnit + +val client = OkHttpClient() +val request = Request.Builder() + .url("https://jsonplaceholder.typicode.com/todos/1") + .build() +val response = client.newCall(request).execute() + +println(response.body!!.string()) +"""; + expect(kotlinOkHttpCodeGen.getCode(requestModel), expectedCode); + }); + + test('getCode returns valid code for POST request', () { + const requestModel = RequestModel( + url: 'https://jsonplaceholder.typicode.com/posts', + method: HTTPVerb.post, + requestBody: '{"title": "foo","body": "bar","userId": 1}', + requestBodyContentType: ContentType.json, + id: '1', + ); + const expectedCode = """import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.RequestBody.Companion.asRequestBody +import java.io.File +import java.util.concurrent.TimeUnit + +val client = OkHttpClient() +val mediaType = "application/json".toMediaType() +val body = "{\"title\": \"foo\",\"body\": \"bar\",\"userId\": 1}".toRequestBody(mediaType) +val request = Request.Builder() + .url("https://jsonplaceholder.typicode.com/posts") + .post(body) + .build() +val response = client.newCall(request).execute() + +println(response.body!!.string()) +"""; + expect(kotlinOkHttpCodeGen.getCode(requestModel), expectedCode); + }); + + test('getCode returns valid code for DELETE request', () { + const requestModel = RequestModel( + url: 'https://jsonplaceholder.typicode.com/posts/1', + method: HTTPVerb.delete, + requestBody: '{"title": "foo","body": "bar","userId": 1}', + requestBodyContentType: ContentType.json, + id: '1', + ); + const expectedCode = """import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.RequestBody.Companion.asRequestBody +import java.io.File +import java.util.concurrent.TimeUnit + +val client = OkHttpClient() +val mediaType = "application/json".toMediaType() +val body = "{\"title\": \"foo\",\"body\": \"bar\",\"userId\": 1}".toRequestBody(mediaType) +val request = Request.Builder() + .url("https://jsonplaceholder.typicode.com/posts/1") + .method("DELETE", body) + .build() +val response = client.newCall(request).execute() + +println(response.body!!.string()) +"""; + expect(kotlinOkHttpCodeGen.getCode(requestModel), expectedCode); + }); + + test('getCode returns valid code for HEAD request', () { + const requestModel = RequestModel( + url: 'https://jsonplaceholder.typicode.com/posts/1', + method: HTTPVerb.head, + id: '1', + ); + const expectedCode = """import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.RequestBody.Companion.asRequestBody +import java.io.File +import java.util.concurrent.TimeUnit + +val client = OkHttpClient() +val request = Request.Builder() + .url("https://jsonplaceholder.typicode.com/posts/1") + .head() + .build() +val response = client.newCall(request).execute() + +println(response.body!!.string()) +"""; + expect(kotlinOkHttpCodeGen.getCode(requestModel), expectedCode); + }); + + test( + 'getCode returns valid code for requests with headers and query parameters', + () { + const requestModel = RequestModel( + url: 'https://jsonplaceholder.typicode.com/posts', + method: HTTPVerb.get, + requestParams: [ + KVRow('userId', 1), + ], + requestHeaders: [ + KVRow('Custom-Header-1', 'Value-1'), + KVRow('Custom-Header-2', 'Value-2') + ], + id: '1', + ); + const expectedCode = """import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.RequestBody.Companion.asRequestBody +import java.io.File +import java.util.concurrent.TimeUnit + +val client = OkHttpClient() +val request = Request.Builder() + .url("https://jsonplaceholder.typicode.com/posts") + .addQueryParameter("userId", "1") + .addHeader("Custom-Header-1", "Value-1") + .addHeader("Custom-Header-2", "Value-2") + .build() +val response = client.newCall(request).execute() + +println(response.body!!.string()) +"""; + expect(kotlinOkHttpCodeGen.getCode(requestModel), expectedCode); + }); + }); +} diff --git a/test/widgets/codegen_previewer_test.dart b/test/widgets/codegen_previewer_test.dart index c5db6728..4b51aa8a 100644 --- a/test/widgets/codegen_previewer_test.dart +++ b/test/widgets/codegen_previewer_test.dart @@ -1,6 +1,7 @@ +import 'package:apidash/widgets/widgets.dart' + show ViewCodePane, CodeGenPreviewer; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:apidash/widgets/codegen_previewer.dart'; import 'package:apidash/consts.dart'; import '../test_consts.dart'; @@ -58,6 +59,8 @@ void main() async { Expanded( child: ViewCodePane( code: code, + codegenLanguage: CodegenLanguage.dartHttp, + onChangedCodegenLanguage: (p0) {}, ), ), ], @@ -84,6 +87,8 @@ void main() async { Expanded( child: ViewCodePane( code: code, + codegenLanguage: CodegenLanguage.dartHttp, + onChangedCodegenLanguage: (p0) {}, ), ), ],