diff --git a/example/lib/main.dart b/example/lib/main.dart index 761fa17..8774fe7 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -44,10 +44,14 @@ class _TodoPageState extends State { late final _dio = Dio( BaseOptions( - sendTimeout: const Duration(seconds: 30), - connectTimeout: const Duration(seconds: 30), - receiveTimeout: const Duration(seconds: 30), - ), + sendTimeout: const Duration(seconds: 30), + connectTimeout: const Duration(seconds: 30), + receiveTimeout: const Duration(seconds: 30), + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': '*', + }), ); final _chuckerHttpClient = ChuckerHttpClient(http.Client()); @@ -81,7 +85,7 @@ class _TodoPageState extends State { switch (_clientType) { case _Client.dio: - _dio.get('$_baseUrl$path', queryParameters: {'userId': 1}); + _dio.get('$_baseUrl$path', queryParameters: {'userId': '1'}); break; case _Client.http: _chuckerHttpClient.get(Uri.parse('$_baseUrl$path?userId=1')); diff --git a/lib/src/http/chucker_http_client.dart b/lib/src/http/chucker_http_client.dart index 5371192..3163fbc 100644 --- a/lib/src/http/chucker_http_client.dart +++ b/lib/src/http/chucker_http_client.dart @@ -129,8 +129,10 @@ class ChuckerHttpClient extends BaseClient { statusCode: statusCode, connectionTimeout: 0, contentType: request.headers['Content-Type'], - headers: request.headers.toString(), - queryParameters: request.url.queryParameters.toString(), + // headers: request.headers.toString(), + headers: request.headers.cast(), + // queryParameters: request.url.queryParameters.toString(), + queryParameters: request.url.queryParameters, receiveTimeout: 0, request: requestBody, requestSize: request.contentLength?.toDouble() ?? 0, diff --git a/lib/src/interceptors/chopper_interceptor.dart b/lib/src/interceptors/chopper_interceptor.dart index ec58ccb..9ae6f7d 100644 --- a/lib/src/interceptors/chopper_interceptor.dart +++ b/lib/src/interceptors/chopper_interceptor.dart @@ -46,10 +46,12 @@ class ChuckerChopperInterceptor implements ResponseInterceptor { statusCode: response.statusCode, connectionTimeout: 0, contentType: _requestType(response), - headers: response.base.headers.toString(), - queryParameters: - response.base.request?.url.queryParameters.toString() ?? - emptyString, + // headers: response.base.headers.toString(), + headers: Map.from(response.base.headers), + // queryParameters: + // response.base.request?.url.queryParameters.toString() ?? + // emptyString, + queryParameters: response.base.request?.url.queryParameters ?? {}, receiveTimeout: 0, request: _requestBody(response), requestSize: 2, diff --git a/lib/src/interceptors/dio_interceptor.dart b/lib/src/interceptors/dio_interceptor.dart index 72d8d83..8ba632b 100644 --- a/lib/src/interceptors/dio_interceptor.dart +++ b/lib/src/interceptors/dio_interceptor.dart @@ -86,8 +86,11 @@ class ChuckerDioInterceptor extends Interceptor { connectionTimeout: response.requestOptions.connectTimeout?.inMilliseconds ?? 0, contentType: response.requestOptions.contentType, - headers: response.requestOptions.headers.toString(), - queryParameters: response.requestOptions.queryParameters.toString(), + // headers: response.requestOptions.headers.toString(), + headers: response.requestOptions.headers.cast(), + // queryParameters: response.requestOptions.queryParameters.toString(), + queryParameters: + response.requestOptions.queryParameters.cast(), receiveTimeout: response.requestOptions.receiveTimeout?.inMilliseconds ?? 0, request: _separateFileObjects(response.requestOptions).data, @@ -122,8 +125,11 @@ class ChuckerDioInterceptor extends Interceptor { connectionTimeout: response.requestOptions.connectTimeout?.inMilliseconds ?? 0, contentType: response.requestOptions.contentType, - headers: response.requestOptions.headers.toString(), - queryParameters: response.requestOptions.queryParameters.toString(), + // headers: response.requestOptions.headers.toString(), + headers: response.requestOptions.headers.cast(), + // queryParameters: response.requestOptions.queryParameters.toString(), + queryParameters: + response.requestOptions.queryParameters.cast(), receiveTimeout: response.requestOptions.receiveTimeout?.inMilliseconds ?? 0, request: _separateFileObjects(response.requestOptions).data, diff --git a/lib/src/models/api_response.dart b/lib/src/models/api_response.dart index 7ca50c5..3a460af 100644 --- a/lib/src/models/api_response.dart +++ b/lib/src/models/api_response.dart @@ -1,8 +1,9 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; -///[ApiResponse] is the api data model to save and retrieve from local disk +/// [ApiResponse] is the API data model to save and retrieve from local disk class ApiResponse { - ///[ApiResponse] is the api data model to save and retrieve from local disk + /// [ApiResponse] is the API data model to save and retrieve from local disk ApiResponse({ required this.body, required this.baseUrl, @@ -12,7 +13,9 @@ class ApiResponse { required this.connectionTimeout, required this.contentType, required this.headers, + // required this.headersMap, required this.queryParameters, + // required this.queryParametersMap, required this.receiveTimeout, required this.request, required this.requestSize, @@ -25,7 +28,7 @@ class ApiResponse { required this.clientLibrary, }); - ///Convert json to [ApiResponse] + /// Convert JSON to [ApiResponse] factory ApiResponse.fromJson(Map json) => ApiResponse( body: json['body'] as dynamic, baseUrl: json['baseUrl'] as String, @@ -33,13 +36,13 @@ class ApiResponse { statusCode: json['statusCode'] as int, connectionTimeout: json['connectionTimeout'] as int, contentType: json['contentType'] as String?, - headers: json['headers'] as String, - queryParameters: json['queryParameters'] as String, + headers: _parseMap(json['headers']), + queryParameters: _parseMap(json['queryParameters']), receiveTimeout: json['receiveTimeout'] as int, request: json['request'] as dynamic, - requestSize: json['requestSize'] as double, + requestSize: (json['requestSize'] as num).toDouble(), requestTime: DateTime.parse(json['requestTime'] as String), - responseSize: json['responseSize'] as double, + responseSize: (json['responseSize'] as num).toDouble(), responseTime: DateTime.parse(json['responseTime'] as String), responseType: json['responseType'] as String, sendTimeout: json['sendTimeout'] as int, @@ -48,7 +51,40 @@ class ApiResponse { clientLibrary: (json['clientLibrary'] as String?) ?? 'N/A', ); - ///Mocked instance of [ApiResponse]. ***ONLY FOR TESTING**** + /// Helper function to parse JSON strings into a Map + static Map _parseMap(dynamic jsonString) { + if (jsonString is String && jsonString.isNotEmpty && jsonString != '{}') { + try { + final Map parsed = + jsonDecode(jsonString) as Map; + return parsed.map((key, value) => MapEntry(key, value.toString())); + } catch (e) { + debugPrint('Failed to parse JSON: $e'); + return {}; + } + } else if (jsonString is Map) { + return jsonString.map((key, value) => MapEntry(key, value.toString())); + } else { + return {}; + } + } + + /// Helper function to parse JSON strings into a Map + // static Map _parseMap(String jsonString) { + // if (jsonString.isEmpty || jsonString == '{}') { + // return {}; + // } + // try { + // final Map parsed = + // jsonDecode(jsonString) as Map; + // return parsed.map((key, value) => MapEntry(key, value.toString())); + // } catch (e) { + // debugPrint('Failed to parse JSON: $e'); + // return {}; + // } + // } + + /// Mocked instance of [ApiResponse]. ***ONLY FOR TESTING**** factory ApiResponse.mock() => ApiResponse( body: {'': ''}, baseUrl: '', @@ -57,8 +93,14 @@ class ApiResponse { statusCode: 200, connectionTimeout: 0, contentType: 'application/json', - headers: '', - queryParameters: '', + // headers: '', + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': '*', + }, + // queryParameters: '', + queryParameters: {}, receiveTimeout: 0, request: {'': ''}, requestSize: 0, @@ -71,72 +113,80 @@ class ApiResponse { clientLibrary: '', ); - ///DateTime when request is sent + /// DateTime when request is sent final DateTime requestTime; - ///DateTime when response is received + /// DateTime when response is received final DateTime responseTime; /// Request base url, it can contain sub path. final String baseUrl; - /// Api end-point + /// API end-point final String path; - ///Http method such `GET` + /// HTTP method such as `GET` final String method; - ///Http status code. For more details, visit [https://developer.mozilla.org/en-US/docs/Web/HTTP/Status] + /// HTTP status code. For more details, visit [https://developer.mozilla.org/en-US/docs/Web/HTTP/Status] final int statusCode; - ///Size of request data + /// Size of request data final double requestSize; - ///Size of response data + /// Size of response data final double responseSize; - ///Request data + /// Request data final dynamic request; - ///Response data + /// Response data final dynamic body; - ///Request data type + /// Request data type final String? contentType; - ///Request headers - final String headers; + /// Request headers + // final String headers; + + /// Headers parsed as a Map + final Map headers; - ///Timeout in milliseconds for sending data + /// Timeout in milliseconds for sending data final int sendTimeout; - ///Response data type + /// Response data type final String responseType; - ///Timeout in milliseconds for receiving data + /// Timeout in milliseconds for receiving data final int receiveTimeout; - ///Request query params - final String queryParameters; + /// Request query params + // final String queryParameters; - ///Timeout in milliseconds for making connection + /// Query parameters parsed as a Map + final Map queryParameters; + + /// Timeout in milliseconds for making connection final int connectionTimeout; - ///To check whether user has selected this instance or not + /// To check whether user has selected this instance or not final bool checked; - ///The client which is used for network call + /// The client which is used for network call final String clientLibrary; - ///Convert [ApiResponse] to json. + /// Convert [ApiResponse] to JSON. Map toJson() { return { 'body': body, 'connectionTimeout': connectionTimeout, 'contentType': contentType, 'headers': headers, + // 'headersMap': headersMap, 'method': method, 'queryParameters': queryParameters, + // 'queryParametersMap': queryParametersMap, 'receiveTimeout': receiveTimeout, 'request': request, 'requestSize': requestSize, @@ -153,7 +203,7 @@ class ApiResponse { }; } - ///Copies current data and returns new object + /// Copies current data and returns new object ApiResponse copyWith({ DateTime? requestTime, DateTime? responseTime, @@ -167,11 +217,13 @@ class ApiResponse { String? response, dynamic body, String? contentType, - String? headers, + // String? headers, + Map? headers, int? sendTimeout, String? responseType, int? receiveTimeout, - String? queryParameters, + // String? queryParameters, + Map? queryParametersMap, int? connectionTimeout, bool? checked, String? clientLibrary, @@ -184,8 +236,10 @@ class ApiResponse { statusCode: statusCode ?? this.statusCode, connectionTimeout: connectionTimeout ?? this.connectionTimeout, contentType: contentType ?? this.contentType, + // headers: headers ?? this.headers, headers: headers ?? this.headers, - queryParameters: queryParameters ?? this.queryParameters, + // queryParameters: queryParameters ?? this.queryParameters, + queryParameters: queryParametersMap ?? this.queryParameters, receiveTimeout: receiveTimeout ?? this.receiveTimeout, request: request ?? this.request, requestSize: requestSize ?? this.requestSize, @@ -227,24 +281,46 @@ $prettyJsonRequest $prettyJson'''; } - ///Formatted json response string + /// Formatted JSON response string String get prettyJson { return const JsonEncoder.withIndent(' ').convert(body); } - ///Formatted json response string + /// Formatted JSON request string String get prettyJsonRequest { return const JsonEncoder.withIndent(' ').convert(request); } - @override + /// Headers parsed as a Map + // Map get headersMap { + // debugPrint('Headers: $headers'); // Tambahkan ini untuk debugging + // try { + // final Map parsed = + // jsonDecode(headers) as Map; + // return parsed.map((key, value) => MapEntry(key, value.toString())); + // } catch (e) { + // debugPrint('Failed to parse headers: $e'); + // return {}; + // } + // } + + // Map get queryParametersMap { + // debugPrint( + // 'Query Parameters: $queryParameters'); // Tambahkan ini untuk debugging + // try { + // final Map parsed = + // jsonDecode(queryParameters) as Map; + // return parsed.map((key, value) => MapEntry(key, value.toString())); + // } catch (e) { + // debugPrint('Failed to parse query parameters: $e'); + // return {}; + // } + // } - ///Equates [other] to this - // ignore: avoid_equals_and_hash_code_on_mutable_classes + @override bool operator ==(Object other) => other is ApiResponse && other.requestTime == requestTime; @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => requestTime.millisecondsSinceEpoch; } diff --git a/lib/src/view/api_detail_page.dart b/lib/src/view/api_detail_page.dart index b14b6c2..590175d 100644 --- a/lib/src/view/api_detail_page.dart +++ b/lib/src/view/api_detail_page.dart @@ -8,6 +8,7 @@ import 'package:chucker_flutter/src/view/tabs/overview.dart'; import 'package:chucker_flutter/src/view/widgets/app_bar.dart'; import 'package:chucker_flutter/src/view/widgets/sizeable_text_button.dart'; import 'package:flutter/material.dart'; +import 'dart:convert'; import 'package:flutter/services.dart'; import 'package:share_plus/share_plus.dart'; @@ -30,6 +31,7 @@ class _ApiDetailsPageState extends State { @override Widget build(BuildContext context) { + // deb('ApiDetailsPage build ${widget.api.toString()}'); return Directionality( textDirection: Localization.textDirection, child: Scaffold( @@ -56,6 +58,14 @@ class _ApiDetailsPageState extends State { }, icon: const Icon(Icons.share), ), + TextButton( + onPressed: () { + Clipboard.setData( + ClipboardData(text: _cURLRepresentation(widget.api))); + }, + child: const Text('Copy cURL Command', + style: TextStyle(color: Colors.white)), + ), ], ), body: SafeArea( @@ -126,6 +136,41 @@ class _ApiDetailsPageState extends State { } } +String _cURLRepresentation(ApiResponse api) { + List components = ['curl -i']; + + if (api.method.toUpperCase() != 'GET') { + components.add('-X ${api.method}'); + } + + api.headers.forEach((k, v) { + if (k != 'Cookie') { + components.add('-H "$k: $v"'); + } + }); + + if (api.body != null && api.body.toString().isNotEmpty) { + final encodedBody = api.body.toString().replaceAll('"', '\\"'); + components.add('-d "$encodedBody"'); + } + + // Construct the full URL manually + final queryParams = api.queryParameters.isNotEmpty + ? api.queryParameters.entries.map((e) { + final key = Uri.encodeComponent(e.key); + final value = Uri.encodeComponent(e.value.toString()); + return '$key=$value'; + }).join('&') + : ''; + + final fullUrl = + api.baseUrl + api.path + (queryParams.isNotEmpty ? '?$queryParams' : ''); + + components.add('"$fullUrl"'); + + return components.join(' \\\n\t'); +} + class _PreviewModeControl extends StatelessWidget { const _PreviewModeControl({ required this.jsonPreviewType, diff --git a/lib/src/view/tabs/overview.dart b/lib/src/view/tabs/overview.dart index cdf56e2..a8a5dc4 100644 --- a/lib/src/view/tabs/overview.dart +++ b/lib/src/view/tabs/overview.dart @@ -72,11 +72,12 @@ class OverviewTabView extends StatelessWidget { attribute: 'Response Time', value: api.responseTime.toString(), ), - _dataRow(context, attribute: 'Headers', value: api.headers), + _dataRow(context, + attribute: 'Headers', value: api.headers.toString()), _dataRow( context, attribute: 'Query Parameters', - value: api.queryParameters, + value: api.queryParameters.toString(), ), _dataRow( context,