Skip to content

Commit

Permalink
Merge branch 'develop' into release/chopper_generator/v7.1.0
Browse files Browse the repository at this point in the history
# Conflicts:
#	chopper/lib/src/annotations.dart
#	chopper/lib/src/http_logging_interceptor.dart
#	chopper/lib/src/interceptor.dart
#	chopper/lib/src/request.dart
#	chopper/lib/src/response.dart
#	chopper/test/ensure_build_test.dart
#	chopper/test/test_service.chopper.dart
#	chopper/test/test_service.dart
#	chopper/test/test_service_variable.chopper.dart
#	chopper/test/test_service_variable.dart
#	chopper_generator/lib/src/generator.dart
#	chopper_generator/lib/src/vars.dart
#	chopper_generator/test/ensure_build_test.dart
#	chopper_generator/test/test_service.chopper.dart
#	chopper_generator/test/test_service.dart
#	chopper_generator/test/test_service_variable.chopper.dart
#	chopper_generator/test/test_service_variable.dart
#	faq.md
  • Loading branch information
techouse committed Jan 9, 2024
2 parents aaaf482 + 33615ed commit 50c4b5e
Show file tree
Hide file tree
Showing 28 changed files with 2,556 additions and 97 deletions.
127 changes: 127 additions & 0 deletions chopper/lib/src/annotations.dart

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions chopper/lib/src/chopper_http_exception.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import 'package:chopper/src/response.dart';

/// An exception thrown when a [Response] is unsuccessful < 200 or > 300.
class ChopperHttpException implements Exception {
ChopperHttpException(this.response);

final Response response;

@override
String toString() {
return 'Could not fetch the response for ${response.base.request}. Status code: ${response.statusCode}, error: ${response.error}';
}
}
3 changes: 3 additions & 0 deletions chopper/lib/src/http_logging_interceptor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ enum Level {
body,
}

/// {@template http_logging_interceptor}
/// A [RequestInterceptor] and [ResponseInterceptor] implementation which logs
/// HTTP request and response data.
///
Expand All @@ -70,9 +71,11 @@ enum Level {
/// leak sensitive information, such as `Authorization` headers and user data
/// in response bodies. This interceptor should only be used in a controlled way
/// or in a non-production environment.
/// {@endtemplate}
@immutable
class HttpLoggingInterceptor
implements RequestInterceptor, ResponseInterceptor {
/// {@macro http_logging_interceptor}
HttpLoggingInterceptor({this.level = Level.body, Logger? logger})
: _logger = logger ?? chopperLogger,
_logBody = level == Level.body,
Expand Down
9 changes: 9 additions & 0 deletions chopper/lib/src/interceptor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,17 @@ abstract interface class ErrorConverter {
FutureOr<Response> convertError<BodyType, InnerType>(Response response);
}

/// {@template HeadersInterceptor}
/// A [RequestInterceptor] that adds [headers] to every request.
///
/// Note that this interceptor will overwrite existing headers having the same
/// keys as [headers].
/// {@endtemplate}
@immutable
class HeadersInterceptor implements RequestInterceptor {
final Map<String, String> headers;

/// {@macro HeadersInterceptor}
const HeadersInterceptor(this.headers);

@override
Expand Down Expand Up @@ -163,6 +166,7 @@ class CurlInterceptor implements RequestInterceptor {
}
}

/// {@template JsonConverter}
/// A [Converter] implementation that calls [json.encode] on [Request]s and
/// [json.decode] on [Response]s using the [dart:convert](https://api.dart.dev/stable/2.10.3/dart-convert/dart-convert-library.html)
/// package's [utf8] and [json] utilities.
Expand All @@ -176,8 +180,10 @@ class CurlInterceptor implements RequestInterceptor {
/// If content type header is modified (for example by using
/// `@Post(headers: {'content-type': '...'})`), `JsonConverter` won't add the
/// header and it won't call json.encode if content type is not JSON.
/// {@endtemplate}
@immutable
class JsonConverter implements Converter, ErrorConverter {
/// {@macro JsonConverter}
const JsonConverter();

@override
Expand Down Expand Up @@ -270,13 +276,16 @@ class JsonConverter implements Converter, ErrorConverter {
}
}

/// {@template FormUrlEncodedConverter}
/// A [Converter] implementation that converts only [Request]s having a [Map] as their body.
///
/// This `Converter` also adds the `content-type: application/x-www-form-urlencoded`
/// header to each request, but only if the `content-type` header is not set in
/// the original request.
/// {@endtemplate}
@immutable
class FormUrlEncodedConverter implements Converter, ErrorConverter {
/// {@macro FormUrlEncodedConverter}
const FormUrlEncodedConverter();

@override
Expand Down
3 changes: 3 additions & 0 deletions chopper/lib/src/request.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import 'package:equatable/equatable.dart' show EquatableMixin;
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';

/// {@template request}
/// This class represents an HTTP request that can be made with Chopper.
/// {@endtemplate}
base class Request extends http.BaseRequest with EquatableMixin {
final Uri uri;
final Uri baseUri;
Expand All @@ -17,6 +19,7 @@ base class Request extends http.BaseRequest with EquatableMixin {
final bool useBrackets;
final bool includeNullQueryVars;

/// {@macro request}
Request(
String method,
this.uri,
Expand Down
27 changes: 27 additions & 0 deletions chopper/lib/src/response.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import 'dart:typed_data';

import 'package:chopper/src/chopper_http_exception.dart';
import 'package:equatable/equatable.dart' show EquatableMixin;
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';

/// {@template response}
/// A [http.BaseResponse] wrapper representing a response of a Chopper network call.
///
/// ```dart
Expand All @@ -15,6 +17,7 @@ import 'package:meta/meta.dart';
/// @Get(path: '/items/{id}')
/// Future<Response<Item>> fetchItem();
/// ```
/// {@endtemplate}
@immutable
base class Response<BodyType> with EquatableMixin {
/// The [http.BaseResponse] from `package:http` that this [Response] wraps.
Expand All @@ -30,6 +33,7 @@ base class Response<BodyType> with EquatableMixin {
/// The body of the response if [isSuccessful] is false.
final Object? error;

/// {@macro response}
const Response(this.base, this.body, {this.error});

/// Makes a copy of this Response, replacing original values with the given ones.
Expand Down Expand Up @@ -67,6 +71,29 @@ base class Response<BodyType> with EquatableMixin {
String get bodyString =>
base is http.Response ? (base as http.Response).body : '';

/// Check if the response is an error and if the error is of type [ErrorType] and casts the error to [ErrorType]. Otherwise it returns null.
ErrorType? errorWhereType<ErrorType>() {
if (error != null && error is ErrorType) {
return error as ErrorType;
} else {
return null;
}
}

/// Returns the response body if [Response] [isSuccessful] and [body] is not null.
/// Otherwise it throws an [HttpException] with the response status code and error object.
/// If the error object is an [Exception], it will be thrown instead.
BodyType get bodyOrThrow {
if (isSuccessful && body != null) {
return body!;
} else {
if (error is Exception) {
throw error!;
}
throw ChopperHttpException(this);
}
}

@override
List<Object?> get props => [
base,
Expand Down
21 changes: 21 additions & 0 deletions chopper/test/chopper_http_exception_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'package:chopper/src/chopper_http_exception.dart';
import 'package:chopper/src/response.dart';
import 'package:http/http.dart' as http;
import 'package:test/test.dart';

void main() {
test('ChopperHttpException toString prints available information', () {
final request = http.Request('GET', Uri.parse('http://localhost:8000'));
final base = http.Response('Foobar', 400, request: request);
final response = Response(base, 'Foobar', error: 'FooError');

final exception = ChopperHttpException(response);

final result = exception.toString();

expect(
result,
'Could not fetch the response for GET http://localhost:8000. Status code: 400, error: FooError',
);
});
}
1 change: 1 addition & 0 deletions chopper/test/ensure_build_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ void main() {
gitDiffPathArguments: [
'test/test_service.chopper.dart',
'test/test_service_variable.chopper.dart',
'test/test_without_response_service.chopper.dart',
'test/test_service_base_url.chopper.dart',
],
);
Expand Down
3 changes: 3 additions & 0 deletions chopper/test/fixtures/error_fixtures.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class FooErrorType {
const FooErrorType();
}
114 changes: 114 additions & 0 deletions chopper/test/response_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import 'package:chopper/src/chopper_http_exception.dart';
import 'package:chopper/src/response.dart';
import 'package:http/http.dart' as http;
import 'package:test/test.dart';

import 'fixtures/error_fixtures.dart';

void main() {
group('Response error casting test', () {
test('Response is succesfull, [returns null]', () {
final base = http.Response('Foobar', 200);

final response = Response(base, 'Foobar');

final result = response.errorWhereType<FooErrorType>();

expect(result, isNull);
});

test('Response is unsuccessful and has no error object, [returns null]',
() {
final base = http.Response('Foobar', 400);

final response = Response(base, '');

final result = response.errorWhereType<FooErrorType>();

expect(result, isNull);
});

test(
'Response is unsuccessful and has error object of different type, [returns null]',
() {
final base = http.Response('Foobar', 400);

final response = Response(base, '', error: 'Foobar');

final result = response.errorWhereType<FooErrorType>();

expect(result, isNull);
});

test(
'Response is unsuccessful and has error object of specified type, [returns error as ErrorType]',
() {
final base = http.Response('Foobar', 400);

final response = Response(base, 'Foobar', error: FooErrorType());

final result = response.errorWhereType<FooErrorType>();

expect(result, isNotNull);
expect(result, isA<FooErrorType>());
});
});

group('bodyOrThrow tests', () {
test('Response is successful and has body, [bodyOrThrow returns body]', () {
final base = http.Response('Foobar', 200);
final response = Response(base, {'Foo': 'Bar'});

final result = response.bodyOrThrow;

expect(result, isNotNull);
expect(result, {'Foo': 'Bar'});
});

test(
'Response is unsuccessful and has Exception as error, [bodyOrThrow throws error]',
() {
final base = http.Response('Foobar', 400);
final response = Response(base, '', error: Exception('Error occurred'));

expect(() => response.bodyOrThrow, throwsA(isA<Exception>()));
});

test(
'Response is unsuccessful and has non-exception object as error, [bodyOrThrow throws error]',
() {
final base = http.Response('Foobar', 400);
final response = Response(base, '', error: 'Error occurred');

expect(() => response.bodyOrThrow, throwsA(isA<ChopperHttpException>()));
});

test(
'Response is unsuccessful and has no error, [bodyOrThrow throws ChopperHttpException]',
() {
final base = http.Response('Foobar', 400);
final response = Response(base, '');

expect(() => response.bodyOrThrow, throwsA(isA<ChopperHttpException>()));
});

test(
'Response is successful and has no body, [bodyOrThrow throws ChopperHttpException]',
() {
final base = http.Response('Foobar', 200);
final Response<String> response = Response(base, null);

expect(() => response.bodyOrThrow, throwsA(isA<ChopperHttpException>()));
});

test('Response is successful and has void body, [bodyOrThrow returns void]',
() {
final base = http.Response('Foobar', 200);
// Ignoring void checks for testing purposes
//ignore: void_checks
final Response<void> response = Response(base, '');

expect(() => response.bodyOrThrow, returnsNormally);
});
});
}
30 changes: 28 additions & 2 deletions chopper/test/test_service.chopper.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion chopper/test/test_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ abstract class HttpTestService extends ChopperService {
});

@Get(path: 'https://test.com')
Future fullUrl();
Future<Response> fullUrl();

@Get(path: '/list/string')
Future<Response<List<String>>> listString();
Expand Down Expand Up @@ -204,6 +204,15 @@ abstract class HttpTestService extends ChopperService {
@Header('x-double') double? doubleHeader,
@Header('x-enum') ExampleEnum? enumHeader,
});

@Post(path: 'publish')
@FactoryConverter(request: FormUrlEncodedConverter.requestFactory)
Future<Response<void>> publish(
@Field('review_id') final String reviewId,
@Field() final List<int> negatives,
@Field() final List<int> positives, [
@Field() final String? signature,
]);
}

Request customConvertRequest(Request req) {
Expand Down
Loading

0 comments on commit 50c4b5e

Please sign in to comment.