Feature/middlewares #9

Merged
hugo merged 4 commits from Feature/middlewares into master 2022-06-24 14:00:03 +00:00
16 changed files with 720 additions and 267 deletions
Showing only changes of commit 14cb3485e4 - Show all commits

View File

@ -17,8 +17,13 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';
import 'package:wyatt_http_client/src/authentication/refresh_token_client.dart';
import 'package:wyatt_http_client/src/rest_client.dart';
import 'package:wyatt_http_client/src/middleware_client.dart';
import 'package:wyatt_http_client/src/middlewares/body_to_json_middleware.dart';
import 'package:wyatt_http_client/src/middlewares/refresh_token_middleware.dart';
import 'package:wyatt_http_client/src/middlewares/simple_logger_middleware.dart';
import 'package:wyatt_http_client/src/middlewares/uri_prefix_middleware.dart';
import 'package:wyatt_http_client/src/pipeline.dart';
import 'package:wyatt_http_client/src/utils/http_status.dart';
import 'package:wyatt_http_client/src/utils/protocols.dart';
enum EmailVerificationAction {
@ -262,24 +267,14 @@ class Login {
class FastAPI {
final String baseUrl;
final RefreshTokenClient client;
final MiddlewareClient client;
final int apiVersion;
FastAPI({
this.baseUrl = 'localhost:80',
RefreshTokenClient? client,
MiddlewareClient? client,
this.apiVersion = 1,
}) : client = client ??
RefreshTokenClient(
authorizationEndpoint: '',
tokenEndpoint: '',
accessTokenParser: (body) => body['access_token']! as String,
refreshTokenParser: (body) => body['refresh_token']! as String,
inner: RestClient(
protocol: Protocols.http,
authority: baseUrl,
),
);
}) : client = client ?? MiddlewareClient();
String get apiPath => '/api/v$apiVersion';
@ -323,15 +318,22 @@ class FastAPI {
}
Future<TokenSuccess> signInWithPassword(Login login) async {
final r = await client.authorize(login.toMap());
final r = await client.post(
Uri.parse('$apiPath/auth/sign-in-with-password'),
body: login.toMap(),
);
if (r.statusCode != 200) {
throw Exception('Invalid reponse: ${r.statusCode}');
} else {
return TokenSuccess.fromJson(r.body);
}
Future<TokenSuccess> refresh() async {
final r = await client.refresh();
return TokenSuccess.fromJson(r?.body ?? '');
}
// Future<TokenSuccess> refresh() async {
// final r = await client.refresh();
// return TokenSuccess.fromJson(r?.body ?? '');
// }
Future<List<Account>> getAccountList() async {
final r = await client.get(
Uri.parse('$apiPath/account'),
@ -351,19 +353,32 @@ class FastAPI {
}
void main(List<String> args) async {
final api = FastAPI(
client: RefreshTokenClient(
final Pipeline pipeline = Pipeline()
.addMiddleware(SimpleLoggerMiddleware())
.addMiddleware(
UriPrefixMiddleware(
protocol: Protocols.http,
authority: 'localhost:80',
),
)
.addMiddleware(BodyToJsonMiddleware())
.addMiddleware(
RefreshTokenMiddleware(
authorizationEndpoint: '/api/v1/auth/sign-in-with-password',
tokenEndpoint: '/api/v1/auth/refresh',
accessTokenParser: (body) => body['access_token']! as String,
refreshTokenParser: (body) => body['refresh_token']! as String,
inner: RestClient(
protocol: Protocols.http,
authority: 'localhost:80',
),
unauthorized: HttpStatus.forbidden,
),
);
print(pipeline.getLogic());
final client = MiddlewareClient(pipeline: pipeline);
final api = FastAPI(
client: client,
);
// await api.sendSignUpCode('git@pcl.ovh');
// final verifiedAccount = await api.verifyCode(
// VerifyCode(

View File

@ -117,7 +117,7 @@ import 'package:wyatt_http_client/src/utils/protocols.dart';
// }
Future<void> main(List<String> args) async {
final Pipeline pipeline1 = Pipeline()
final Pipeline pipeline = Pipeline()
.addMiddleware(SimpleLoggerMiddleware())
.addMiddleware(
UriPrefixMiddleware(
@ -125,21 +125,18 @@ Future<void> main(List<String> args) async {
authority: 'localhost:80',
),
)
.addMiddleware(BodyToJsonMiddleware());
final Pipeline pipeline2 = Pipeline().addMiddleware(
.addMiddleware(BodyToJsonMiddleware())
.addMiddleware(
RefreshTokenMiddleware(
authorizationEndpoint:
'http://localhost:80/api/v1/account/test?action=authorize',
tokenEndpoint: 'http://localhost:80/api/v1/account/test?action=refresh',
innerClientMiddlewares: pipeline1.middleware,
authorizationEndpoint: '/api/v1/account/test?action=authorize',
tokenEndpoint: '/api/v1/account/test?action=refresh',
accessTokenParser: (body) => body['access_token']! as String,
refreshTokenParser: (body) => body['refresh_token']! as String,
),
);
final Pipeline pipeline = pipeline1 + pipeline2;
print(pipeline.getLogic());
final client = MiddlewareClient(pipeline);
final client = MiddlewareClient(pipeline: pipeline);
final r = await client.post(
Uri.parse('/api/v1/account/test'),
body: <String, String>{

View File

@ -14,60 +14,27 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:http/http.dart';
import 'package:wyatt_http_client/src/middleware_client.dart';
import 'package:wyatt_http_client/src/models/middleware_request.dart';
import 'package:wyatt_http_client/src/models/middleware_response.dart';
part of 'pipeline.dart';
class Middleware {
Middleware? child;
/// The http [MiddlewareClient] used by this [Middleware]
MiddlewareClient? _client;
Middleware({
this.child,
});
String getName() => 'MiddlewareNode';
Middleware._({
this.child,
MiddlewareClient? client,
}) : _client = client;
// ignore: avoid_setters_without_getters
set httpClient(MiddlewareClient? client) => _client = client;
String getName() => 'Middleware';
Client? get client => _client?.inner;
void setClient(MiddlewareClient? client) {
_client = client;
child?.setClient(client);
}
Client? getClient() {
return _client?.inner;
}
Middleware deepCopy() {
if (child != null) {
return Middleware._(child: child?.deepCopy(), client: _client);
} else {
return Middleware._(client: _client);
}
}
void addChild(Middleware middleware) {
if (child != null) {
child?.addChild(middleware);
} else {
child = middleware;
}
}
MiddlewareRequest onRequest(
Future<MiddlewareRequest> onRequest(
MiddlewareRequest request,
) {
return child?.onRequest(request) ?? request;
) async {
return request;
}
MiddlewareResponse onResponse(MiddlewareResponse response) {
return child?.onResponse(response) ?? response;
Future<MiddlewareResponse> onResponse(MiddlewareResponse response) async {
return response;
}
@override

View File

@ -17,33 +17,32 @@
import 'dart:convert';
import 'package:http/http.dart';
import 'package:wyatt_http_client/src/middleware.dart';
import 'package:wyatt_http_client/src/models/middleware_context.dart';
import 'package:wyatt_http_client/src/models/middleware_request.dart';
import 'package:wyatt_http_client/src/models/middleware_response.dart';
import 'package:wyatt_http_client/src/models/unfreezed_request.dart';
import 'package:wyatt_http_client/src/pipeline.dart';
import 'package:wyatt_http_client/src/utils/http_methods.dart';
class MiddlewareClient extends BaseClient {
final Client inner;
final Middleware middleware;
final Pipeline pipeline;
MiddlewareClient(
this.pipeline, {
Middleware? middleware,
MiddlewareClient({
Pipeline? pipeline,
Client? inner,
}) : inner = inner ?? Client(),
middleware = middleware ?? pipeline.middleware {
this.middleware.setClient(this);
}) : pipeline = pipeline ?? Pipeline(),
inner = inner ?? Client() {
this.pipeline.setClient(this);
}
@override
Future<Response> head(Uri url, {Map<String, String>? headers}) =>
_sendUnstreamed('HEAD', url, headers);
_sendUnstreamed(HttpMethods.head.method, url, headers);
@override
Future<Response> get(Uri url, {Map<String, String>? headers}) =>
_sendUnstreamed('GET', url, headers);
_sendUnstreamed(HttpMethods.get.method, url, headers);
@override
Future<Response> post(
@ -52,7 +51,7 @@ class MiddlewareClient extends BaseClient {
Object? body,
Encoding? encoding,
}) =>
_sendUnstreamed('POST', url, headers, body, encoding);
_sendUnstreamed(HttpMethods.post.method, url, headers, body, encoding);
@override
Future<Response> put(
@ -61,7 +60,7 @@ class MiddlewareClient extends BaseClient {
Object? body,
Encoding? encoding,
}) =>
_sendUnstreamed('PUT', url, headers, body, encoding);
_sendUnstreamed(HttpMethods.put.method, url, headers, body, encoding);
@override
Future<Response> patch(
@ -70,7 +69,7 @@ class MiddlewareClient extends BaseClient {
Object? body,
Encoding? encoding,
}) =>
_sendUnstreamed('PATCH', url, headers, body, encoding);
_sendUnstreamed(HttpMethods.patch.method, url, headers, body, encoding);
@override
Future<Response> delete(
@ -79,7 +78,7 @@ class MiddlewareClient extends BaseClient {
Object? body,
Encoding? encoding,
}) =>
_sendUnstreamed('DELETE', url, headers, body, encoding);
_sendUnstreamed(HttpMethods.delete.method, url, headers, body, encoding);
@override
Future<StreamedResponse> send(BaseRequest request) {
@ -93,8 +92,7 @@ class MiddlewareClient extends BaseClient {
Object? body,
Encoding? encoding,
]) async {
final modifiedRequest = middleware.onRequest(
MiddlewareRequest(
final originalRequest = MiddlewareRequest(
unfreezedRequest: UnfreezedRequest(
method: method,
url: url,
@ -103,14 +101,25 @@ class MiddlewareClient extends BaseClient {
encoding: encoding,
),
httpRequest: Request(method, url),
),
context: MiddlewareContext(pipeline: pipeline),
);
final modifiedRequest = await pipeline.onRequest(
originalRequest.copyWith(),
);
final res = await Response.fromStream(
await send(modifiedRequest.httpRequest),
);
final response =
middleware.onResponse(MiddlewareResponse(httpResponse: res));
final response = await pipeline.onResponse(
MiddlewareResponse(
httpResponse: res,
middlewareRequest: modifiedRequest,
context: MiddlewareContext(
pipeline: pipeline,
originalRequest: originalRequest,
),
),
);
return response.httpResponse as Response;
}

View File

@ -0,0 +1,103 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
part of 'pipeline.dart';
class MiddlewareNode {
final Pipeline pipeline;
Middleware? middleware;
late MiddlewareNode _parent;
late MiddlewareNode _child;
final bool _isEnd;
/// Reference to the previous [MiddlewareNode] in the [Pipeline]
MiddlewareNode get parent => _parent;
/// Reference to the next [MiddlewareNode] in the [Pipeline]
MiddlewareNode get child => _child;
/// Whether this is the begin [MiddlewareNode]
bool get isBegin => _parent == this;
/// Whether this is the end [MiddlewareNode]
bool get isEnd => _child == this;
/// Whether this is the first [MiddlewareNode]
bool get isFirst => !isBegin && _parent == pipeline.begin;
/// Whether this is the last [MiddlewareNode]
bool get isLast => !isEnd && _child == pipeline.end;
MiddlewareNode._(
this.pipeline,
this.middleware, {
MiddlewareNode? parent,
MiddlewareNode? child,
}) : _isEnd = false {
_parent = parent ?? this;
_child = child ?? this;
}
MiddlewareNode._end(this.pipeline) : _isEnd = true {
_child = this;
}
MiddlewareNode._begin(this.pipeline) : _isEnd = true {
_parent = this;
}
/// Creates a new [MiddlewareNode] right **before** this in [pipeline]
MiddlewareNode insertBefore(Middleware middleware) {
if (isBegin) {
throw StateError(
'A MiddlewareNode cannot be inserted '
'before begin MiddlewareNode',
);
}
final newMiddlewareNode =
MiddlewareNode._(pipeline, middleware, parent: _parent, child: this);
_parent._child = newMiddlewareNode;
_parent = newMiddlewareNode;
pipeline._length++;
return newMiddlewareNode;
}
/// Creates a new [MiddlewareNode] right **after** this in [pipeline]
MiddlewareNode insertAfter(Middleware middleware) {
if (isEnd) {
throw StateError(
'A MiddlewareNode cannot be inserted '
'after end MiddlewareNode',
);
}
final newMiddlewareNode =
MiddlewareNode._(pipeline, middleware, parent: this, child: _child);
_child._parent = newMiddlewareNode;
_child = newMiddlewareNode;
pipeline._length++;
return newMiddlewareNode;
}
MiddlewareNode remove() {
if (_isEnd) throw StateError('Cannot remove end MiddlewareNode');
_child._parent = _parent;
_parent._child = _child;
pipeline._length--;
return child;
}
}

View File

@ -16,21 +16,17 @@
import 'dart:convert';
import 'package:wyatt_http_client/src/middleware.dart';
import 'package:wyatt_http_client/src/models/middleware_request.dart';
import 'package:wyatt_http_client/src/pipeline.dart';
class BodyToJsonMiddleware extends Middleware {
BodyToJsonMiddleware({
super.child,
});
@override
String getName() => 'BodyToJson';
@override
String getName() => 'BodyToJsonMiddleware';
@override
MiddlewareRequest onRequest(MiddlewareRequest request) {
Future<MiddlewareRequest> onRequest(MiddlewareRequest request) {
print(
'BodyToJson::OnRequest: transforms body in json if Map then update '
'${getName()}::OnRequest: transforms body in json if Map then update '
'headers with right content-type',
);
var newReq = request.unfreezedRequest;

View File

@ -14,13 +14,9 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_http_client/src/middleware.dart';
import 'package:wyatt_http_client/src/pipeline.dart';
class DefaultMiddleware extends Middleware {
DefaultMiddleware({
super.child,
});
@override
String getName() => 'DefaultMiddleware';
}

View File

@ -14,49 +14,61 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_http_client/src/middleware.dart';
import 'dart:convert';
import 'package:http/http.dart';
import 'package:wyatt_http_client/src/middleware_client.dart';
import 'package:wyatt_http_client/src/middlewares/default_middleware.dart';
import 'package:wyatt_http_client/src/models/middleware_request.dart';
import 'package:wyatt_http_client/src/models/middleware_response.dart';
import 'package:wyatt_http_client/src/pipeline.dart';
import 'package:wyatt_http_client/src/utils/authentication_methods.dart';
import 'package:wyatt_http_client/src/utils/header_keys.dart';
import 'package:wyatt_http_client/src/utils/http_status.dart';
typedef TokenParser = String Function(Map<String, dynamic>);
class RefreshTokenMiddleware extends Middleware {
final String authorizationEndpoint;
final String tokenEndpoint;
String? accessToken;
final TokenParser accessTokenParser;
String? refreshToken;
final TokenParser refreshTokenParser;
Middleware innerClientMiddlewares;
final String authenticationHeader;
final String authenticationMethod;
final HttpStatus unauthorized;
final int maxRetries;
RefreshTokenMiddleware({
required this.authorizationEndpoint,
required this.tokenEndpoint,
Middleware? innerClientMiddlewares,
super.child,
}) : innerClientMiddlewares = innerClientMiddlewares ?? DefaultMiddleware();
required this.accessTokenParser,
required this.refreshTokenParser,
this.authenticationHeader = HeaderKeys.authorization,
this.authenticationMethod = AuthenticationMethods.bearer,
this.unauthorized = HttpStatus.unauthorized,
this.maxRetries = 3,
});
@override
String getName() => 'RefreshTokenMiddleware';
String getName() => 'RefreshToken';
@override
MiddlewareRequest onRequest(MiddlewareRequest request) {
Future<MiddlewareRequest> onRequest(MiddlewareRequest request) async {
print(
'RefreshToken::OnRequest: accessToken: $accessToken',
'${getName()}::OnRequest: accessToken: $accessToken',
);
if (accessToken == null) {
// Refresh token
final pipeline = Pipeline().addMiddleware(innerClientMiddlewares);
print(pipeline.getLogic());
final client = MiddlewareClient(
pipeline,
inner: getClient(),
);
final _ = client.post(Uri.parse(tokenEndpoint));
if (request.context.originalRequest?.unfreezedRequest.url ==
Uri.parse(authorizationEndpoint)) {
return super.onRequest(request);
}
if (accessToken != null) {
// Modify header with accessToken
var newReq = request.unfreezedRequest;
final mutation = {
'authorization': accessToken ?? '',
authenticationHeader: '$authenticationMethod $accessToken',
};
Map<String, String>? headers = newReq.headers;
if (headers != null) {
@ -66,7 +78,136 @@ class RefreshTokenMiddleware extends Middleware {
}
newReq = newReq.copyWith(headers: headers);
request.updateUnfreezedRequest(newReq);
return super.onRequest(request);
}
if (refreshToken != null) {
// Refresh accessToken with refreshToken before perform request
final subPipeline = request.context.pipeline.fromUntil(this);
final httpClient = MiddlewareClient(
pipeline: subPipeline,
inner: client,
);
final Map<String, String> headers = {
authenticationHeader: '$authenticationHeader $refreshToken',
};
final response =
await httpClient.get(Uri.parse(tokenEndpoint), headers: headers);
final status = HttpStatus.from(response.statusCode);
if (status.isSuccess()) {
final body = jsonDecode(response.body) as Map<String, dynamic>;
accessToken = accessTokenParser(body);
// Then modify current request with accessToken
var newReq = request.unfreezedRequest;
final mutation = {
authenticationHeader: '$authenticationMethod $accessToken',
};
Map<String, String>? headers = newReq.headers;
if (headers != null) {
headers.addAll(mutation);
} else {
headers = mutation;
}
newReq = newReq.copyWith(headers: headers);
request.updateUnfreezedRequest(newReq);
} else {
// Retry
int retries = 0;
while (retries < maxRetries) {
final Map<String, String> headers = {
authenticationHeader: '$authenticationHeader $refreshToken',
};
final response =
await httpClient.get(Uri.parse(tokenEndpoint), headers: headers);
final status = HttpStatus.from(response.statusCode);
if (status.isSuccess()) {
final body = jsonDecode(response.body) as Map<String, dynamic>;
accessToken = accessTokenParser(body);
// Then modify current request with accessToken
var newReq = request.unfreezedRequest;
final mutation = {
authenticationHeader: '$authenticationMethod $accessToken',
};
Map<String, String>? headers = newReq.headers;
if (headers != null) {
headers.addAll(mutation);
} else {
headers = mutation;
}
newReq = newReq.copyWith(headers: headers);
request.updateUnfreezedRequest(newReq);
break;
}
retries++;
}
}
return super.onRequest(request);
}
// Pass
return super.onRequest(request);
}
@override
Future<MiddlewareResponse> onResponse(MiddlewareResponse response) async {
final res = await super.onResponse(response);
final status = HttpStatus.from(res.httpResponse.statusCode);
if (res.context.originalRequest?.unfreezedRequest.url ==
Uri.parse(authorizationEndpoint)) {
if (status.isSuccess()) {
final body = jsonDecode((res.httpResponse as Response).body) as Map<String, dynamic>;
final accessToken = accessTokenParser(body);
final refreshToken = refreshTokenParser(body);
if (accessToken.isNotEmpty) {
this.accessToken = accessToken;
}
if (refreshToken.isNotEmpty) {
this.refreshToken = refreshToken;
}
}
return res;
}
if (status == unauthorized) {
print(
'${getName()}::OnResponse: $unauthorized',
);
// Refresh token then retry
final subPipeline = res.context.pipeline.fromUntil(this);
final httpClient = MiddlewareClient(
pipeline: subPipeline,
inner: client,
);
final Map<String, String> headers = {
authenticationHeader: '$authenticationHeader $refreshToken',
};
final response =
await httpClient.get(Uri.parse(tokenEndpoint), headers: headers);
final refreshstatus = HttpStatus.from(response.statusCode);
if (refreshstatus.isSuccess()) {
print(
'${getName()}::OnResponse: refresh successfuly',
);
final body = jsonDecode(response.body) as Map<String, dynamic>;
accessToken = accessTokenParser(body);
// Then modify current request with accessToken
final midReq = res.middlewareRequest;
final newReq = midReq.httpRequest;
final mutation = {
authenticationHeader: '$authenticationMethod $accessToken',
};
Map<String, String>? headers = newReq.headers;
if (headers != null) {
headers.addAll(mutation);
} else {
headers = mutation;
}
midReq.updateHttpRequest(headers: headers);
final newRes = await httpClient.send(midReq.httpRequest);
return res.copyWith(httpResponse: res as Response);
}
}
return res;
}
}

View File

@ -14,32 +14,29 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_http_client/src/middleware.dart';
import 'package:wyatt_http_client/src/models/middleware_request.dart';
import 'package:wyatt_http_client/src/models/middleware_response.dart';
import 'package:wyatt_http_client/src/pipeline.dart';
class SimpleLoggerMiddleware extends Middleware {
SimpleLoggerMiddleware({
super.child,
});
@override
String getName() => 'SimpleLogger';
@override
String getName() => 'SimpleLoggerMiddleware';
@override
MiddlewareRequest onRequest(MiddlewareRequest request) {
Future<MiddlewareRequest> onRequest(MiddlewareRequest request) {
print(
'Logger::OnRequest: ${request.httpRequest.method} '
'${request.httpRequest.url}',
'${getName()}::OnRequest: ${request.httpRequest.method} '
'${request.httpRequest.url}\n${request.unfreezedRequest.headers}'
'\n>> ${request.unfreezedRequest.body}',
);
return super.onRequest(request);
}
@override
MiddlewareResponse onResponse(MiddlewareResponse response) {
final res = super.onResponse(response);
Future<MiddlewareResponse> onResponse(MiddlewareResponse response) async {
final res = await super.onResponse(response);
print(
'Logger::OnResponse: ${res.httpResponse.statusCode} -> '
'${getName()}::OnResponse: ${res.httpResponse.statusCode} -> '
'received ${res.httpResponse.contentLength} bytes',
);
return res;

View File

@ -14,8 +14,8 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_http_client/src/middleware.dart';
import 'package:wyatt_http_client/src/models/middleware_request.dart';
import 'package:wyatt_http_client/src/pipeline.dart';
import 'package:wyatt_http_client/src/utils/protocols.dart';
class UriPrefixMiddleware extends Middleware {
@ -25,17 +25,16 @@ class UriPrefixMiddleware extends Middleware {
UriPrefixMiddleware({
required this.protocol,
required this.authority,
super.child,
});
@override
String getName() => 'UriPrefixMiddleware';
String getName() => 'UriPrefix';
@override
MiddlewareRequest onRequest(MiddlewareRequest request) {
Future<MiddlewareRequest> onRequest(MiddlewareRequest request) {
final Uri uri =
Uri.parse('${protocol.scheme}$authority${request.httpRequest.url}');
print('UriPrefix::OnRequest: ${request.httpRequest.url} -> $uri');
print('${getName()}::OnRequest: ${request.httpRequest.url} -> $uri');
request.updateHttpRequest(url: uri);
return super.onRequest(request);
}

View File

@ -0,0 +1,48 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_http_client/src/models/middleware_request.dart';
import 'package:wyatt_http_client/src/models/middleware_response.dart';
import 'package:wyatt_http_client/src/pipeline.dart';
class MiddlewareContext {
Pipeline pipeline;
MiddlewareRequest? originalRequest;
MiddlewareResponse? originalResponse;
MiddlewareContext({
required this.pipeline,
this.originalRequest,
this.originalResponse,
});
MiddlewareContext copyWith({
Pipeline? pipeline,
MiddlewareRequest? originalRequest,
MiddlewareResponse? originalResponse,
}) {
return MiddlewareContext(
pipeline: pipeline ?? this.pipeline,
originalRequest: originalRequest ?? this.originalRequest,
originalResponse: originalResponse ?? this.originalResponse,
);
}
@override
String toString() => 'MiddlewareContext(pipeline: $pipeline, '
'originalRequest: $originalRequest, originalResponse: $originalResponse)';
}

View File

@ -16,25 +16,33 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:http/http.dart';
import 'package:wyatt_http_client/src/models/middleware_context.dart';
import 'package:wyatt_http_client/src/models/unfreezed_request.dart';
import 'package:wyatt_http_client/src/utils/utils.dart';
class MiddlewareRequest {
UnfreezedRequest unfreezedRequest;
Request httpRequest;
MiddlewareContext context;
MiddlewareRequest({
required this.unfreezedRequest,
required this.httpRequest,
});
required this.context,
}) {
context = context.copyWith(originalRequest: this);
}
MiddlewareRequest copyWith({
UnfreezedRequest? unfreezedRequest,
Request? httpRequest,
MiddlewareContext? context,
}) {
return MiddlewareRequest(
unfreezedRequest: unfreezedRequest ?? this.unfreezedRequest,
httpRequest: httpRequest ?? this.httpRequest,
context: context ?? this.context,
);
}
@ -85,5 +93,5 @@ class MiddlewareRequest {
@override
String toString() => 'MiddlewareRequest(unfreezedRequest: '
'$unfreezedRequest, httpRequest: $httpRequest)';
'$unfreezedRequest, httpRequest: $httpRequest, context: $context)';
}

View File

@ -16,22 +16,35 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:http/http.dart';
import 'package:wyatt_http_client/src/models/middleware_context.dart';
import 'package:wyatt_http_client/src/models/middleware_request.dart';
class MiddlewareResponse {
BaseResponse httpResponse;
MiddlewareRequest middlewareRequest;
MiddlewareContext context;
MiddlewareResponse({
required this.httpResponse,
});
required this.middlewareRequest,
required this.context,
}) {
context = context.copyWith(originalResponse: this);
}
MiddlewareResponse copyWith({
BaseResponse? httpResponse,
MiddlewareRequest? middlewareRequest,
MiddlewareContext? context,
}) {
return MiddlewareResponse(
httpResponse: httpResponse ?? this.httpResponse,
middlewareRequest: middlewareRequest ?? this.middlewareRequest,
context: context ?? this.context,
);
}
@override
String toString() => 'MiddlewareResponse(httpResponse: $httpResponse)';
String toString() => 'MiddlewareResponse(httpResponse: $httpResponse, '
'middlewareRequest: $middlewareRequest, context: $context)';
}

View File

@ -14,44 +14,140 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_http_client/src/middleware.dart';
import 'package:wyatt_http_client/src/middlewares/default_middleware.dart';
import 'package:http/http.dart';
import 'package:wyatt_http_client/src/middleware_client.dart';
import 'package:wyatt_http_client/src/models/middleware_request.dart';
import 'package:wyatt_http_client/src/models/middleware_response.dart';
part 'middleware.dart';
part 'middleware_node.dart';
class Pipeline {
final Middleware _middleware;
int _length = 0;
late final MiddlewareNode begin;
late final MiddlewareNode end;
Pipeline() : _middleware = DefaultMiddleware();
MiddlewareNode get first => begin.child;
MiddlewareNode get last => end.parent;
bool get isEmpty => _length == 0;
bool get isNotEmpty => !isEmpty;
int get length => _length;
Pipeline() {
_initialize();
}
Pipeline.fromIterable(Iterable<Middleware?> list) {
_initialize();
MiddlewareNode parent = begin;
for (final element in list) {
if (element != null) {
parent = parent.insertAfter(element);
}
}
}
Pipeline.from(Pipeline pipeline)
: this.fromIterable(pipeline.middlewares.map((node) => node.middleware));
void _initialize() {
_length = 0;
begin = MiddlewareNode._begin(this);
end = MiddlewareNode._end(this);
begin._child = end;
end._parent = begin;
}
Iterable<MiddlewareNode> get middlewares sync* {
for (var middleware = first;
middleware != end;
middleware = middleware.child) {
yield middleware;
}
}
int indexOf(MiddlewareNode node) {
int i = -1;
int j = -1;
for (final element in middlewares) {
j++;
if (element == node) {
i = j;
continue;
}
}
return i;
}
int indexOfChild(Middleware middleware) {
int i = -1;
int j = -1;
for (final element in middlewares) {
j++;
if (element.middleware == middleware) {
i = j;
continue;
}
}
return i;
}
/// Create new [Pipeline] from first [Middleware] to the specified one.
Pipeline fromUntil(Middleware middleware, {bool include = false}) {
final nodes = <Middleware?>[];
for (final element in middlewares) {
if (element.middleware != middleware) {
nodes.add(element.middleware);
}
if (element.middleware == middleware) {
if (include) {
nodes.add(element.middleware);
}
break;
}
}
return Pipeline.fromIterable(nodes);
}
Pipeline addMiddleware(Middleware middleware) {
_middleware.addChild(middleware);
last.insertAfter(middleware);
return this;
}
Middleware get middleware {
return _middleware;
void setClient(MiddlewareClient? client) {
for (var node = first; node != end; node = node.child) {
node.middleware?.httpClient = client;
}
}
Pipeline operator +(Pipeline other) {
final copy = _middleware.deepCopy()..addChild(other.middleware);
return Pipeline()..addMiddleware(copy);
Future<MiddlewareRequest> onRequest(MiddlewareRequest request) async {
MiddlewareRequest req = request;
for (var node = first; node != end; node = node.child) {
req = await node.middleware?.onRequest(req) ?? req;
}
return req;
}
Future<MiddlewareResponse> onResponse(MiddlewareResponse response) async {
MiddlewareResponse res = response;
for (var node = last; node != begin; node = node.parent) {
res = await node.middleware?.onResponse(res) ?? res;
}
return res;
}
String getLogic() {
final req = <String>[];
final res = <String>[];
Middleware? m = _middleware;
while (m != null) {
if (m is! DefaultMiddleware) {
req.add('$m');
res.insert(0, '$m');
}
m = m.child;
for (final m in middlewares) {
req.add('${m.middleware}');
res.insert(0, '${m.middleware}');
}
return '[Req] -> ${req.join(' -> ')}\n[Res] -> ${res.join(' -> ')}';
}
@override
String toString() {
return middleware.toString();
return getLogic();
}
}

View File

@ -0,0 +1,28 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
enum HttpMethods {
head('HEAD'),
get('GET'),
post('POST'),
put('PUT'),
patch('PATCH'),
delete('DELETE');
final String method;
const HttpMethods(this.method);
}

View File

@ -15,69 +15,109 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
/// Status codes for HTTP responses. Extracted from dart:io
abstract class HttpStatus {
static const int continue_ = 100;
static const int switchingProtocols = 101;
static const int processing = 102;
static const int ok = 200;
static const int created = 201;
static const int accepted = 202;
static const int nonAuthoritativeInformation = 203;
static const int noContent = 204;
static const int resetContent = 205;
static const int partialContent = 206;
static const int multiStatus = 207;
static const int alreadyReported = 208;
static const int imUsed = 226;
static const int multipleChoices = 300;
static const int movedPermanently = 301;
static const int found = 302;
static const int movedTemporarily = 302; // Common alias for found.
static const int seeOther = 303;
static const int notModified = 304;
static const int useProxy = 305;
static const int temporaryRedirect = 307;
static const int permanentRedirect = 308;
static const int badRequest = 400;
static const int unauthorized = 401;
static const int paymentRequired = 402;
static const int forbidden = 403;
static const int notFound = 404;
static const int methodNotAllowed = 405;
static const int notAcceptable = 406;
static const int proxyAuthenticationRequired = 407;
static const int requestTimeout = 408;
static const int conflict = 409;
static const int gone = 410;
static const int lengthRequired = 411;
static const int preconditionFailed = 412;
static const int requestEntityTooLarge = 413;
static const int requestUriTooLong = 414;
static const int unsupportedMediaType = 415;
static const int requestedRangeNotSatisfiable = 416;
static const int expectationFailed = 417;
static const int misdirectedRequest = 421;
static const int unprocessableEntity = 422;
static const int locked = 423;
static const int failedDependency = 424;
static const int upgradeRequired = 426;
static const int preconditionRequired = 428;
static const int tooManyRequests = 429;
static const int requestHeaderFieldsTooLarge = 431;
static const int connectionClosedWithoutResponse = 444;
static const int unavailableForLegalReasons = 451;
static const int clientClosedRequest = 499;
static const int internalServerError = 500;
static const int notImplemented = 501;
static const int badGateway = 502;
static const int serviceUnavailable = 503;
static const int gatewayTimeout = 504;
static const int httpVersionNotSupported = 505;
static const int variantAlsoNegotiates = 506;
static const int insufficientStorage = 507;
static const int loopDetected = 508;
static const int notExtended = 510;
static const int networkAuthenticationRequired = 511;
enum HttpStatus {
continue_(100),
switchingProtocols(101),
processing(102),
ok(200),
created(201),
accepted(202),
nonAuthoritativeInformation(203),
noContent(204),
resetContent(205),
partialContent(206),
multiStatus(207),
alreadyReported(208),
imUsed(226),
multipleChoices(300),
movedPermanently(301),
found(302),
movedTemporarily(302), // Common alias for found.
seeOther(303),
notModified(304),
useProxy(305),
temporaryRedirect(307),
permanentRedirect(308),
badRequest(400),
unauthorized(401),
paymentRequired(402),
forbidden(403),
notFound(404),
methodNotAllowed(405),
notAcceptable(406),
proxyAuthenticationRequired(407),
requestTimeout(408),
conflict(409),
gone(410),
lengthRequired(411),
preconditionFailed(412),
requestEntityTooLarge(413),
requestUriTooLong(414),
unsupportedMediaType(415),
requestedRangeNotSatisfiable(416),
expectationFailed(417),
misdirectedRequest(421),
unprocessableEntity(422),
locked(423),
failedDependency(424),
upgradeRequired(426),
preconditionRequired(428),
tooManyRequests(429),
requestHeaderFieldsTooLarge(431),
connectionClosedWithoutResponse(444),
unavailableForLegalReasons(451),
clientClosedRequest(499),
internalServerError(500),
notImplemented(501),
badGateway(502),
serviceUnavailable(503),
gatewayTimeout(504),
httpVersionNotSupported(505),
variantAlsoNegotiates(506),
insufficientStorage(507),
loopDetected(508),
notExtended(510),
networkAuthenticationRequired(511),
// Client generated status code.
static const int networkConnectTimeoutError = 599;
networkConnectTimeoutError(599);
final int statusCode;
const HttpStatus(this.statusCode);
bool equals(Object other) {
if (other is HttpStatus) {
return statusCode == other.statusCode;
}
if (other is int) {
return statusCode == other;
}
return false;
}
bool isInfo() {
return statusCode >= 100 && statusCode < 200;
}
bool isSuccess() {
return statusCode >= 200 && statusCode < 300;
}
bool isRedirection() {
return statusCode >= 300 && statusCode < 400;
}
bool isClientError() {
return statusCode >= 400 && statusCode < 500;
}
bool isServerError() {
return statusCode >= 500 && statusCode < 600;
}
factory HttpStatus.from(int status) {
return HttpStatus.values
.firstWhere((element) => element.statusCode == status);
}
}