Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support copy value in line, support create cUrl command correctly (work in Postman) #104

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

Alice is an HTTP Inspector tool for Flutter which helps debugging http requests. It catches and stores http requests and responses, which can be viewed via simple UI. It is inspired from [Chuck](https://github.com/jgilfelt/chuck) and [Chucker](https://github.com/ChuckerTeam/chucker).

**Root**

<table>
<tr>
<td>
Expand Down Expand Up @@ -51,7 +53,19 @@ Alice is an HTTP Inspector tool for Flutter which helps debugging http requests.
<img width="250px" src="https://raw.githubusercontent.com/jhomlala/alice/master/media/12.png">
</td>
</tr>
</table>

**Forked**
<table>

<tr>
<td>
<img width="250px" src="https://github.com/tronghuy5555/alice/blob/master/media/forked/1.png">
</td>
<td>
<img width="250px" src="https://github.com/tronghuy5555/alice/blob/master/media/forked/2.png">
</td>
</tr>
</table>

**Supported Dart http client plugins:**
Expand All @@ -74,14 +88,28 @@ Alice is an HTTP Inspector tool for Flutter which helps debugging http requests.
✔️ HTTP calls search
✔️ Flutter/Android logs

**Fork Features:**
✔️ Refresh call (No support for form data)
✔️ Generate curl Postman style (Must edit form files path before run it)

## Install

1. Add this to your **pubspec.yaml** file:

**Root:**
```yaml
dependencies:
alice: ^0.3.3
```
**Forked:**

```yaml
dependencies:
alice:
git:
url: https://github.com/tronghuy5555/alice.git
ref: v1.1.0
```

2. Install it

Expand Down Expand Up @@ -162,6 +190,11 @@ If you want to hide share button, you can use `showShareButton` parameter.
Alice alice = Alice(..., showShareButton: false);
```

If you want to refresh call, you can use `retryDio` parameter
```dart
Dio dio = Dio();
dio.interceptors.add(alice.getDioInterceptor(retryDio: [your Dio]));
```
### HTTP Client configuration
If you're using Dio, you just need to add interceptor.

Expand All @@ -170,7 +203,6 @@ Dio dio = Dio();
dio.interceptors.add(alice.getDioInterceptor());
```


If you're using HttpClient from dart:io package:

```dart
Expand Down
8 changes: 4 additions & 4 deletions lib/alice.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@ import 'package:alice/core/alice_dio_interceptor.dart';
import 'package:alice/core/alice_http_adapter.dart';
import 'package:alice/core/alice_http_client_adapter.dart';
import 'package:alice/model/alice_http_call.dart';
import 'package:alice/model/alice_log.dart';
import 'package:chopper/chopper.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:http/http.dart' as http;

export 'package:alice/model/alice_log.dart';

class Alice {
Expand Down Expand Up @@ -82,8 +81,9 @@ class Alice {
}

/// Get Dio interceptor which should be applied to Dio instance.
AliceDioInterceptor getDioInterceptor() {
return AliceDioInterceptor(_aliceCore);
/// [retryDio] support retry request
AliceDioInterceptor getDioInterceptor({Dio? retryDio}) {
return AliceDioInterceptor(_aliceCore, retryDio: retryDio);
}

/// Handle request from HttpClient
Expand Down
13 changes: 12 additions & 1 deletion lib/core/alice_core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,6 @@ class AliceCore {
);
final indexToReplace = originalCalls.indexOf(calls.first);
originalCalls[indexToReplace] = call;

callsSubject.add(originalCalls);
} else {
callsSubject.add([...callsSubject.value, call]);
Expand Down Expand Up @@ -301,6 +300,18 @@ class AliceCore {
callsSubject.add([]);
}

/// Remove call item
void removeCallId(int callId) {
if (callsSubject.value.isNotEmpty) {
final calls = List<AliceHttpCall>.from(callsSubject.value);
final int index = calls.indexWhere((element) => element.id == callId);
if (index >= 0) {
calls.removeAt(index);
callsSubject.add(calls);
}
}
}

AliceHttpCall? _selectCall(int requestId) =>
callsSubject.value.firstWhereOrNull((call) => call.id == requestId);

Expand Down
53 changes: 46 additions & 7 deletions lib/core/alice_dio_interceptor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import 'package:dio/dio.dart';
class AliceDioInterceptor extends InterceptorsWrapper {
/// AliceCore instance
final AliceCore aliceCore;
final Dio? retryDio;

/// Creates dio interceptor
AliceDioInterceptor(this.aliceCore);
AliceDioInterceptor(this.aliceCore, {this.retryDio});

/// Handles dio request and creates alice http call based on it
@override
Expand Down Expand Up @@ -58,9 +59,10 @@ class AliceDioInterceptor extends InterceptorsWrapper {
data.files.forEach((entry) {
files.add(
AliceFormDataFile(
entry.value.filename,
entry.value.contentType.toString(),
entry.value.length,
key: entry.key,
fileName: entry.value.filename,
contentType: entry.value.contentType.toString(),
length: entry.value.length,
),
);
});
Expand All @@ -72,16 +74,53 @@ class AliceDioInterceptor extends InterceptorsWrapper {
request.body = data;
}
}

final parentCallId = options.extra['parent_call_id'] as int?;
if (parentCallId != null) {
aliceCore.removeCallId(parentCallId);
call.parentCallId = parentCallId;
}
request.time = DateTime.now();
request.headers = options.headers;
request.contentType = options.contentType.toString();
request.queryParameters = options.queryParameters;

call.request = request;
call.response = AliceHttpResponse();

aliceCore.addCall(call);
final isSupportRetryCallBack =
retryDio != null && !(options.data is FormData);

call.retryCallBack = isSupportRetryCallBack
? () {
retryDio?.request<dynamic>(options.path,
data: options.data,
queryParameters: options.queryParameters,
onReceiveProgress: options.onReceiveProgress,
onSendProgress: options.onSendProgress,
cancelToken: options.cancelToken,
options: Options(
method: options.method,
sendTimeout: options.sendTimeout,
receiveTimeout: options.receiveTimeout,
extra: <String, dynamic>{
...options.extra,
'parent_call_id': call.id
},
headers: options.headers,
responseType: options.responseType,
contentType: options.contentType,
validateStatus: options.validateStatus,
receiveDataWhenStatusError:
options.receiveDataWhenStatusError,
maxRedirects: options.maxRedirects,
followRedirects: options.followRedirects,
requestEncoder: options.requestEncoder,
responseDecoder: options.responseDecoder,
listFormat: options.listFormat));
}
: null;
aliceCore.addCall(
call,
);
handler.next(options);
}

Expand Down
1 change: 1 addition & 0 deletions lib/helper/alice_save_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ class AliceSaveHelper {
stringBuffer.write("--------------------------------------------\n");
stringBuffer.write("General data\n");
stringBuffer.write("--------------------------------------------\n");
stringBuffer.write("Uri: ${call.uri} \n");
stringBuffer.write("Server: ${call.server} \n");
stringBuffer.write("Method: ${call.method} \n");
stringBuffer.write("Endpoint: ${call.endpoint} \n");
Expand Down
8 changes: 7 additions & 1 deletion lib/model/alice_form_data_file.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
class AliceFormDataFile {
final String? key;
final String? fileName;
final String contentType;
final int length;

AliceFormDataFile(this.fileName, this.contentType, this.length);
AliceFormDataFile({
required this.fileName,
required this.contentType,
required this.length,
required this.key,
});
}
101 changes: 65 additions & 36 deletions lib/model/alice_http_call.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import 'dart:convert';

import 'package:alice/model/alice_http_error.dart';
import 'package:alice/model/alice_http_request.dart';
import 'package:alice/model/alice_http_response.dart';
import 'package:alice/utils/alice_parser.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';

class AliceHttpCall {
final int id;
Expand All @@ -13,11 +18,16 @@ class AliceHttpCall {
String server = "";
String uri = "";
int duration = 0;

//When using retry call
int? parentCallId;
VoidCallback? retryCallBack;
AliceHttpRequest? request;
AliceHttpResponse? response;
AliceHttpError? error;
static const kDefaultFormDataValue = 'replace-with-your-value';

bool get shouldShowRetryButton => retryCallBack != null;
bool get hasParentCall => parentCallId != null;
AliceHttpCall(this.id) {
loading = true;
createdTime = DateTime.now();
Expand All @@ -29,48 +39,67 @@ class AliceHttpCall {
}

String getCurlCommand() {
var compressed = false;
var curlCmd = "curl";
curlCmd += " -X $method";
List<String> postmanCurl = ['curl ${_renderRequest()}'];
postmanCurl.addAll([_renderHeader(), _renderData()]);
return postmanCurl.join(' \\\n\t');
}

String _singleQuoteCharacter(String content) {
return '\'$content\'';
}

String _renderRequest() {
return '--location --request $method ${_singleQuoteCharacter(uri)} ';
}

String _renderHeader() {
final headers = request!.headers;
if (headers.isEmpty) return '';
List<String> headerList = [];
final blackListHeader = [Headers.contentLengthHeader];
headers.forEach((key, dynamic value) {
if ("Accept-Encoding" == key && "gzip" == value) {
compressed = true;
}
curlCmd += " -H '$key: $value'";
if (!blackListHeader.contains(key))
headerList.add('--header ${_singleQuoteCharacter('$key: $value')}');
});
return headerList.join(' \\\n\t');
}

String _renderData() {
final dynamic requestBody = request?.body;
final headers = request!.headers;
String dataContent = '';
if (requestBody.toString().isEmpty) return dataContent;
if (requestBody is Map && requestBody.isNotEmpty) {
final formattedRequestBody = AliceParser.formatBody(
requestBody, AliceParser.getContentType(headers),
parseJson: (dynamic data) {
return jsonEncode(data);
});

final String requestBody = request!.body.toString();
if (requestBody != '') {
// try to keep to a single line and use a subshell to preserve any line breaks
curlCmd += " --data \$'${requestBody.replaceAll("\n", "\\n")}'";
}
dataContent +=
"--data-raw ${_singleQuoteCharacter(formattedRequestBody.replaceAll("\n", "\\n"))}";
} else if (requestBody is String) {
if (requestBody == 'Form data') {
final List<String> formList = [];

final queryParamMap = request!.queryParameters;
int paramCount = queryParamMap.keys.length;
var queryParams = "";
if (paramCount > 0) {
queryParams += "?";
queryParamMap.forEach((key, dynamic value) {
queryParams += '$key=$value';
paramCount -= 1;
if (paramCount > 0) {
queryParams += "&";
if (request?.formDataFiles?.isNotEmpty == true) {
request?.formDataFiles?.forEach((form) {
formList.add(
'--form ${_singleQuoteCharacter('${form.key}=${kDefaultFormDataValue}')}');
});
} else if (request?.formDataFields?.isNotEmpty == true) {
request?.formDataFields?.forEach((form) {
formList.add(
'--form ${_singleQuoteCharacter('${form.name}=${form.value}')}');
});
}
});
}

// If server already has http(s) don't add it again
if (server.contains("http://") || server.contains("https://")) {
// ignore: join_return_with_assignment
curlCmd +=
"${compressed ? " --compressed " : " "}${"'$server$endpoint$queryParams'"}";
} else {
// ignore: join_return_with_assignment
curlCmd +=
"${compressed ? " --compressed " : " "}${"'${secure ? 'https://' : 'http://'}$server$endpoint$queryParams'"}";
dataContent += formList.join(' \\\n\t');
} else {
dataContent +=
"--data-raw ${_singleQuoteCharacter(requestBody.replaceAll("\n", "\\n"))}";
}
}

return curlCmd;
return dataContent;
}
}
Loading