diff --git a/README.md b/README.md index 2719f747..03b97b25 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ # API Dash ⚡️ -[![Discord Server Invite](https://img.shields.io/badge/DISCORD-JOIN%20SERVER-5663F7?style=for-the-badge&logo=discord&logoColor=white)](https://discord.com/invite/bBeSdtJ6Ue) - -GSoC - +Discord Server Invite
+foss42%2Fapidash | Trendshift     GSoC ### Please support this initiative by giving this project a Star ⭐️ @@ -138,6 +136,13 @@ API Dash can be downloaded from the links below: - Customize various options using a dedicated Settings screen. - Window Configuration (Size & Position) is persisted and restored on app start. (Only macOS & Windows) + +## Supported by ❤️ + + + lambdatest-logo + + ## Code Generators API Dash currently supports API integration code generation for the following languages/libraries. diff --git a/doc/dev_guide/openapi.md b/doc/dev_guide/openapi.md new file mode 100644 index 00000000..aa9b87f8 --- /dev/null +++ b/doc/dev_guide/openapi.md @@ -0,0 +1,111 @@ +# OpenAPI + +The OpenAPI import feature was failing when trying to import specifications from URLs like https://catfact.ninja/docs?api-docs.json. The error "The fetched content does not look like a valid OpenAPI spec (JSON or YAML)" was shown even though the content was a valid OpenAPI 3.0 specification. This was caused by a bug in the openapi_spec package (v0.15.0) that cannot parse OpenAPI specs containing "security": [[]] (empty security arrays), which is valid according to the OpenAPI 3.0 specification. + +> Fix + +Added a workaround in OpenApiImportService.tryParseSpec() that detects parsing failures and automatically removes problematic security fields containing empty arrays before retrying the parse operation. This is a temporary workaround until the upstream package is fixed. + +- [APIDash](https://drive.google.com/file/d/1CWocxCVW99-bEWkZlwInGq0JykHalv9a/view?usp=sharing) - Works without any fix +- [Cat Fact API](https://drive.google.com/file/d/1ox71b3tT4Lv-9jw7zV1ronWQR_uW3K25/view?usp=drive_link) - Works with this fix +- [DigitalOcean Droplet Metadata API](https://drive.google.com/file/d/1XKZXJvrwvAVm3OVBEZFhScOuCMjPJBZh/view?usp=drive_link) - Works without any fix +- [GitHub v3 REST API](https://drive.google.com/file/d/1WcJXSosHPD0uiybJrqpJSknM5FA0De02/view?usp=drive_link) - Doesn't Work +- [Swagger Petstore](https://drive.google.com/file/d/1LBqBrlcsXo7Clr7VKn7CYe75c_H4U8zQ/view?usp=drive_link) - Doesn't Work +- [RailwayStations REST API](https://drive.google.com/file/d/1jVFk-hNf_gb_VeBuAomOgh6tWByU9Fyi/view?usp=drive_link) - Doesn't Work +- [UniProt REST API Server](https://drive.google.com/file/d/1KTIqKC7SludxsyCYN6kXWQySve4GpbhD/view?usp=drive_link) - Doesn't Work +- [VIT-AP VTOP API](https://drive.google.com/file/d/1B5Mh3IK2uUBoRSocEKQd2Dvf7SZWm03M/view?usp=drive_link) - Works without any fix + +It’s not our parser that causes the issue. The failures come from the documents themselves and how the openapi_spec package (correctly) enforces OpenAPI shapes. Valid security fields work fine as per the package docs; the broken cases are due to invalid spec content. + +### Findings per document + +- cat_facts.json (also the Cat Facts URL) + + - Problem: Top-level security is malformed: security: [[]] + - Why it fails: In OpenAPI 3.0, top-level security must be an array of SecurityRequirement objects (maps). Examples: + - Valid: security: [] (no requirements) or security: [ { api_key: [] } ] + - Invalid: security: [[]] (array of arrays) + - openapi_spec error: type 'List' is not a subtype of type 'Map' + - Conclusion: The document is invalid. This is not a general “security field” issue, just this malformed shape. + +- railway-stations.yaml + + - Problem: Component parameter reference points to a Parameter missing required fields (e.g., 'in'). + - Error: CheckedFromJsonException: Could not create Parameter. There is a problem with "in". Invalid union type "null"! + - The stack/message points at $ref: #/components/parameters/Authorization. + - Conclusion: Not related to security. The referenced Parameter definition is incomplete (missing in: header|query|path|cookie) or otherwise invalid. + +- travel.yaml + + - Problem: Same class of failure as railway-stations.yaml, with a parameter ref like $ref: #/components/parameters/page. + - Error: CheckedFromJsonException... problem with "in" (Invalid union type "null"). + - Note: components.securitySchemes is present here and is not the cause. + - Conclusion: Also a spec issue with parameter component definitions/references. + +- digitalocean.yaml + - Result: Parses successfully with openapi_spec. + - Note: No top-level security; nothing problematic here. + - Conclusion: Confirms the parser handles valid documents correctly. + +Steps to reproduce failures from local files, + +``` +import 'dart:io'; +import 'package:openapi_spec/openapi_spec.dart'; + +void main(List args) async { + + // Pass file paths as args below. + final paths = args.isNotEmpty + ? args + : [ + './cat_facts.json', + './railway-stations.yaml', + ]; + + for (final p in paths) { + stdout.writeln('\n=== Parsing: $p ==='); + final f = File(p); + if (!await f.exists()) { + stdout.writeln('Skip: file not found'); + continue; + } + + final content = await f.readAsString(); + + try { + final spec = OpenApi.fromString(source: content, format: null); + stdout.writeln('SUCCESS: title="${spec.info.title}", version="${spec.info.version}"'); + stdout.writeln('Paths: ${spec.paths?.length ?? 0}'); + } catch (e, st) { + final err = e.toString(); + stdout.writeln('FAIL: ${err.substring(0, err.length.clamp(0, 400))}...'); + // Stack Trace + final stStr = st.toString(); + if (stStr.isNotEmpty) { + stdout.writeln('Stack:\n$stStr'); + } + } + } +} +``` + +### How to run + +- Create a new dart project, put the openapi spec file and this script there. +- Add the depndency, `dart pub add openapi_spec: ^0.15.0` +- Run: + - `dart run path/to/this/file` + +### Expected outcomes + +- `cat_facts.json` + + - FAIL with an error like: + - type 'List' is not a subtype of type 'Map' + - This is triggered by the invalid top-level security shape: security: [[]] + +- `railway-stations.yaml` + - FAIL with an error like: + - CheckedFromJsonException: Could not create `Parameter`. There is a problem with "in". Invalid union type "null"! + - This points to a components/parameters reference missing required “in”. diff --git a/lib/dashbot/services/openapi_import_service.dart b/lib/dashbot/services/openapi_import_service.dart index 954f649d..b5bcc90b 100644 --- a/lib/dashbot/services/openapi_import_service.dart +++ b/lib/dashbot/services/openapi_import_service.dart @@ -132,15 +132,53 @@ class OpenApiImportService { /// Try to parse a JSON or YAML OpenAPI spec string. /// Returns null if parsing fails. + /// + /// NOTE: There's a known issue with the openapi_spec package where + /// security fields containing empty arrays (e.g., "security": [[]]) + /// cause parsing failures. This method includes a workaround. static OpenApi? tryParseSpec(String source) { try { - // Let the library infer JSON/YAML return OpenApi.fromString(source: source, format: null); - } catch (_) { + } catch (e) { + // Try workaround for security field parsing issues + try { + final processedSource = _removeProblematicSecurityField(source); + if (processedSource != source) { + return OpenApi.fromString(source: processedSource, format: null); + } + } catch (_) { + // Workaround failed, fall through to return null + } return null; } } + /// Removes problematic security fields that cause parsing issues. + /// TODO: Remove this workaround once openapi_spec package fixes + /// the issue with security fields containing empty arrays. + static String _removeProblematicSecurityField(String source) { + try { + final spec = jsonDecode(source) as Map; + + if (spec.containsKey('security')) { + final security = spec['security']; + if (security is List && _hasEmptySecurityArrays(security)) { + spec.remove('security'); + return jsonEncode(spec); + } + } + + return source; + } catch (e) { + throw FormatException('Failed to preprocess OpenAPI spec: $e'); + } + } + + /// Checks if security list contains empty arrays that cause parsing issues. + static bool _hasEmptySecurityArrays(List security) { + return security.any((item) => item is List && item.isEmpty); + } + /// Build a single request payload from a path + method operation. /// The payload mirrors CurlImportService payload shape for reuse. static Map _payloadForOperation({ diff --git a/lib/main.dart b/lib/main.dart index 510c28b0..2b80ff65 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,7 +14,6 @@ void main() async { await Stac.initialize(); //Load all LLMs - // await LLMManager.fetchAvailableLLMs(); await ModelManager.fetchAvailableModels(); var settingsModel = await getSettingsFromSharedPrefs(); @@ -30,9 +29,6 @@ void main() async { settingsModel = settingsModel?.copyWithPath(workspaceFolderPath: null); } - // TODO: Load all models at init - // await ModelManager.loadAvailableLLMs(); - runApp( ProviderScope( overrides: [ diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 51de409b..68c45cfd 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -250,12 +250,15 @@ class CollectionStateNotifier newModel = switch (apiType) { APIType.rest || APIType.graphql => currentModel.copyWith( apiType: apiType, + requestTabIndex: 0, name: name ?? currentModel.name, description: description ?? currentModel.description, httpRequestModel: const HttpRequestModel(), - aiRequestModel: null), + aiRequestModel: null, + ), APIType.ai => currentModel.copyWith( apiType: apiType, + requestTabIndex: 0, name: name ?? currentModel.name, description: description ?? currentModel.description, httpRequestModel: null, @@ -350,8 +353,20 @@ class CollectionStateNotifier executionRequestModel.httpRequestModel!); } - // Terminal: start network log + // Terminal final terminal = ref.read(terminalStateProvider.notifier); + + var valRes = getValidationResult(substitutedHttpRequestModel); + if (valRes != null) { + terminal.logSystem( + category: 'validation', + message: valRes, + level: TerminalLevel.error, + ); + ref.read(showTerminalBadgeProvider.notifier).state = true; + } + + // Terminal: start network log final logId = terminal.startNetwork( apiType: executionRequestModel.apiType, method: substitutedHttpRequestModel.method, diff --git a/lib/providers/ui_providers.dart b/lib/providers/ui_providers.dart index 4e5d217f..638d8e0b 100644 --- a/lib/providers/ui_providers.dart +++ b/lib/providers/ui_providers.dart @@ -13,6 +13,7 @@ final historyCodePaneVisibleStateProvider = StateProvider((ref) => false); final saveDataStateProvider = StateProvider((ref) => false); final clearDataStateProvider = StateProvider((ref) => false); final hasUnsavedChangesProvider = StateProvider((ref) => false); +final showTerminalBadgeProvider = StateProvider((ref) => false); // final nameTextFieldControllerProvider = // StateProvider.autoDispose((ref) { diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index 5ff7987a..21457302 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -76,13 +76,21 @@ class Dashboard extends ConsumerWidget { style: Theme.of(context).textTheme.labelSmall, ), kVSpacer10, - IconButton( - isSelected: railIdx == 3, - onPressed: () { - ref.read(navRailIndexStateProvider.notifier).state = 3; - }, - icon: const Icon(Icons.terminal_outlined), - selectedIcon: const Icon(Icons.terminal), + Badge( + backgroundColor: Theme.of(context).colorScheme.error, + isLabelVisible: + ref.watch(showTerminalBadgeProvider) && railIdx != 3, + child: IconButton( + isSelected: railIdx == 3, + onPressed: () { + ref.read(navRailIndexStateProvider.notifier).state = + 3; + ref.read(showTerminalBadgeProvider.notifier).state = + false; + }, + icon: const Icon(Icons.terminal_outlined), + selectedIcon: const Icon(Icons.terminal), + ), ), Text( 'Logs', diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane_graphql.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane_graphql.dart index 87ed3104..65fd7956 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane_graphql.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane_graphql.dart @@ -15,7 +15,7 @@ class EditGraphQLRequestPane extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final selectedId = ref.watch(selectedIdStateProvider); - var tabIndex = ref.watch( + final tabIndex = ref.watch( selectedRequestModelProvider.select((value) => value?.requestTabIndex)); final codePaneVisible = ref.watch(codePaneVisibleStateProvider); final headerLength = ref.watch(selectedRequestModelProvider @@ -34,9 +34,6 @@ class EditGraphQLRequestPane extends ConsumerWidget { final hasAuth = ref.watch(selectedRequestModelProvider.select((value) => value?.httpRequestModel?.authModel?.type != APIAuthType.none)); - if (tabIndex >= 3) { - tabIndex = 0; - } return RequestPane( selectedId: selectedId, codePaneVisible: codePaneVisible, diff --git a/lib/services/hive_services.dart b/lib/services/hive_services.dart index 7ae90149..ff3f1888 100644 --- a/lib/services/hive_services.dart +++ b/lib/services/hive_services.dart @@ -1,5 +1,5 @@ import 'package:flutter/foundation.dart'; -import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hive_ce_flutter/hive_flutter.dart'; import 'secure_credential_storage.dart'; enum HiveBoxType { normal, lazy } diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 72b57468..bca76b79 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -8,4 +8,5 @@ export 'http_utils.dart'; export 'js_utils.dart'; export 'save_utils.dart'; export 'ui_utils.dart'; +export 'validation_utils.dart'; export 'window_utils.dart'; diff --git a/lib/utils/validation_utils.dart b/lib/utils/validation_utils.dart new file mode 100644 index 00000000..261faa08 --- /dev/null +++ b/lib/utils/validation_utils.dart @@ -0,0 +1,18 @@ +import 'package:apidash_core/apidash_core.dart'; + +String? getValidationResult(HttpRequestModel requestModel) { + if (requestModel.url.trim().isEmpty) { + return 'Request URL is empty. Please provide a valid URL.'; + } + if (requestModel.method == HTTPVerb.get && requestModel.hasAnyBody) { + return 'GET request contains a body. This is not supported.'; + } + if (requestModel.hasJsonData) { + try { + kJsonDecoder.convert(requestModel.body!); + } catch (e) { + return 'Invalid JSON in request body: ${e.toString()}'; + } + } + return null; +} diff --git a/packages/better_networking/lib/models/http_request_model.dart b/packages/better_networking/lib/models/http_request_model.dart index 3443e3d6..51caf361 100644 --- a/packages/better_networking/lib/models/http_request_model.dart +++ b/packages/better_networking/lib/models/http_request_model.dart @@ -47,6 +47,10 @@ class HttpRequestModel with _$HttpRequestModel { bool get hasTextContentType => bodyContentType == ContentType.text; int get contentLength => utf8.encode(body ?? "").length; bool get hasBody => hasJsonData || hasTextData || hasFormData; + bool get hasAnyBody => + (hasJsonContentType && contentLength > 0) || + (hasTextContentType && contentLength > 0) || + (hasFormDataContentType && formDataMapList.isNotEmpty); bool get hasJsonData => kMethodsWithBody.contains(method) && hasJsonContentType && diff --git a/pubspec.lock b/pubspec.lock index 311796c2..1817c5b3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -844,14 +844,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.3" - hive_flutter: - dependency: "direct main" + hive_ce: + dependency: transitive description: - name: hive_flutter - sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc + name: hive_ce + sha256: "81d39a03c4c0ba5938260a8c3547d2e71af59defecea21793d57fc3551f0d230" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "2.15.1" + hive_ce_flutter: + dependency: "direct main" + description: + name: hive_ce_flutter + sha256: "26d656c9e8974f0732f1d09020e2d7b08ba841b8961a02dbfb6caf01474b0e9a" + url: "https://pub.dev" + source: hosted + version: "2.3.3" hooks_riverpod: dependency: "direct main" description: @@ -944,6 +952,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + isolate_channel: + dependency: transitive + description: + name: isolate_channel + sha256: f3d36f783b301e6b312c3450eeb2656b0e7d1db81331af2a151d9083a3f6b18d + url: "https://pub.dev" + source: hosted + version: "0.2.2+1" jaspr: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8b44c46b..00428913 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,7 +37,7 @@ dependencies: fvp: ^0.32.1 highlight: ^0.7.0 highlighter: ^0.1.1 - hive_flutter: ^1.1.0 + hive_ce_flutter: ^2.3.3 hooks_riverpod: ^2.5.2 intl: ^0.19.0 jinja: ^0.6.1 diff --git a/test/dashbot/services/openapi_import_service_test.dart b/test/dashbot/services/openapi_import_service_test.dart index 918f7fa3..6dfdb699 100644 --- a/test/dashbot/services/openapi_import_service_test.dart +++ b/test/dashbot/services/openapi_import_service_test.dart @@ -31,6 +31,86 @@ void main() { expect(summary, contains('POST')); }); + test('tryParseSpec handles problematic security field with empty arrays', + () { + const specWithEmptySecurityArray = ''' +{ + "openapi": "3.0.0", + "info": { + "title": "Cat Fact API", + "version": "1.0.0" + }, + "paths": { + "/fact": { + "get": { + "responses": { + "200": { + "description": "Success" + } + } + } + } + }, + "security": [[]] +}'''; + + final result = + OpenApiImportService.tryParseSpec(specWithEmptySecurityArray); + expect(result, isNotNull); + expect(result!.info.title, equals('Cat Fact API')); + expect(result.info.version, equals('1.0.0')); + expect(result.paths, isNotNull); + expect(result.paths!.keys, contains('/fact')); + }); + + test('tryParseSpec handles valid security field with actual requirements', + () { + const specWithRealSecurity = ''' +{ + "openapi": "3.0.0", + "info": { + "title": "Secured API", + "version": "1.0.0" + }, + "paths": { + "/secured": { + "get": { + "responses": { + "200": { + "description": "Success" + } + } + } + } + }, + "security": [ + { + "api_key": [] + } + ] +}'''; + + final result = OpenApiImportService.tryParseSpec(specWithRealSecurity); + expect(result, isNotNull); + expect(result!.info.title, equals('Secured API')); + }); + + test('tryParseSpec returns null for invalid JSON', () { + const invalidSpec = 'not valid json'; + final result = OpenApiImportService.tryParseSpec(invalidSpec); + expect(result, isNull); + }); + + test('tryParseSpec returns null for non-OpenAPI JSON', () { + const nonOpenApiSpec = ''' +{ + "notOpenApi": true, + "someField": "value" +}'''; + final result = OpenApiImportService.tryParseSpec(nonOpenApiSpec); + expect(result, isNull); + }); + test('extractSpecMeta includes endpoints & baseUrl', () { final spec = OpenApiImportService.tryParseSpec(_specJson)!; final meta = OpenApiImportService.extractSpecMeta(spec);