diff --git a/CHANGELOG.md b/CHANGELOG.md
index 60fc1efb..af4dc123 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## 2022-06-24
+
+### Changes
+
+---
+
+Packages with breaking changes:
+
+ - There are no breaking changes in this release.
+
+Packages with other changes:
+
+ - [`wyatt_http_client` - `v1.2.0`](#wyatt_http_client---v120)
+
+---
+
+#### `wyatt_http_client` - `v1.2.0`
+
+ - **FEAT**: add new middleware feature.
+ - **FEAT**: implements doublelinked list for middlewares.
+ - **FEAT**: [WIP] implements middleware system.
+ - **FEAT**: [WIP] work on middleware feature.
+
+
## 2022-05-23
### Changes
diff --git a/packages/wyatt_http_client/CHANGELOG.md b/packages/wyatt_http_client/CHANGELOG.md
index a9868ded..f597ef73 100644
--- a/packages/wyatt_http_client/CHANGELOG.md
+++ b/packages/wyatt_http_client/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 1.2.0
+
+ - **FEAT**: add new middleware feature.
+ - **FEAT**: implements doublelinked list for middlewares.
+ - **FEAT**: [WIP] implements middleware system.
+ - **FEAT**: [WIP] work on middleware feature.
+
## 1.1.0
- **FEAT**: add oauth2 refresh token client.
diff --git a/packages/wyatt_http_client/README.md b/packages/wyatt_http_client/README.md
index 8b55e735..ee2b78d5 100644
--- a/packages/wyatt_http_client/README.md
+++ b/packages/wyatt_http_client/README.md
@@ -1,39 +1,182 @@
-
-TODO: Put a short description of the package here that helps potential users
-know whether this package might be useful for them.
+# Dart - HTTP Client
-## Features
+
+
+
+
+
+
-TODO: List what your package can do. Maybe include images, gifs, or videos.
+HTTP Client for Dart with Middlewares !
## Getting started
-TODO: List prerequisites and provide or point to information on how to
-start using the package.
+Simply add wyatt_http_client in pubspec.yaml, then
+
+```dart
+import 'package:wyatt_http_client/wyatt_http_client.dart';
+```
## Usage
-TODO: Include short and useful examples for package users. Add longer examples
-to `/example` folder.
+Firstly you have to understand **Middleware** and **Pipeline** concepts.
+
+In `wyatt_http_client` a middleware is an object where requests and responses
+pass through. And a pipeline is basicaly a list of middlewares.
+
+In a pipeline with middlewares A and B, if request pass through A, then B,
+the response will pass through B then A.
+
+> You can `print(pipeline)` to get full process order of a pipeline.
+
+For example, if you want to log every request, and simplify an url you can use provided `SimpleLogger` and `UriPrefix` .
```dart
-const like = 'sample';
+// Create the Pipeline
+final Pipeline pipeline = Pipeline()
+ .addMiddleware(
+ UriPrefixMiddleware(
+ protocol: Protocols.http,
+ authority: 'localhost:80',
+ ),
+ )
+ .addMiddleware(SimpleLoggerMiddleware());
```
-## Additional information
+Then if you print the pipeline,
-TODO: Tell users more about the package: where to find more information, how to
-contribute to the package, how to file issues, what response they can expect
-from the package authors, and more.
+```
+[Req] -> UriPrefix -> SimpleLogger
+[Res] -> SimpleLogger
+```
+
+> The `response` doesn't pass through `UriPrefix` because it's an `OnRequestMiddleware` only.
+
+And you can create a client.
+
+```dart
+final client = MiddlewareClient(pipeline: pipeline);
+```
+
+At this point you can use `client` like every Client from `package:http/http.dart` .
+
+## Recipes
+
+### Rest API with URL Authentication
+
+Let's build a client for a REST API where the (bad) authentication is through the URL.
+We need some middlewares:
+
+* SimpleLogger, to log every request and response (useful for debug).
+* BodyToJson, to automaticaly transform Map object to JSON.
+* UriPrefix, to simplify the build of an API Object (save protocol and API prefix).
+* UnsafeAuth, to use url based authentication.
+
+Let's start by creating the Pipeline:
+
+```dart
+final Pipeline pipeline = Pipeline()
+ .addMiddleware(
+ UriPrefixMiddleware(
+ protocol: Protocols.http,
+ authority: 'localhost:80',
+ ),
+ )
+ .addMiddleware(BodyToJsonMiddleware())
+ .addMiddleware(
+ UnsafeAuthMiddleware(
+ username: 'wyatt',
+ password: 'motdepasse',
+ ),
+ )
+ .addMiddleware(SimpleLoggerMiddleware());
+```
+
+Then simply create a client and make a call.
+
+```dart
+final client = MiddlewareClient(pipeline: pipeline);
+
+await client.get(Uri.parse('/protected'));
+```
+
+> Here it make a `GET` call on `http://localhost:80/protected?username=wyatt&password=motdepasse`
+
+And voilĂ .
+
+### Rest API with Oauth2
+
+So now we want a real authentication.
+
+```dart
+final Pipeline pipeline = Pipeline()
+ .addMiddleware(
+ UriPrefixMiddleware(
+ protocol: Protocols.http,
+ authority: 'localhost:80',
+ ),
+ )
+ .addMiddleware(BodyToJsonMiddleware())
+ .addMiddleware(
+ RefreshTokenAuthMiddleware(
+ authorizationEndpoint: '/auth/sign-in',
+ tokenEndpoint: '/auth/refresh',
+ accessTokenParser: (body) => body['access_token']! as String,
+ refreshTokenParser: (body) => body['refresh_token']! as String,
+ unauthorized: HttpStatus.forbidden,
+ ),
+ )
+ .addMiddleware(SimpleLoggerMiddleware());
+```
+
+> Here we just change `UnsafeAuthMiddleware` by `RefreshTokenAuthMiddleware` and the whole app while adapt to a new authentication system.
+
+### Create a new Middleware
+
+You can create your own middleware by implementing `Middleware` class, and use mixins to add `OnRequest` and/or `OnResponse` methods.
+
+```dart
+class SimpleLoggerMiddleware
+ with OnRequestMiddleware, OnResponseMiddleware
+ implements Middleware {
+
+ @override
+ String getName() => 'SimpleLogger';
+
+ @override
+ Future onRequest(
+ MiddlewareContext context,
+ MiddlewareRequest request,
+ ) async {
+ print('${getName()}::OnRequest');
+ return request;
+ }
+
+ @override
+ Future onResponse(
+ MiddlewareContext context,
+ MiddlewareResponse response,
+ ) async {
+ print('${getName()}::OnResponse');
+ return response;
+ }
+}
+```
diff --git a/packages/wyatt_http_client/example/http_client_example.dart b/packages/wyatt_http_client/example/http_client_example.dart
index e8766148..57b16bee 100644
--- a/packages/wyatt_http_client/example/http_client_example.dart
+++ b/packages/wyatt_http_client/example/http_client_example.dart
@@ -17,14 +17,7 @@
import 'dart:async';
import 'dart:io';
-import 'package:wyatt_http_client/src/authentication/basic_authentication_client.dart';
-import 'package:wyatt_http_client/src/authentication/bearer_authentication_client.dart';
-import 'package:wyatt_http_client/src/authentication/digest_authentication_client.dart';
-import 'package:wyatt_http_client/src/authentication/refresh_token_client.dart';
-import 'package:wyatt_http_client/src/authentication/unsafe_authentication_client.dart';
-import 'package:wyatt_http_client/src/rest_client.dart';
-import 'package:wyatt_http_client/src/utils/header_keys.dart';
-import 'package:wyatt_http_client/src/utils/protocols.dart';
+import 'package:wyatt_http_client/wyatt_http_client.dart';
String lastToken = '';
int token = 0;
@@ -42,7 +35,7 @@ Future handleBasic(HttpRequest req) async {
Future handleBasicNegotiate(HttpRequest req) async {
if (req.headers.value('Authorization') == null) {
- req.response.statusCode = HttpStatus.unauthorized;
+ req.response.statusCode = HttpStatus.unauthorized.statusCode;
req.response.headers.set(HeaderKeys.wwwAuthenticate, 'Basic realm="Wyatt"');
print(req.response.headers.value('WWW-Authenticate'));
return req.response.close();
@@ -56,7 +49,7 @@ Future handleBearer(HttpRequest req) async {
Future handleDigest(HttpRequest req) async {
if (req.headers.value('Authorization') == null) {
- req.response.statusCode = HttpStatus.unauthorized;
+ req.response.statusCode = HttpStatus.unauthorized.statusCode;
req.response.headers.set(
'WWW-Authenticate',
'Digest realm="Wyatt", '
@@ -110,7 +103,7 @@ Future handleOauth2RefreshToken(HttpRequest req) async {
return req.response.close();
} else {
lastToken = receivedToken;
- req.response.statusCode = HttpStatus.unauthorized;
+ req.response.statusCode = HttpStatus.unauthorized.statusCode;
return req.response.close();
}
default:
@@ -160,13 +153,13 @@ Future server() async {
print('Authorized');
error = 0;
} else {
- request.response.statusCode = HttpStatus.unauthorized;
+ request.response.statusCode = HttpStatus.unauthorized.statusCode;
}
break;
case '/test/oauth2-test-timeout':
error++;
print('Error $error');
- request.response.statusCode = HttpStatus.unauthorized;
+ request.response.statusCode = HttpStatus.unauthorized.statusCode;
break;
case '/test/oauth2-login':
if (request.method == 'POST') {
@@ -189,12 +182,12 @@ Future server() async {
}
break;
case '/test/oauth2-refresh-error':
- request.response.statusCode = HttpStatus.unauthorized;
+ request.response.statusCode = HttpStatus.unauthorized.statusCode;
break;
default:
print(' => Unknown path or method');
- request.response.statusCode = HttpStatus.notFound;
+ request.response.statusCode = HttpStatus.notFound.statusCode;
}
request.response.close();
print('====================');
@@ -204,73 +197,98 @@ Future server() async {
Future main() async {
unawaited(server());
final base = 'localhost:8080';
- final restClient = RestClient(protocol: Protocols.http, authority: base);
+ final uriPrefix = UriPrefixMiddleware(
+ protocol: Protocols.http,
+ authority: base,
+ );
+ final jsonEncoder = BodyToJsonMiddleware();
+ final logger = SimpleLoggerMiddleware();
// Basic
- final basic = BasicAuthenticationClient(
+ final basicAuth = BasicAuthMiddleware(
username: 'username',
password: 'password',
- inner: restClient,
+ );
+ final basic = MiddlewareClient(
+ pipeline: Pipeline.fromIterable([
+ uriPrefix,
+ basicAuth,
+ logger,
+ ]),
);
await basic.get(Uri.parse('/test/basic-test'));
- // Basic with negotiate
- final basicWithNegotiate = BasicAuthenticationClient(
- username: 'username',
- password: 'password',
- preemptive: false,
- inner: restClient,
- );
- await basicWithNegotiate.get(Uri.parse('/test/basic-test-with-negotiate'));
-
// Digest
- final digest = DigestAuthenticationClient(
+ final digestAuth = DigestAuthMiddleware(
username: 'Mufasa',
password: 'Circle Of Life',
- inner: restClient,
+ );
+ final digest = MiddlewareClient(
+ pipeline: Pipeline.fromIterable([
+ uriPrefix,
+ digestAuth,
+ logger,
+ ]),
);
await digest.get(Uri.parse('/test/digest-test'));
- // Bearer
- final bearer = BearerAuthenticationClient(
- token: 'access-token-test',
- inner: restClient,
- );
- await bearer.get(Uri.parse('/test/bearer-test'));
+ // // Bearer
+ // final bearer = BearerAuthenticationClient(
+ // token: 'access-token-test',
+ // inner: restClient,
+ // );
+ // await bearer.get(Uri.parse('/test/bearer-test'));
- // API Key
- final apiKey = BearerAuthenticationClient(
- token: 'awesome-api-key',
- authenticationMethod: 'ApiKey',
- inner: restClient,
- );
- await apiKey.get(Uri.parse('/test/apikey-test'));
+ // // API Key
+ // final apiKey = BearerAuthenticationClient(
+ // token: 'awesome-api-key',
+ // authenticationMethod: 'ApiKey',
+ // inner: restClient,
+ // );
+ // await apiKey.get(Uri.parse('/test/apikey-test'));
// Unsafe URL
- final unsafe = UnsafeAuthenticationClient(
+ final unsafeAuth = UnsafeAuthMiddleware(
username: 'Mufasa',
password: 'Circle Of Life',
- inner: restClient,
+ );
+ final unsafe = MiddlewareClient(
+ pipeline: Pipeline.fromIterable([
+ uriPrefix,
+ unsafeAuth,
+ logger,
+ ]),
);
await unsafe.get(Uri.parse('/test/unsafe-test'));
// OAuth2
- final refreshToken = RefreshTokenClient(
+ final refreshTokenAuth = RefreshTokenAuthMiddleware(
authorizationEndpoint: '/test/oauth2-test?action=login',
tokenEndpoint: '/test/oauth2-test?action=refresh',
accessTokenParser: (body) => body['accessToken']! as String,
refreshTokenParser: (body) => body['refreshToken']! as String,
- inner: restClient,
+ );
+ final refreshToken = MiddlewareClient(
+ pipeline: Pipeline.fromIterable([
+ uriPrefix,
+ jsonEncoder,
+ refreshTokenAuth,
+ logger,
+ ]),
);
await refreshToken.get(Uri.parse('/test/oauth2-test'));
- await refreshToken.authorize({
- 'username': 'username',
- 'password': 'password',
- });
+ // Login
+ await refreshToken.post(
+ Uri.parse('/test/oauth2-test'),
+ body: {
+ 'username': 'username',
+ 'password': 'password',
+ },
+ );
await refreshToken.get(Uri.parse('/test/oauth2-test'));
- await refreshToken.refresh();
- await refreshToken.get(Uri.parse('/test/oauth2-test'));
- await refreshToken.get(Uri.parse('/test/oauth2-test?action=access-denied'));
+ // await refreshToken.refresh();
+ // await refreshToken.get(Uri.parse('/test/oauth2-test'));
+ // await refreshToken.get(Uri.parse('/test/oauth2-test?action=access-denied'));
exit(0);
}
diff --git a/packages/wyatt_http_client/example/http_client_fastapi_example.dart b/packages/wyatt_http_client/example/http_client_fastapi_example.dart
new file mode 100644
index 00000000..5393611c
--- /dev/null
+++ b/packages/wyatt_http_client/example/http_client_fastapi_example.dart
@@ -0,0 +1,398 @@
+// 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 .
+
+// ignore_for_file: public_member_api_docs, sort_constructors_first
+import 'dart:convert';
+
+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_auth_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 {
+ signUp,
+ resetPassword,
+ changeEmail;
+
+ String toSnakeCase() {
+ return name.splitMapJoin(
+ RegExp('[A-Z]'),
+ onMatch: (m) => '_${m[0]?.toLowerCase()}',
+ onNonMatch: (n) => n,
+ );
+ }
+
+ factory EmailVerificationAction.fromString(String str) {
+ return EmailVerificationAction.values.firstWhere(
+ (EmailVerificationAction element) => element.toSnakeCase() == str,
+ );
+ }
+}
+
+class VerifyCode {
+ final String email;
+ final String verificationCode;
+ final EmailVerificationAction action;
+ VerifyCode({
+ required this.email,
+ required this.verificationCode,
+ required this.action,
+ });
+
+ VerifyCode copyWith({
+ String? email,
+ String? verificationCode,
+ EmailVerificationAction? action,
+ }) {
+ return VerifyCode(
+ email: email ?? this.email,
+ verificationCode: verificationCode ?? this.verificationCode,
+ action: action ?? this.action,
+ );
+ }
+
+ Map toMap() {
+ return {
+ 'email': email,
+ 'verification_code': verificationCode,
+ 'action': action.toSnakeCase(),
+ };
+ }
+
+ factory VerifyCode.fromMap(Map map) {
+ return VerifyCode(
+ email: map['email'] as String,
+ verificationCode: map['verification_code'] as String,
+ action: EmailVerificationAction.fromString(map['action'] as String),
+ );
+ }
+
+ String toJson() => json.encode(toMap());
+
+ factory VerifyCode.fromJson(String source) =>
+ VerifyCode.fromMap(json.decode(source) as Map);
+
+ @override
+ String toString() =>
+ 'VerifyCode(email: $email, verificationCode: $verificationCode, action: $action)';
+}
+
+class Account {
+ final String email;
+ final String? sessionId;
+ Account({
+ required this.email,
+ this.sessionId,
+ });
+
+ Account copyWith({
+ String? email,
+ String? sessionId,
+ }) {
+ return Account(
+ email: email ?? this.email,
+ sessionId: sessionId ?? this.sessionId,
+ );
+ }
+
+ Map toMap() {
+ return {
+ 'email': email,
+ 'session_id': sessionId,
+ };
+ }
+
+ factory Account.fromMap(Map map) {
+ return Account(
+ email: map['email'] as String,
+ sessionId: map['session_id'] != null ? map['session_id'] as String : null,
+ );
+ }
+
+ String toJson() => json.encode(toMap());
+
+ factory Account.fromJson(String source) =>
+ Account.fromMap(json.decode(source) as Map);
+
+ @override
+ String toString() => 'Account(email: $email, sessionId: $sessionId)';
+}
+
+class SignUp {
+ final String sessionId;
+ final String password;
+ SignUp({
+ required this.sessionId,
+ required this.password,
+ });
+
+ SignUp copyWith({
+ String? sessionId,
+ String? password,
+ }) {
+ return SignUp(
+ sessionId: sessionId ?? this.sessionId,
+ password: password ?? this.password,
+ );
+ }
+
+ Map toMap() {
+ return {
+ 'session_id': sessionId,
+ 'password': password,
+ };
+ }
+
+ factory SignUp.fromMap(Map map) {
+ return SignUp(
+ sessionId: map['session_id'] as String,
+ password: map['password'] as String,
+ );
+ }
+
+ String toJson() => json.encode(toMap());
+
+ factory SignUp.fromJson(String source) =>
+ SignUp.fromMap(json.decode(source) as Map);
+
+ @override
+ String toString() => 'SignUp(sessionId: $sessionId, password: $password)';
+}
+
+class TokenSuccess {
+ final String accessToken;
+ final String refreshToken;
+ final Account account;
+ TokenSuccess({
+ required this.accessToken,
+ required this.refreshToken,
+ required this.account,
+ });
+
+ TokenSuccess copyWith({
+ String? accessToken,
+ String? refreshToken,
+ Account? account,
+ }) {
+ return TokenSuccess(
+ accessToken: accessToken ?? this.accessToken,
+ refreshToken: refreshToken ?? this.refreshToken,
+ account: account ?? this.account,
+ );
+ }
+
+ Map toMap() {
+ return {
+ 'access_token': accessToken,
+ 'refresh_token': refreshToken,
+ 'account': account.toMap(),
+ };
+ }
+
+ factory TokenSuccess.fromMap(Map map) {
+ return TokenSuccess(
+ accessToken: map['access_token'] as String,
+ refreshToken: map['refresh_token'] as String,
+ account: Account.fromMap(map['account'] as Map),
+ );
+ }
+
+ String toJson() => json.encode(toMap());
+
+ factory TokenSuccess.fromJson(String source) =>
+ TokenSuccess.fromMap(json.decode(source) as Map);
+
+ @override
+ String toString() =>
+ 'TokenSuccess(accessToken: $accessToken, refreshToken: $refreshToken, account: $account)';
+}
+
+class Login {
+ final String email;
+ final String password;
+ Login({
+ required this.email,
+ required this.password,
+ });
+
+ Login copyWith({
+ String? email,
+ String? password,
+ }) {
+ return Login(
+ email: email ?? this.email,
+ password: password ?? this.password,
+ );
+ }
+
+ Map toMap() {
+ return {
+ 'email': email,
+ 'password': password,
+ };
+ }
+
+ factory Login.fromMap(Map map) {
+ return Login(
+ email: map['email'] as String,
+ password: map['password'] as String,
+ );
+ }
+
+ String toJson() => json.encode(toMap());
+
+ factory Login.fromJson(String source) =>
+ Login.fromMap(json.decode(source) as Map);
+
+ @override
+ String toString() => 'Login(email: $email, password: $password)';
+}
+
+class FastAPI {
+ final String baseUrl;
+ final MiddlewareClient client;
+ final int apiVersion;
+
+ FastAPI({
+ this.baseUrl = 'localhost:80',
+ MiddlewareClient? client,
+ this.apiVersion = 1,
+ }) : client = client ?? MiddlewareClient();
+
+ String get apiPath => '/api/v$apiVersion';
+
+ Future sendSignUpCode(String email) async {
+ final r = await client.post(
+ Uri.parse('$apiPath/auth/send-sign-up-code'),
+ body: {
+ 'email': email,
+ },
+ );
+ if (r.statusCode != 201) {
+ throw Exception('Invalid reponse: ${r.statusCode}');
+ }
+ }
+
+ Future verifyCode(VerifyCode verifyCode) async {
+ final r = await client.post(
+ Uri.parse('$apiPath/auth/verify-code'),
+ body: verifyCode.toMap(),
+ );
+ if (r.statusCode != 202) {
+ throw Exception('Invalid reponse: ${r.statusCode}');
+ } else {
+ return Account.fromMap(
+ (jsonDecode(r.body) as Map)['account']
+ as Map,
+ );
+ }
+ }
+
+ Future signUp(SignUp signUp) async {
+ final r = await client.post(
+ Uri.parse('$apiPath/auth/sign-up'),
+ body: signUp.toMap(),
+ );
+ if (r.statusCode != 201) {
+ throw Exception('Invalid reponse: ${r.statusCode}');
+ } else {
+ return Account.fromJson(r.body);
+ }
+ }
+
+ Future signInWithPassword(Login login) async {
+ 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 refresh() async {
+ // final r = await client.refresh();
+ // return TokenSuccess.fromJson(r?.body ?? '');
+ // }
+
+ Future> getAccountList() async {
+ final r = await client.get(
+ Uri.parse('$apiPath/account'),
+ );
+ if (r.statusCode != 200) {
+ throw Exception('Invalid reponse: ${r.statusCode}');
+ } else {
+ final list = (jsonDecode(r.body) as Map)['founds']
+ as List