506 lines
14 KiB
Dart
506 lines
14 KiB
Dart
import 'dart:convert';
|
|
import 'dart:core';
|
|
import 'dart:io';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:collection/collection.dart';
|
|
import 'package:equatable/equatable.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:http_parser/http_parser.dart';
|
|
import 'package:mime_type/mime_type.dart';
|
|
|
|
import '/flutter_flow/uploaded_file.dart';
|
|
import 'get_streamed_response.dart';
|
|
|
|
enum ApiCallType { GET, POST, DELETE, PUT, PATCH }
|
|
|
|
enum BodyType { NONE, JSON, TEXT, X_WWW_FORM_URL_ENCODED, MULTIPART, BLOB }
|
|
|
|
class ApiCallOptions extends Equatable {
|
|
const ApiCallOptions({
|
|
this.callName = '',
|
|
required this.callType,
|
|
required this.apiUrl,
|
|
required this.headers,
|
|
required this.params,
|
|
this.bodyType,
|
|
this.body,
|
|
this.returnBody = true,
|
|
this.encodeBodyUtf8 = false,
|
|
this.decodeUtf8 = false,
|
|
this.alwaysAllowBody = false,
|
|
this.cache = false,
|
|
this.isStreamingApi = false,
|
|
});
|
|
|
|
final String callName;
|
|
final ApiCallType callType;
|
|
final String apiUrl;
|
|
final Map<String, dynamic> headers;
|
|
final Map<String, dynamic> params;
|
|
final BodyType? bodyType;
|
|
final String? body;
|
|
final bool returnBody;
|
|
final bool encodeBodyUtf8;
|
|
final bool decodeUtf8;
|
|
final bool alwaysAllowBody;
|
|
final bool cache;
|
|
final bool isStreamingApi;
|
|
|
|
ApiCallOptions clone() => ApiCallOptions(
|
|
callName: callName,
|
|
callType: callType,
|
|
apiUrl: apiUrl,
|
|
headers: _cloneMap(headers),
|
|
params: _cloneMap(params),
|
|
bodyType: bodyType,
|
|
body: body,
|
|
returnBody: returnBody,
|
|
encodeBodyUtf8: encodeBodyUtf8,
|
|
decodeUtf8: decodeUtf8,
|
|
alwaysAllowBody: alwaysAllowBody,
|
|
cache: cache,
|
|
isStreamingApi: isStreamingApi,
|
|
);
|
|
|
|
@override
|
|
List<Object?> get props => [
|
|
callName,
|
|
callType.name,
|
|
apiUrl,
|
|
headers,
|
|
params,
|
|
bodyType,
|
|
body,
|
|
returnBody,
|
|
encodeBodyUtf8,
|
|
decodeUtf8,
|
|
alwaysAllowBody,
|
|
cache,
|
|
isStreamingApi,
|
|
];
|
|
|
|
static Map<String, dynamic> _cloneMap(Map<String, dynamic> map) {
|
|
try {
|
|
return json.decode(json.encode(map)) as Map<String, dynamic>;
|
|
} catch (_) {
|
|
return Map.from(map);
|
|
}
|
|
}
|
|
}
|
|
|
|
class ApiCallResponse {
|
|
const ApiCallResponse(
|
|
this.jsonBody,
|
|
this.headers,
|
|
this.statusCode, {
|
|
this.response,
|
|
this.streamedResponse,
|
|
this.exception,
|
|
});
|
|
final dynamic jsonBody;
|
|
final Map<String, String> headers;
|
|
final int statusCode;
|
|
final http.Response? response;
|
|
final http.StreamedResponse? streamedResponse;
|
|
final Object? exception;
|
|
bool get succeeded => statusCode >= 200 && statusCode < 300;
|
|
String getHeader(String headerName) => headers[headerName] ?? '';
|
|
String get bodyText =>
|
|
response?.body ??
|
|
(jsonBody is String ? jsonBody as String : jsonEncode(jsonBody));
|
|
String get exceptionMessage => exception.toString();
|
|
|
|
static ApiCallResponse fromHttpResponse(
|
|
http.Response response,
|
|
bool returnBody,
|
|
bool decodeUtf8,
|
|
BodyType? bodyType,
|
|
) {
|
|
dynamic jsonBody;
|
|
try {
|
|
if (bodyType == BodyType.BLOB) {
|
|
jsonBody = response.bodyBytes;
|
|
} else {
|
|
final responseBody = decodeUtf8 && returnBody
|
|
? const Utf8Decoder().convert(response.bodyBytes)
|
|
: response.body;
|
|
jsonBody = returnBody ? json.decode(responseBody) : null;
|
|
}
|
|
} catch (_) {}
|
|
return ApiCallResponse(
|
|
jsonBody,
|
|
response.headers,
|
|
response.statusCode,
|
|
response: response,
|
|
);
|
|
}
|
|
|
|
static ApiCallResponse fromCloudCallResponse(Map<String, dynamic> response) =>
|
|
ApiCallResponse(
|
|
response['body'],
|
|
ApiManager.toStringMap(response['headers'] ?? {}),
|
|
response['statusCode'] ?? 400,
|
|
);
|
|
}
|
|
|
|
class ApiManager {
|
|
ApiManager._();
|
|
|
|
static final Map<ApiCallOptions, ApiCallResponse> _apiCache = {};
|
|
|
|
static ApiManager? _instance;
|
|
static ApiManager get instance => _instance ??= ApiManager._();
|
|
|
|
static String? _accessToken;
|
|
|
|
static void clearCache(String callName) => _apiCache.keys
|
|
.toSet()
|
|
.forEach((k) => k.callName == callName ? _apiCache.remove(k) : null);
|
|
|
|
static Map<String, String> toStringMap(Map map) =>
|
|
map.map((key, value) => MapEntry(key.toString(), value.toString()));
|
|
|
|
static String asQueryParams(Map<String, dynamic> map) => map.entries
|
|
.map((e) =>
|
|
"${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value.toString())}")
|
|
.join('&');
|
|
|
|
static Future<ApiCallResponse> urlRequest(
|
|
ApiCallType callType,
|
|
String apiUrl,
|
|
Map<String, dynamic> headers,
|
|
Map<String, dynamic> params,
|
|
bool returnBody,
|
|
bool decodeUtf8,
|
|
bool isStreamingApi, {
|
|
http.Client? client,
|
|
BodyType? bodyType,
|
|
}) async {
|
|
if (params.isNotEmpty) {
|
|
final specifier =
|
|
Uri.parse(apiUrl).queryParameters.isNotEmpty ? '&' : '?';
|
|
apiUrl = '$apiUrl$specifier${asQueryParams(params)}';
|
|
}
|
|
if (isStreamingApi) {
|
|
client ??= http.Client();
|
|
final request =
|
|
http.Request(callType.toString().split('.').last, Uri.parse(apiUrl))
|
|
..headers.addAll(toStringMap(headers));
|
|
final streamedResponse = await getStreamedResponse(request);
|
|
return ApiCallResponse(
|
|
null,
|
|
streamedResponse.headers,
|
|
streamedResponse.statusCode,
|
|
streamedResponse: streamedResponse,
|
|
);
|
|
}
|
|
final makeRequest = callType == ApiCallType.GET
|
|
? (client != null ? client.get : http.get)
|
|
: (client != null ? client.delete : http.delete);
|
|
final response =
|
|
await makeRequest(Uri.parse(apiUrl), headers: toStringMap(headers));
|
|
return ApiCallResponse.fromHttpResponse(
|
|
response, returnBody, decodeUtf8, bodyType);
|
|
}
|
|
|
|
static Future<ApiCallResponse> requestWithBody(
|
|
ApiCallType type,
|
|
String apiUrl,
|
|
Map<String, dynamic> headers,
|
|
Map<String, dynamic> params,
|
|
String? body,
|
|
BodyType? bodyType,
|
|
bool returnBody,
|
|
bool encodeBodyUtf8,
|
|
bool decodeUtf8,
|
|
bool alwaysAllowBody,
|
|
bool isStreamingApi, {
|
|
http.Client? client,
|
|
}) async {
|
|
assert(
|
|
{ApiCallType.POST, ApiCallType.PUT, ApiCallType.PATCH}.contains(type) ||
|
|
(alwaysAllowBody && type == ApiCallType.DELETE),
|
|
'Invalid ApiCallType $type for request with body',
|
|
);
|
|
final postBody =
|
|
createBody(headers, params, body, bodyType, encodeBodyUtf8);
|
|
if (isStreamingApi) {
|
|
client ??= http.Client();
|
|
final request =
|
|
http.Request(type.toString().split('.').last, Uri.parse(apiUrl))
|
|
..headers.addAll(toStringMap(headers));
|
|
request.body = postBody;
|
|
final streamedResponse = await getStreamedResponse(request);
|
|
return ApiCallResponse(
|
|
null,
|
|
streamedResponse.headers,
|
|
streamedResponse.statusCode,
|
|
streamedResponse: streamedResponse,
|
|
);
|
|
}
|
|
|
|
if (bodyType == BodyType.MULTIPART) {
|
|
return multipartRequest(type, apiUrl, headers, params, returnBody,
|
|
decodeUtf8, alwaysAllowBody, bodyType);
|
|
}
|
|
|
|
final requestFn = {
|
|
ApiCallType.POST: client != null ? client.post : http.post,
|
|
ApiCallType.PUT: client != null ? client.put : http.put,
|
|
ApiCallType.PATCH: client != null ? client.patch : http.patch,
|
|
ApiCallType.DELETE: client != null ? client.delete : http.delete,
|
|
}[type]!;
|
|
final response = await requestFn(Uri.parse(apiUrl),
|
|
headers: toStringMap(headers), body: postBody);
|
|
return ApiCallResponse.fromHttpResponse(
|
|
response, returnBody, decodeUtf8, bodyType);
|
|
}
|
|
|
|
static Future<ApiCallResponse> multipartRequest(
|
|
ApiCallType? type,
|
|
String apiUrl,
|
|
Map<String, dynamic> headers,
|
|
Map<String, dynamic> params,
|
|
bool returnBody,
|
|
bool decodeUtf8,
|
|
bool alwaysAllowBody,
|
|
BodyType? bodyType,
|
|
) async {
|
|
assert(
|
|
{ApiCallType.POST, ApiCallType.PUT, ApiCallType.PATCH}.contains(type) ||
|
|
(alwaysAllowBody && type == ApiCallType.DELETE),
|
|
'Invalid ApiCallType $type for request with body',
|
|
);
|
|
|
|
bool isFile(dynamic e) =>
|
|
e is FFUploadedFile ||
|
|
e is List<FFUploadedFile> ||
|
|
(e is List && e.firstOrNull is FFUploadedFile);
|
|
|
|
final nonFileParams = toStringMap(
|
|
Map.fromEntries(params.entries.where((e) => !isFile(e.value))));
|
|
|
|
List<http.MultipartFile> files = [];
|
|
params.entries.where((e) => isFile(e.value)).forEach((e) {
|
|
final param = e.value;
|
|
final uploadedFiles = param is List
|
|
? param as List<FFUploadedFile>
|
|
: [param as FFUploadedFile];
|
|
for (var uploadedFile in uploadedFiles) {
|
|
files.add(
|
|
http.MultipartFile.fromBytes(
|
|
e.key,
|
|
uploadedFile.bytes ?? Uint8List.fromList([]),
|
|
filename: uploadedFile.name,
|
|
contentType: _getMediaType(uploadedFile.name),
|
|
),
|
|
);
|
|
}
|
|
});
|
|
|
|
final request = http.MultipartRequest(
|
|
type.toString().split('.').last, Uri.parse(apiUrl))
|
|
..headers.addAll(toStringMap(headers))
|
|
..files.addAll(files);
|
|
nonFileParams.forEach((key, value) => request.fields[key] = value);
|
|
|
|
final response = await http.Response.fromStream(await request.send());
|
|
return ApiCallResponse.fromHttpResponse(
|
|
response, returnBody, decodeUtf8, bodyType);
|
|
}
|
|
|
|
static MediaType? _getMediaType(String? filename) {
|
|
final contentType = mime(filename);
|
|
if (contentType == null) {
|
|
return null;
|
|
}
|
|
final parts = contentType.split('/');
|
|
if (parts.length != 2) {
|
|
return null;
|
|
}
|
|
return MediaType(parts.first, parts.last);
|
|
}
|
|
|
|
static dynamic createBody(
|
|
Map<String, dynamic> headers,
|
|
Map<String, dynamic>? params,
|
|
String? body,
|
|
BodyType? bodyType,
|
|
bool encodeBodyUtf8,
|
|
) {
|
|
String? contentType;
|
|
dynamic postBody;
|
|
switch (bodyType) {
|
|
case BodyType.JSON:
|
|
contentType = 'application/json';
|
|
postBody = body ?? json.encode(params ?? {});
|
|
break;
|
|
case BodyType.TEXT:
|
|
contentType = 'text/plain';
|
|
postBody = body ?? json.encode(params ?? {});
|
|
break;
|
|
case BodyType.X_WWW_FORM_URL_ENCODED:
|
|
contentType = 'application/x-www-form-urlencoded';
|
|
postBody = toStringMap(params ?? {});
|
|
break;
|
|
case BodyType.MULTIPART:
|
|
contentType = 'multipart/form-data';
|
|
postBody = params;
|
|
break;
|
|
case BodyType.BLOB:
|
|
contentType = 'application/octet-stream';
|
|
postBody = body;
|
|
break;
|
|
case BodyType.NONE:
|
|
case null:
|
|
break;
|
|
}
|
|
if (contentType != null &&
|
|
!headers.keys.any((h) => h.toLowerCase() == 'content-type')) {
|
|
headers['Content-Type'] = contentType;
|
|
}
|
|
return encodeBodyUtf8 && postBody is String
|
|
? utf8.encode(postBody)
|
|
: postBody;
|
|
}
|
|
|
|
Future<ApiCallResponse> call(ApiCallOptions options) => makeApiCall(
|
|
callName: options.callName,
|
|
apiUrl: options.apiUrl,
|
|
callType: options.callType,
|
|
headers: options.headers,
|
|
params: options.params,
|
|
body: options.body,
|
|
bodyType: options.bodyType,
|
|
returnBody: options.returnBody,
|
|
encodeBodyUtf8: options.encodeBodyUtf8,
|
|
decodeUtf8: options.decodeUtf8,
|
|
alwaysAllowBody: options.alwaysAllowBody,
|
|
cache: options.cache,
|
|
isStreamingApi: options.isStreamingApi,
|
|
options: options,
|
|
);
|
|
|
|
Future<ApiCallResponse> makeApiCall({
|
|
required String callName,
|
|
required String apiUrl,
|
|
required ApiCallType callType,
|
|
Map<String, dynamic> headers = const {},
|
|
Map<String, dynamic> params = const {},
|
|
String? body,
|
|
BodyType? bodyType,
|
|
bool returnBody = true,
|
|
bool encodeBodyUtf8 = false,
|
|
bool decodeUtf8 = false,
|
|
bool alwaysAllowBody = false,
|
|
bool cache = false,
|
|
bool isStreamingApi = false,
|
|
ApiCallOptions? options,
|
|
http.Client? client,
|
|
}) async {
|
|
final callOptions = options ??
|
|
ApiCallOptions(
|
|
callName: callName,
|
|
callType: callType,
|
|
apiUrl: apiUrl,
|
|
headers: headers,
|
|
params: params,
|
|
bodyType: bodyType,
|
|
body: body,
|
|
returnBody: returnBody,
|
|
encodeBodyUtf8: encodeBodyUtf8,
|
|
decodeUtf8: decodeUtf8,
|
|
alwaysAllowBody: alwaysAllowBody,
|
|
cache: cache,
|
|
isStreamingApi: isStreamingApi,
|
|
);
|
|
if (_accessToken != null) {
|
|
headers[HttpHeaders.authorizationHeader] = 'Bearer $_accessToken';
|
|
}
|
|
if (!apiUrl.startsWith('http')) {
|
|
apiUrl = 'https://$apiUrl';
|
|
}
|
|
|
|
if (cache && _apiCache.containsKey(callOptions)) {
|
|
return _apiCache[callOptions]!;
|
|
}
|
|
|
|
ApiCallResponse result;
|
|
try {
|
|
switch (callType) {
|
|
case ApiCallType.GET:
|
|
result = await urlRequest(
|
|
callType,
|
|
apiUrl,
|
|
headers,
|
|
params,
|
|
returnBody,
|
|
decodeUtf8,
|
|
isStreamingApi,
|
|
client: client,
|
|
);
|
|
break;
|
|
case ApiCallType.DELETE:
|
|
result = alwaysAllowBody
|
|
? await requestWithBody(
|
|
callType,
|
|
apiUrl,
|
|
headers,
|
|
params,
|
|
body,
|
|
bodyType,
|
|
returnBody,
|
|
encodeBodyUtf8,
|
|
decodeUtf8,
|
|
alwaysAllowBody,
|
|
isStreamingApi,
|
|
client: client,
|
|
)
|
|
: await urlRequest(
|
|
callType,
|
|
apiUrl,
|
|
headers,
|
|
params,
|
|
returnBody,
|
|
decodeUtf8,
|
|
isStreamingApi,
|
|
client: client,
|
|
);
|
|
break;
|
|
case ApiCallType.POST:
|
|
case ApiCallType.PUT:
|
|
case ApiCallType.PATCH:
|
|
result = await requestWithBody(
|
|
callType,
|
|
apiUrl,
|
|
headers,
|
|
params,
|
|
body,
|
|
bodyType,
|
|
returnBody,
|
|
encodeBodyUtf8,
|
|
decodeUtf8,
|
|
alwaysAllowBody,
|
|
isStreamingApi,
|
|
client: client,
|
|
);
|
|
break;
|
|
}
|
|
|
|
if (cache) {
|
|
_apiCache[callOptions] = result;
|
|
}
|
|
} catch (e) {
|
|
result = ApiCallResponse(null, {}, -1, exception: e);
|
|
}
|
|
|
|
print('API Call: $callName');
|
|
print('URL: $apiUrl');
|
|
print('Headers: $headers');
|
|
print('Params: $params');
|
|
print('Response: ${result.jsonBody}');
|
|
return result;
|
|
}
|
|
}
|