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

✨ Restructure interceptors #547

Merged
merged 34 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5da6cf9
:construction: Created base for interceptor chain implementation.
Guldem Jan 4, 2024
9080848
:white_check_mark: Updated interceptor tests. Removed InterceptorFunc…
Guldem Jan 4, 2024
578322c
:white_check_mark: Updated authenticator tests
Guldem Jan 4, 2024
4b84040
:white_check_mark: Updated base tests
Guldem Jan 4, 2024
4f5e34a
:white_check_mark: Updated client tests
Guldem Jan 4, 2024
a18a972
:white_check_mark: Updated http log interceptor tests
Guldem Jan 4, 2024
14050f8
:art: Improved chain proceed checks
Guldem Jan 5, 2024
5228433
:art: Fixed passing of generic types
Guldem Jan 5, 2024
df7a51d
Merge branch 'develop' of github.com:lejard-h/chopper into feature/re…
Guldem Jan 5, 2024
f78c3d3
:art: Improve passing of generic (BodyType) within InterceptorChain
Guldem Jan 9, 2024
b49c033
Merge branch 'develop' of github.com:lejard-h/chopper into feature/re…
Guldem Jan 9, 2024
fb38e24
:white_check_mark: Added authenticator interceptor tests
Guldem Jan 10, 2024
9959e9b
:art: Updated interceptor chain constraints.
Guldem Jan 10, 2024
e8bed4e
:white_check_mark: Added tests for interceptor chain and updated auth…
Guldem Jan 10, 2024
21b4adf
:white_check_mark: Added request_converter_interceptor_test
Guldem Jan 10, 2024
0f00054
:white_check_mark: Added response_converter_interceptor_test
Guldem Jan 23, 2024
322c6af
Merge branch 'develop' of github.com:lejard-h/chopper into feature/re…
Guldem Jan 23, 2024
a13db02
Merge branch 'develop' of github.com:lejard-h/chopper into feature/re…
Guldem Jan 25, 2024
b99538e
Merge branch 'develop' of github.com:lejard-h/chopper into feature/re…
Guldem Mar 1, 2024
714f430
:art: Added dart doc to new classes. Restructured some files and upda…
Guldem Mar 1, 2024
81c72fc
:goal_net: Added custom exception
Guldem Mar 1, 2024
8cad462
:memo: Updated documentation
Guldem Mar 1, 2024
3cbfd27
:art: Fixed request and response stream controller usages
Guldem Mar 1, 2024
05569ef
:sparkles: Added response time to http interceptor logger
Guldem Mar 1, 2024
76a0235
:rotating_light: Fix lint warnings
Guldem Mar 1, 2024
fd3cb3f
:fire: Removed old import
Guldem Mar 5, 2024
3547a84
:art: Changed request stream interceptor callback to async callback
Guldem Mar 12, 2024
91c9953
:art: Applied nitpicky feedback 😜(shortened invocations)
Guldem Mar 12, 2024
3eeaa15
:memo: Added more documentation on breaking out of an interceptor
Guldem Mar 12, 2024
5ca29f8
:art: Applied correct FutureOr as defined in the Interceptor interface.
Guldem Mar 13, 2024
43a8c3b
:memo: Added information about breaking to the faq with reference to …
Guldem Mar 13, 2024
204d034
:art: Formatted files
Guldem Mar 13, 2024
d76e4f7
Merge branch 'develop' of github.com:lejard-h/chopper into feature/re…
Guldem Mar 13, 2024
25f6d71
Merge branch 'develop' of github.com:lejard-h/chopper into feature/re…
Guldem Apr 5, 2024
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
6 changes: 4 additions & 2 deletions chopper/lib/chopper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ export 'src/annotations.dart';
export 'src/authenticator.dart';
export 'src/base.dart';
export 'src/chopper_http_exception.dart';
export 'src/chopper_exception.dart';
export 'src/chopper_log_record.dart';
export 'src/constants.dart';
export 'src/extensions.dart';
export 'src/http_logging_interceptor.dart';
export 'src/interceptor.dart';
export 'src/chain/chain.dart';
export 'src/interceptors/interceptor.dart';
export 'src/converters.dart';
export 'src/list_format.dart';
export 'src/request.dart';
export 'src/response.dart';
Expand Down
2 changes: 1 addition & 1 deletion chopper/lib/src/annotations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ typedef ConvertRequest = FutureOr<Request> Function(Request request);

/// A function that should convert the body of a [Response] from the HTTP
/// representation to a Dart object.
typedef ConvertResponse<T> = FutureOr<Response> Function(Response response);
typedef ConvertResponse<T> = FutureOr<Response<T>> Function(Response response);

/// {@template FactoryConverter}
/// Defines custom [Converter] methods for a single network API endpoint.
Expand Down
2 changes: 1 addition & 1 deletion chopper/lib/src/authenticator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ abstract class Authenticator {
/// Returns a [Request] that includes credentials to satisfy
/// an authentication challenge received in [response], based on
/// the incoming [request] or optionally, the [originalRequest]
/// (which was not modified with any previous [RequestInterceptor]s).
/// (which was not modified with any previous [Interceptor]s).
///
/// Otherwise, return `null` if the challenge cannot be satisfied.
///
Expand Down
217 changes: 21 additions & 196 deletions chopper/lib/src/base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,15 @@ import 'dart:async';

import 'package:chopper/src/annotations.dart';
import 'package:chopper/src/authenticator.dart';
import 'package:chopper/src/chain/call.dart';
import 'package:chopper/src/constants.dart';
import 'package:chopper/src/interceptor.dart';
import 'package:chopper/src/converters.dart';
import 'package:chopper/src/interceptors/interceptor.dart';
import 'package:chopper/src/request.dart';
import 'package:chopper/src/response.dart';
import 'package:chopper/src/utils.dart';
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';

@visibleForTesting
const List<Type> allowedInterceptorsType = [
RequestInterceptor,
RequestInterceptorFunc,
ResponseInterceptor,
ResponseInterceptorFunc1,
ResponseInterceptorFunc2,
DynamicResponseInterceptorFunc,
];

/// ChopperClient is the main class of the Chopper API.
///
/// It manages registered services, encodes and decodes data, and intercepts
Expand All @@ -46,8 +37,7 @@ base class ChopperClient {
final ErrorConverter? errorConverter;

late final Map<Type, ChopperService> _services;
late final List _requestInterceptors;
late final List _responseInterceptors;
late final List<Interceptor> interceptors;
final StreamController<Request> _requestController =
StreamController<Request>.broadcast();
final StreamController<Response> _responseController =
Expand Down Expand Up @@ -79,11 +69,10 @@ base class ChopperClient {
/// );
/// ```
///
/// [RequestInterceptor]s and [ResponseInterceptor]s can be added to the client
/// [Interceptor]s can be added to the client
/// with the [interceptors] parameter.
///
/// See [RequestInterceptor], [ResponseInterceptor], [HttpLoggingInterceptor],
/// [HeadersInterceptor], [CurlInterceptor]
/// See [HttpLoggingInterceptor], [HeadersInterceptor], [CurlInterceptor]
///
/// ```dart
/// final chopper = ChopperClient(
Expand Down Expand Up @@ -114,7 +103,7 @@ base class ChopperClient {
ChopperClient({
Uri? baseUrl,
http.Client? client,
Iterable? interceptors,
this.interceptors = const [],
this.authenticator,
this.converter,
this.errorConverter,
Expand All @@ -126,36 +115,13 @@ base class ChopperClient {
),
baseUrl = baseUrl ?? Uri(),
httpClient = client ?? http.Client(),
_clientIsInternal = client == null,
assert(
interceptors?.every(_isAnInterceptor) ?? true,
'Unsupported type for interceptors, it only support the following types:\n'
' - ${allowedInterceptorsType.join('\n - ')}',
),
_requestInterceptors = [
...?interceptors?.where(_isRequestInterceptor),
],
_responseInterceptors = [
...?interceptors?.where(_isResponseInterceptor),
] {
_clientIsInternal = client == null {
_services = <Type, ChopperService>{
for (final ChopperService service in services?.toSet() ?? [])
service.definitionType: service..client = this
};
}

static bool _isRequestInterceptor(value) =>
value is RequestInterceptor || value is RequestInterceptorFunc;

static bool _isResponseInterceptor(value) =>
value is ResponseInterceptor ||
value is ResponseInterceptorFunc1 ||
value is ResponseInterceptorFunc2 ||
value is DynamicResponseInterceptorFunc;

static bool _isAnInterceptor(value) =>
_isResponseInterceptor(value) || _isRequestInterceptor(value);

/// Retrieve any service included in the [ChopperClient]
///
/// ```dart
Expand Down Expand Up @@ -183,100 +149,6 @@ base class ChopperClient {
return service as ServiceType;
}

Future<Request> _encodeRequest(Request request) async =>
converter?.convertRequest(request) ?? request;

static Future<Response<BodyType>> _decodeResponse<BodyType, InnerType>(
Response response,
Converter withConverter,
) async =>
await withConverter.convertResponse<BodyType, InnerType>(response);

Future<Request> _interceptRequest(Request req) async {
final body = req.body;
for (final i in _requestInterceptors) {
if (i is RequestInterceptor) {
req = await i.onRequest(req);
} else if (i is RequestInterceptorFunc) {
req = await i(req);
}
}

assert(
body == req.body,
'Interceptors should not transform the body of the request'
'Use Request converter instead',
);

return req;
}

Future<Response<BodyType>> _interceptResponse<BodyType, InnerType>(
Response<BodyType> res,
) async {
final body = res.body;
for (final i in _responseInterceptors) {
if (i is ResponseInterceptor) {
res = await i.onResponse(res) as Response<BodyType>;
} else if (i is ResponseInterceptorFunc1) {
res = await i<BodyType>(res);
} else if (i is ResponseInterceptorFunc2) {
res = await i<BodyType, InnerType>(res);
} else if (i is DynamicResponseInterceptorFunc) {
res = await i(res) as Response<BodyType>;
}
}

assert(
body == res.body,
'Interceptors should not transform the body of the response'
'Use Response converter instead',
);

return res;
}

Future<Response<BodyType>> _handleErrorResponse<BodyType, InnerType>(
Response response,
) async {
var error = response.body;
if (errorConverter != null) {
final errorRes = await errorConverter?.convertError<BodyType, InnerType>(
response,
);
error = errorRes?.error ?? errorRes?.body;
}

return Response<BodyType>(response.base, null, error: error);
}

Future<Response<BodyType>> _handleSuccessResponse<BodyType, InnerType>(
Response response,
ConvertResponse? responseConverter,
) async {
if (responseConverter != null) {
response = await responseConverter(response);
} else if (converter != null) {
response =
await _decodeResponse<BodyType, InnerType>(response, converter!);
}

return Response<BodyType>(
response.base,
response.body,
);
}

Future<Request> _handleRequestConverter(
Request request,
ConvertRequest? requestConverter,
) async =>
request.body != null || request.parts.isNotEmpty
? requestConverter != null
? await requestConverter(request)
: await _encodeRequest(request)
: request;

/// Sends a pre-build [Request], applying all provided [Interceptor]s and
/// [Converter]s.
///
Expand All @@ -292,63 +164,22 @@ base class ChopperClient {
Future<Response<BodyType>> send<BodyType, InnerType>(
Request request, {
ConvertRequest? requestConverter,
ConvertResponse? responseConverter,
ConvertResponse<BodyType>? responseConverter,
}) async {
final Request req = await _interceptRequest(
await _handleRequestConverter(request, requestConverter),
final call = Call(
request: request,
client: this,
requestCallback: _requestController.add,
);

_requestController.add(req);

final streamRes = await httpClient.send(await req.toBaseRequest());
if (isTypeOf<BodyType, Stream<List<int>>>()) {
return Response(streamRes, (streamRes.stream) as BodyType);
}

final response = await http.Response.fromStream(streamRes);
dynamic res = Response(response, response.body);

if (authenticator != null) {
final Request? updatedRequest =
await authenticator!.authenticate(req, res, request);

if (updatedRequest != null) {
res = await send<BodyType, InnerType>(
updatedRequest,
requestConverter: requestConverter,
responseConverter: responseConverter,
);
// To prevent double call with typed response
if (_responseIsSuccessful(res.statusCode)) {
await authenticator!.onAuthenticationSuccessful
?.call(updatedRequest, res, request);
return _processResponse(res);
} else {
res = await _handleErrorResponse<BodyType, InnerType>(res);
await authenticator!.onAuthenticationFailed
?.call(updatedRequest, res, request);
return _processResponse(res);
}
}
}

res = _responseIsSuccessful(res.statusCode)
? await _handleSuccessResponse<BodyType, InnerType>(
res,
responseConverter,
)
: await _handleErrorResponse<BodyType, InnerType>(res);

return _processResponse(res);
}
final response = await call.execute<BodyType, InnerType>(
requestConverter,
responseConverter,
);

Future<Response<BodyType>> _processResponse<BodyType, InnerType>(
dynamic res,
) async {
res = await _interceptResponse<BodyType, InnerType>(res);
_responseController.add(res);
_responseController.add(response);

return res;
return response;
}

/// Makes a HTTP GET request using the [send] function.
Expand Down Expand Up @@ -501,20 +332,17 @@ base class ChopperClient {
_responseController.close();
_services.clear();

_requestInterceptors.clear();
_responseInterceptors.clear();

if (_clientIsInternal) {
httpClient.close();
}
}

/// A stream of processed [Request]s, as in after all [Converter]s, and
/// [RequestInterceptor]s have been run.
/// [Interceptor]s have been run.
Stream<Request> get onRequest => _requestController.stream;

/// A stream of processed [Response]s, as in after all [Converter]s and
/// [ResponseInterceptor]s have been run.
/// [Interceptor]s have been run.
Stream<Response> get onResponse => _responseController.stream;
}

Expand Down Expand Up @@ -548,6 +376,3 @@ abstract class ChopperService {
// TODO: use runtimeType
Type get definitionType;
}

bool _responseIsSuccessful(int statusCode) =>
statusCode >= 200 && statusCode < 300;
58 changes: 58 additions & 0 deletions chopper/lib/src/chain/call.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import 'package:chopper/src/annotations.dart';
import 'package:chopper/src/base.dart';
import 'package:chopper/src/chain/interceptor_chain.dart';
import 'package:chopper/src/interceptors/authenticator_interceptor.dart';
import 'package:chopper/src/interceptors/http_call_interceptor.dart';
import 'package:chopper/src/interceptors/interceptor.dart';
import 'package:chopper/src/interceptors/request_converter_interceptor.dart';
import 'package:chopper/src/interceptors/request_stream_interceptor.dart';
import 'package:chopper/src/interceptors/response_converter_interceptor.dart';
import 'package:chopper/src/request.dart';
import 'package:chopper/src/response.dart';

/// {@template Call}
/// A single call to a HTTP endpoint. It holds the [request] and the [client].
/// {@endtemplate}
class Call {
/// {@macro Call}
Call({
required this.request,
required this.client,
required this.requestCallback,
});

/// Request to be executed.
final Request request;

/// Chopper client that created this call.
final ChopperClient client;

/// Callback to send intercepted and converted request to the stream controller.
final void Function(Request event) requestCallback;

Future<Response<BodyType>> execute<BodyType, InnerType>(
ConvertRequest? requestConverter,
ConvertResponse<BodyType>? responseConverter,
) async {
final interceptors = <Interceptor>[
RequestConverterInterceptor(client.converter, requestConverter),
...client.interceptors,
RequestStreamInterceptor(requestCallback),
if (client.authenticator != null)
AuthenticatorInterceptor(client.authenticator!),
ResponseConverterInterceptor<InnerType>(
converter: client.converter,
errorConverter: client.errorConverter,
responseConverter: responseConverter,
),
HttpCallInterceptor(client.httpClient),
];

final interceptorChain = InterceptorChain<BodyType>(
request: request,
interceptors: interceptors,
);

return await interceptorChain.proceed(request);
}
}
Loading
Loading