diff --git a/.github/workflows/publish_dry_run.yml b/.github/workflows/publish_dry_run.yml index 8a23a866..013f2270 100644 --- a/.github/workflows/publish_dry_run.yml +++ b/.github/workflows/publish_dry_run.yml @@ -94,10 +94,7 @@ jobs: id: publish_dry_run if: ${{ env.IS_VERSION_GREATER == 1 }} working-directory: ${{ matrix.package }} - run: | - set -e - yq -i 'del(.dependency_overrides)' pubspec.yaml - dart pub publish --dry-run + run: dart pub publish --dry-run - name: Skip publish (dry run) id: skip_publish_dry_run if: ${{ env.IS_VERSION_GREATER == 0 }} diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index b15c91b9..82da3393 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 7.1.0 + +- Add ability to omit `Response` in service ([#545](https://github.com/lejard-h/chopper/pull/545)) +- Add helper function for fetching errors of specific type ([#543](https://github.com/lejard-h/chopper/pull/543)) +- Improve documentation ([#548](https://github.com/lejard-h/chopper/pull/548)) + ## 7.0.10 - Enable the user to specify non-String type header values by calling `.toString()` on any non-String Dart type. ([#538](https://github.com/lejard-h/chopper/pull/538)) diff --git a/chopper/lib/src/annotations.dart b/chopper/lib/src/annotations.dart index 4fa5e492..7ee1552b 100644 --- a/chopper/lib/src/annotations.dart +++ b/chopper/lib/src/annotations.dart @@ -5,6 +5,7 @@ import 'package:chopper/src/request.dart'; import 'package:chopper/src/response.dart'; import 'package:meta/meta.dart'; +/// {@template ChopperApi} /// Defines a Chopper API. /// /// Must be used on an abstract class that extends the [ChopperService] class. @@ -19,6 +20,7 @@ import 'package:meta/meta.dart'; /// ``` /// /// See [Method] to define an HTTP request +/// {@endtemplate} @immutable final class ChopperApi { /// A part of a URL that every request defined inside a class annotated with [ChopperApi] will be prefixed with. @@ -26,11 +28,13 @@ final class ChopperApi { /// The `baseUrl` can be a top level constant string variable. final String baseUrl; + /// {@macro ChopperApi} const ChopperApi({ this.baseUrl = '', }); } +/// {@template Path} /// Provides a parameter in the url. /// /// Declared as follows inside the path String: @@ -43,6 +47,7 @@ final class ChopperApi { /// @Get(path: '/{param}') /// Future fetch(@Path() String param); /// ``` +/// {@endtemplate} @immutable final class Path { /// Name is used to bind a method parameter to @@ -53,9 +58,11 @@ final class Path { /// ``` final String? name; + /// {@macro Path} const Path([this.name]); } +/// {@template Query} /// Provides the query parameters of a request. /// /// [Query] is used to add query parameters after the request url. @@ -66,6 +73,7 @@ final class Path { /// ``` /// /// See [QueryMap] to pass an [Map] as value +/// {@endtemplate} @immutable final class Query { /// Name is used to bind a method parameter to @@ -76,9 +84,11 @@ final class Query { /// ``` final String? name; + /// {@macro Query} const Query([this.name]); } +/// {@template QueryMap} /// Provides query parameters of a request as [Map]. /// /// ```dart @@ -91,11 +101,14 @@ final class Query { /// fetch({'foo':'bar','list':[1,2]}); /// // something?foo=bar&list=1&list=2 /// ``` +/// {@endtemplate} @immutable final class QueryMap { + /// {@macro QueryMap} const QueryMap(); } +/// {@template Body} /// Declares the Body of [Post], [Put], and [Patch] requests /// /// ```dart @@ -105,11 +118,14 @@ final class QueryMap { /// /// The body can be of any type, but chopper does not automatically convert it to JSON. /// See [Converter] to apply conversion to the body. +/// {@endtemplate} @immutable final class Body { + /// {@macro Body} const Body(); } +/// {@template Header} /// Passes a value to the header of the request. /// /// Use the name of the method parameter or the name specified in the annotation. @@ -118,6 +134,7 @@ final class Body { /// @Get() /// Future fetch(@Header() String foo); /// ``` +/// {@endtemplate} @immutable final class Header { /// Name is used to bind a method parameter to @@ -128,9 +145,11 @@ final class Header { /// ``` final String? name; + /// {@macro Header} const Header([this.name]); } +/// {@template Method} /// Defines an HTTP method. /// /// Must be used inside a [ChopperApi] definition. @@ -148,6 +167,7 @@ final class Header { /// The [Response] type also supports typed parameters like `Future>`. /// However, chopper will not automatically convert the body response to your type. /// A [Converter] needs to be specified for conversion. +/// {@endtemplate} @immutable sealed class Method { /// HTTP method for the request @@ -197,6 +217,7 @@ sealed class Method { /// The above code produces hxxp://path/to/script&foo=foo_var&bar=&baz=baz_var final bool includeNullQueryVars; + /// {@macro Method} const Method( this.method, { this.optionalBody = false, @@ -207,9 +228,12 @@ sealed class Method { }); } +/// {@template Get} /// Defines a method as an HTTP GET request. +/// {@endtemplate} @immutable final class Get extends Method { + /// {@macro Get} const Get({ super.optionalBody = true, super.path, @@ -219,11 +243,14 @@ final class Get extends Method { }) : super(HttpMethod.Get); } +/// {@template Post} /// Defines a method as an HTTP POST request. /// /// Use the [Body] annotation to pass data to send. +/// {@endtemplate} @immutable final class Post extends Method { + /// {@macro Post} const Post({ super.optionalBody, super.path, @@ -233,9 +260,12 @@ final class Post extends Method { }) : super(HttpMethod.Post); } +/// {@template Delete} /// Defines a method as an HTTP DELETE request. +/// {@endtemplate} @immutable final class Delete extends Method { + /// {@macro Delete} const Delete({ super.optionalBody = true, super.path, @@ -245,11 +275,14 @@ final class Delete extends Method { }) : super(HttpMethod.Delete); } +/// {@template Put} /// Defines a method as an HTTP PUT request. /// /// Use the [Body] annotation to pass data to send. +/// {@endtemplate} @immutable final class Put extends Method { + /// {@macro Put} const Put({ super.optionalBody, super.path, @@ -259,10 +292,13 @@ final class Put extends Method { }) : super(HttpMethod.Put); } +/// {@template Patch} /// Defines a method as an HTTP PATCH request. /// Use the [Body] annotation to pass data to send. +/// {@endtemplate} @immutable final class Patch extends Method { + /// {@macro Patch} const Patch({ super.optionalBody, super.path, @@ -272,9 +308,12 @@ final class Patch extends Method { }) : super(HttpMethod.Patch); } +/// {@template Head} /// Defines a method as an HTTP HEAD request. +/// {@endtemplate} @immutable final class Head extends Method { + /// {@macro Head} const Head({ super.optionalBody = true, super.path, @@ -284,8 +323,12 @@ final class Head extends Method { }) : super(HttpMethod.Head); } +/// {@template Options} +/// Defines a method as an HTTP OPTIONS request. +/// {@endtemplate} @immutable final class Options extends Method { + /// {@macro Options} const Options({ super.optionalBody = true, super.path, @@ -302,6 +345,7 @@ typedef ConvertRequest = FutureOr Function(Request request); /// representation to a Dart object. typedef ConvertResponse = FutureOr Function(Response response); +/// {@template FactoryConverter} /// Defines custom [Converter] methods for a single network API endpoint. /// See [ConvertRequest], [ConvertResponse]. /// @@ -331,17 +375,20 @@ typedef ConvertResponse = FutureOr Function(Response response); /// Future> getTodo(@Path("id")); /// } /// ``` +/// {@endtemplate} @immutable final class FactoryConverter { final ConvertRequest? request; final ConvertResponse? response; + /// {@macro FactoryConverter} const FactoryConverter({ this.request, this.response, }); } +/// {@template Field} /// Defines a field for a `x-www-form-urlencoded` request. /// Automatically binds to the name of the method parameter. /// @@ -350,6 +397,7 @@ final class FactoryConverter { /// Future create(@Field() String name); /// ``` /// Will be converted to `{ 'name': value }`. +/// {@endtemplate} @immutable final class Field { /// Name can be use to specify the name of the field @@ -359,20 +407,25 @@ final class Field { /// ``` final String? name; + /// {@macro Field} const Field([this.name]); } +/// {@template FieldMap} /// Provides field parameters of a request as [Map]. /// /// ```dart /// @Post(path: '/something') /// Future fetch(@FieldMap List> query); /// ``` +/// {@endtemplate} @immutable final class FieldMap { + /// {@macro FieldMap} const FieldMap(); } +/// {@template Multipart} /// Defines a multipart request. /// /// ```dart @@ -383,23 +436,29 @@ final class FieldMap { /// /// Use [Part] annotation to send simple data. /// Use [PartFile] annotation to send `File` or `List`. +/// {@endtemplate} @immutable final class Multipart { + /// {@macro Multipart} const Multipart(); } +/// {@template Part} /// Use [Part] to define a part of a [Multipart] request. /// /// All values will be converted to [String] using their [toString] method. /// /// Also accepts `MultipartFile` (from package:http). +/// {@endtemplate} @immutable final class Part { final String? name; + /// {@macro Part} const Part([this.name]); } +/// {@template PartMap} /// Provides part parameters of a request as [PartValue]. /// /// ```dart @@ -407,11 +466,14 @@ final class Part { /// @Multipart /// Future fetch(@PartMap() List query); /// ``` +/// {@endtemplate} @immutable final class PartMap { + /// {@macro PartMap} const PartMap(); } +/// {@template PartFile} /// Use [PartFile] to define a file field for a [Multipart] request. /// /// ```dart @@ -424,13 +486,16 @@ final class PartMap { /// - `List` /// - [String] (path of your file) /// - `MultipartFile` (from package:http) +/// {@endtemplate} @immutable final class PartFile { final String? name; + /// {@macro PartFile} const PartFile([this.name]); } +/// {@template PartFileMap} /// Provides partFile parameters of a request as [PartValueFile]. /// /// ```dart @@ -438,10 +503,72 @@ final class PartFile { /// @Multipart /// Future fetch(@PartFileMap() List query); /// ``` +/// {@endtemplate} @immutable final class PartFileMap { + /// {@macro PartFileMap} const PartFileMap(); } +/// {@macro ChopperApi} +const chopperApi = ChopperApi(); + +/// {@macro Multipart} const multipart = Multipart(); + +/// {@macro Body} const body = Body(); + +/// {@macro Path} +const path = Path(); + +/// {@macro Query} +const query = Query(); + +/// {@macro QueryMap} +const queryMap = QueryMap(); + +/// {@macro Header} +const header = Header(); + +/// {@macro Get} +const get = Get(); + +/// {@macro Post} +const post = Post(); + +/// {@macro Delete} +const delete = Delete(); + +/// {@macro Put} +const put = Put(); + +/// {@macro Patch} +const patch = Patch(); + +/// {@macro Head} +const head = Head(); + +/// {@macro Options} +const options = Options(); + +/// {@macro FactoryConverter} +const factoryConverter = FactoryConverter(); + +/// {@macro Field} +const field = Field(); + +/// {@macro FieldMap} +const fieldMap = FieldMap(); + +/// {@macro Part} +const part = Part(); + +/// {@macro PartMap} +const partMap = PartMap(); + +/// {@macro PartFile} +const partFile = PartFile(); + +/// {@macro PartFileMap} +const partFileMap = PartFileMap(); diff --git a/chopper/lib/src/chopper_http_exception.dart b/chopper/lib/src/chopper_http_exception.dart new file mode 100644 index 00000000..cae57ce2 --- /dev/null +++ b/chopper/lib/src/chopper_http_exception.dart @@ -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}'; + } +} diff --git a/chopper/lib/src/http_logging_interceptor.dart b/chopper/lib/src/http_logging_interceptor.dart index e2453673..bdf37b35 100644 --- a/chopper/lib/src/http_logging_interceptor.dart +++ b/chopper/lib/src/http_logging_interceptor.dart @@ -60,6 +60,7 @@ enum Level { body, } +/// {@template http_logging_interceptor} /// A [RequestInterceptor] and [ResponseInterceptor] implementation which logs /// HTTP request and response data. /// @@ -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, diff --git a/chopper/lib/src/interceptor.dart b/chopper/lib/src/interceptor.dart index 0b11f53f..d76e2920 100644 --- a/chopper/lib/src/interceptor.dart +++ b/chopper/lib/src/interceptor.dart @@ -100,14 +100,17 @@ abstract interface class ErrorConverter { FutureOr convertError(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 headers; + /// {@macro HeadersInterceptor} const HeadersInterceptor(this.headers); @override @@ -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. @@ -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 @@ -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 diff --git a/chopper/lib/src/request.dart b/chopper/lib/src/request.dart index e07dbe3b..68d27331 100644 --- a/chopper/lib/src/request.dart +++ b/chopper/lib/src/request.dart @@ -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; @@ -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, diff --git a/chopper/lib/src/response.dart b/chopper/lib/src/response.dart index 92d1a8bb..b9f8e06d 100644 --- a/chopper/lib/src/response.dart +++ b/chopper/lib/src/response.dart @@ -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 @@ -15,6 +17,7 @@ import 'package:meta/meta.dart'; /// @Get(path: '/items/{id}') /// Future> fetchItem(); /// ``` +/// {@endtemplate} @immutable base class Response with EquatableMixin { /// The [http.BaseResponse] from `package:http` that this [Response] wraps. @@ -30,6 +33,7 @@ base class Response 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. @@ -67,6 +71,29 @@ base class Response 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() { + 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 get props => [ base, diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 9bd3a192..cdc80689 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.0.10 +version: 7.1.0 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper @@ -25,7 +25,7 @@ dev_dependencies: lints: ">=2.1.1 <4.0.0" test: ^1.24.4 transparent_image: ^2.0.1 - chopper_generator: ^7.0.0 + chopper_generator: ^7.1.0 dependency_overrides: chopper_generator: diff --git a/chopper/test/chopper_http_exception_test.dart b/chopper/test/chopper_http_exception_test.dart new file mode 100644 index 00000000..37417e7a --- /dev/null +++ b/chopper/test/chopper_http_exception_test.dart @@ -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', + ); + }); +} diff --git a/chopper/test/ensure_build_test.dart b/chopper/test/ensure_build_test.dart index 0cf64d5f..77fb9a96 100644 --- a/chopper/test/ensure_build_test.dart +++ b/chopper/test/ensure_build_test.dart @@ -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', ], ); diff --git a/chopper/test/fixtures/error_fixtures.dart b/chopper/test/fixtures/error_fixtures.dart new file mode 100644 index 00000000..2c728333 --- /dev/null +++ b/chopper/test/fixtures/error_fixtures.dart @@ -0,0 +1,3 @@ +class FooErrorType { + const FooErrorType(); +} diff --git a/chopper/test/response_test.dart b/chopper/test/response_test.dart new file mode 100644 index 00000000..201d3cb4 --- /dev/null +++ b/chopper/test/response_test.dart @@ -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(); + + 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(); + + 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(); + + 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(); + + expect(result, isNotNull); + expect(result, isA()); + }); + }); + + 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())); + }); + + 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())); + }); + + 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())); + }); + + test( + 'Response is successful and has no body, [bodyOrThrow throws ChopperHttpException]', + () { + final base = http.Response('Foobar', 200); + final Response response = Response(base, null); + + expect(() => response.bodyOrThrow, throwsA(isA())); + }); + + 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 response = Response(base, ''); + + expect(() => response.bodyOrThrow, returnsNormally); + }); + }); +} diff --git a/chopper/test/test_service.chopper.dart b/chopper/test/test_service.chopper.dart index 0db60af9..c6335821 100644 --- a/chopper/test/test_service.chopper.dart +++ b/chopper/test/test_service.chopper.dart @@ -513,14 +513,14 @@ final class _$HttpTestService extends HttpTestService { } @override - Future fullUrl() { + Future> fullUrl() { final Uri $url = Uri.parse('https://test.com'); final Request $request = Request( 'GET', $url, client.baseUrl, ); - return client.send($request); + return client.send($request); } @override @@ -675,4 +675,30 @@ final class _$HttpTestService extends HttpTestService { ); return client.send($request); } + + @override + Future> publish( + String reviewId, + List negatives, + List positives, [ + String? signature, + ]) { + final Uri $url = Uri.parse('/test/publish'); + final $body = { + 'review_id': reviewId, + 'negatives': negatives, + 'positives': positives, + 'signature': signature, + }; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send( + $request, + requestConverter: FormUrlEncodedConverter.requestFactory, + ); + } } diff --git a/chopper/test/test_service.dart b/chopper/test/test_service.dart index 1ed74368..6ae6cb4e 100644 --- a/chopper/test/test_service.dart +++ b/chopper/test/test_service.dart @@ -148,7 +148,7 @@ abstract class HttpTestService extends ChopperService { }); @Get(path: 'https://test.com') - Future fullUrl(); + Future fullUrl(); @Get(path: '/list/string') Future>> listString(); @@ -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> publish( + @Field('review_id') final String reviewId, + @Field() final List negatives, + @Field() final List positives, [ + @Field() final String? signature, + ]); } Request customConvertRequest(Request req) { diff --git a/chopper/test/test_service_variable.chopper.dart b/chopper/test/test_service_variable.chopper.dart index e3611022..9c69ffa0 100644 --- a/chopper/test/test_service_variable.chopper.dart +++ b/chopper/test/test_service_variable.chopper.dart @@ -513,14 +513,14 @@ final class _$HttpTestServiceVariable extends HttpTestServiceVariable { } @override - Future fullUrl() { + Future> fullUrl() { final Uri $url = Uri.parse('https://test.com'); final Request $request = Request( 'GET', $url, client.baseUrl, ); - return client.send($request); + return client.send($request); } @override diff --git a/chopper/test/test_service_variable.dart b/chopper/test/test_service_variable.dart index 81532976..251b48cb 100644 --- a/chopper/test/test_service_variable.dart +++ b/chopper/test/test_service_variable.dart @@ -148,7 +148,7 @@ abstract class HttpTestServiceVariable extends ChopperService { }); @Get(path: 'https://test.com') - Future fullUrl(); + Future fullUrl(); @Get(path: '/list/string') Future>> listString(); diff --git a/chopper/test/test_without_response_service.chopper.dart b/chopper/test/test_without_response_service.chopper.dart new file mode 100644 index 00000000..c7b8462a --- /dev/null +++ b/chopper/test/test_without_response_service.chopper.dart @@ -0,0 +1,705 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'test_without_response_service.dart'; + +// ************************************************************************** +// ChopperGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: type=lint +final class _$HttpTestService extends HttpTestService { + _$HttpTestService([ChopperClient? client]) { + if (client == null) return; + this.client = client; + } + + @override + final Type definitionType = HttpTestService; + + @override + Future getTest( + String id, { + required String dynamicHeader, + }) async { + final Uri $url = Uri.parse('/test/get/${id}'); + final Map $headers = { + 'test': dynamicHeader, + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + headers: $headers, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future headTest() async { + final Uri $url = Uri.parse('/test/head'); + final Request $request = Request( + 'HEAD', + $url, + client.baseUrl, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future optionsTest() async { + final Uri $url = Uri.parse('/test/options'); + final Request $request = Request( + 'OPTIONS', + $url, + client.baseUrl, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future>> getStreamTest() async { + final Uri $url = Uri.parse('/test/get'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + final Response $response = + await client.send>, int>($request); + return $response.bodyOrThrow; + } + + @override + Future getAll() async { + final Uri $url = Uri.parse('/test'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getAllWithTrailingSlash() async { + final Uri $url = Uri.parse('/test/'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getQueryTest({ + String name = '', + int? number, + int? def = 42, + }) async { + final Uri $url = Uri.parse('/test/query'); + final Map $params = { + 'name': name, + 'int': number, + 'default_value': def, + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getQueryMapTest(Map query) async { + final Uri $url = Uri.parse('/test/query_map'); + final Map $params = query; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getQueryMapTest2( + Map query, { + bool? test, + }) async { + final Uri $url = Uri.parse('/test/query_map'); + final Map $params = {'test': test}; + $params.addAll(query); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getQueryMapTest3({ + String name = '', + int? number, + Map filters = const {}, + }) async { + final Uri $url = Uri.parse('/test/query_map'); + final Map $params = { + 'name': name, + 'number': number, + }; + $params.addAll(filters); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getQueryMapTest4({ + String name = '', + int? number, + Map? filters, + }) async { + final Uri $url = Uri.parse('/test/query_map'); + final Map $params = { + 'name': name, + 'number': number, + }; + $params.addAll(filters ?? const {}); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getQueryMapTest5({Map? filters}) async { + final Uri $url = Uri.parse('/test/query_map'); + final Map $params = filters ?? const {}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getBody(dynamic body) async { + final Uri $url = Uri.parse('/test/get_body'); + final $body = body; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postTest(String data) async { + final Uri $url = Uri.parse('/test/post'); + final $body = data; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postStreamTest(Stream> byteStream) async { + final Uri $url = Uri.parse('/test/post'); + final $body = byteStream; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future putTest( + String test, + String data, + ) async { + final Uri $url = Uri.parse('/test/put/${test}'); + final $body = data; + final Request $request = Request( + 'PUT', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future deleteTest(String id) async { + final Uri $url = Uri.parse('/test/delete/${id}'); + final Map $headers = { + 'foo': 'bar', + }; + final Request $request = Request( + 'DELETE', + $url, + client.baseUrl, + headers: $headers, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future patchTest( + String id, + String data, + ) async { + final Uri $url = Uri.parse('/test/patch/${id}'); + final $body = data; + final Request $request = Request( + 'PATCH', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future mapTest(Map map) async { + final Uri $url = Uri.parse('/test/map'); + final $body = map; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postForm(Map fields) async { + final Uri $url = Uri.parse('/test/form/body'); + final $body = fields; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send( + $request, + requestConverter: convertForm, + ); + return $response.bodyOrThrow; + } + + @override + Future postFormUsingHeaders(Map fields) async { + final Uri $url = Uri.parse('/test/form/body'); + final Map $headers = { + 'content-type': 'application/x-www-form-urlencoded', + }; + final $body = fields; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + headers: $headers, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postFormFields( + String foo, + int bar, + ) async { + final Uri $url = Uri.parse('/test/form/body/fields'); + final $body = { + 'foo': foo, + 'bar': bar, + }; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send( + $request, + requestConverter: convertForm, + ); + return $response.bodyOrThrow; + } + + @override + Future forceJsonTest(Map map) async { + final Uri $url = Uri.parse('/test/map/json'); + final $body = map; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send( + $request, + requestConverter: customConvertRequest, + responseConverter: customConvertResponse, + ); + return $response.bodyOrThrow; + } + + @override + Future postResources( + Map a, + Map b, + ) async { + final Uri $url = Uri.parse('/test/multi'); + final List $parts = [ + PartValue>( + '1', + a, + ), + PartValue>( + '2', + b, + ), + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postFile(List bytes) async { + final Uri $url = Uri.parse('/test/file'); + final List $parts = [ + PartValueFile>( + 'file', + bytes, + ) + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postImage(List imageData) async { + final Uri $url = Uri.parse('/test/image'); + final List $parts = [ + PartValueFile>( + 'image', + imageData, + ) + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postMultipartFile( + MultipartFile file, { + String? id, + }) async { + final Uri $url = Uri.parse('/test/file'); + final List $parts = [ + PartValue( + 'id', + id, + ), + PartValueFile( + 'file', + file, + ), + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postListFiles(List files) async { + final Uri $url = Uri.parse('/test/files'); + final List $parts = [ + PartValueFile>( + 'files', + files, + ) + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postMultipartList({ + required List ints, + required List doubles, + required List nums, + required List strings, + }) async { + final Uri $url = Uri.parse('/test/multipart_list'); + final List $parts = [ + PartValue>( + 'ints', + ints, + ), + PartValue>( + 'doubles', + doubles, + ), + PartValue>( + 'nums', + nums, + ), + PartValue>( + 'strings', + strings, + ), + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future fullUrl() async { + final Uri $url = Uri.parse('https://test.com'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future> listString() async { + final Uri $url = Uri.parse('/test/list/string'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + final Response $response = + await client.send, String>($request); + return $response.bodyOrThrow; + } + + @override + Future noBody() async { + final Uri $url = Uri.parse('/test/no-body'); + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingQueryParamIncludeNullQueryVars({ + String? foo, + String? bar, + String? baz, + }) async { + final Uri $url = Uri.parse('/test/query_param_include_null_query_vars'); + final Map $params = { + 'foo': foo, + 'bar': bar, + 'baz': baz, + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + includeNullQueryVars: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingListQueryParam(List value) async { + final Uri $url = Uri.parse('/test/list_query_param'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingListQueryParamWithBrackets(List value) async { + final Uri $url = Uri.parse('/test/list_query_param_with_brackets'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingMapQueryParam(Map value) async { + final Uri $url = Uri.parse('/test/map_query_param'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingMapQueryParamIncludeNullQueryVars( + Map value) async { + final Uri $url = Uri.parse('/test/map_query_param_include_null_query_vars'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + includeNullQueryVars: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingMapQueryParamWithBrackets( + Map value) async { + final Uri $url = Uri.parse('/test/map_query_param_with_brackets'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getHeaders({ + required String stringHeader, + bool? boolHeader, + int? intHeader, + double? doubleHeader, + ExampleEnum? enumHeader, + }) async { + final Uri $url = Uri.parse('/test/headers'); + final Map $headers = { + 'x-string': stringHeader, + if (boolHeader != null) 'x-boolean': boolHeader.toString(), + if (intHeader != null) 'x-int': intHeader.toString(), + if (doubleHeader != null) 'x-double': doubleHeader.toString(), + if (enumHeader != null) 'x-enum': enumHeader.toString(), + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + headers: $headers, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } +} diff --git a/chopper/test/test_without_response_service.dart b/chopper/test/test_without_response_service.dart new file mode 100644 index 00000000..d8a95417 --- /dev/null +++ b/chopper/test/test_without_response_service.dart @@ -0,0 +1,236 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:chopper/chopper.dart'; +import 'package:http/http.dart' show MultipartFile; + +part 'test_without_response_service.chopper.dart'; + +@ChopperApi(baseUrl: '/test') +abstract class HttpTestService extends ChopperService { + static HttpTestService create([ChopperClient? client]) => + _$HttpTestService(client); + + @Get(path: 'get/{id}') + Future getTest( + @Path() String id, { + @Header('test') required String dynamicHeader, + }); + + @Head(path: 'head') + Future headTest(); + + @Options(path: 'options') + Future optionsTest(); + + @Get(path: 'get') + Future>> getStreamTest(); + + @Get(path: '') + Future getAll(); + + @Get(path: '/') + Future getAllWithTrailingSlash(); + + @Get(path: 'query') + Future getQueryTest({ + @Query('name') String name = '', + @Query('int') int? number, + @Query('default_value') int? def = 42, + }); + + @Get(path: 'query_map') + Future getQueryMapTest(@QueryMap() Map query); + + @Get(path: 'query_map') + Future getQueryMapTest2( + @QueryMap() Map query, { + @Query('test') bool? test, + }); + + @Get(path: 'query_map') + Future getQueryMapTest3({ + @Query('name') String name = '', + @Query('number') int? number, + @QueryMap() Map filters = const {}, + }); + + @Get(path: 'query_map') + Future getQueryMapTest4({ + @Query('name') String name = '', + @Query('number') int? number, + @QueryMap() Map? filters, + }); + + @Get(path: 'query_map') + Future getQueryMapTest5({ + @QueryMap() Map? filters, + }); + + @Get(path: 'get_body') + Future getBody(@Body() dynamic body); + + @Post(path: 'post') + Future postTest(@Body() String data); + + @Post(path: 'post') + Future postStreamTest(@Body() Stream> byteStream); + + @Put(path: 'put/{id}') + Future putTest(@Path('id') String test, @Body() String data); + + @Delete(path: 'delete/{id}', headers: {'foo': 'bar'}) + Future deleteTest(@Path() String id); + + @Patch(path: 'patch/{id}') + Future patchTest(@Path() String id, @Body() String data); + + @Post(path: 'map') + Future mapTest(@Body() Map map); + + @FactoryConverter(request: convertForm) + @Post(path: 'form/body') + Future postForm(@Body() Map fields); + + @Post(path: 'form/body', headers: {contentTypeKey: formEncodedHeaders}) + Future postFormUsingHeaders(@Body() Map fields); + + @FactoryConverter(request: convertForm) + @Post(path: 'form/body/fields') + Future postFormFields(@Field() String foo, @Field() int bar); + + @Post(path: 'map/json') + @FactoryConverter( + request: customConvertRequest, + response: customConvertResponse, + ) + Future forceJsonTest(@Body() Map map); + + @Post(path: 'multi') + @multipart + Future postResources( + @Part('1') Map a, + @Part('2') Map b, + ); + + @Post(path: 'file') + @multipart + Future postFile( + @PartFile('file') List bytes, + ); + + @Post(path: 'image') + @multipart + Future postImage( + @PartFile('image') List imageData, + ); + + @Post(path: 'file') + @multipart + Future postMultipartFile( + @PartFile() MultipartFile file, { + @Part() String? id, + }); + + @Post(path: 'files') + @multipart + Future postListFiles(@PartFile() List files); + + @Post(path: 'multipart_list') + @multipart + Future postMultipartList({ + @Part('ints') required List ints, + @Part('doubles') required List doubles, + @Part('nums') required List nums, + @Part('strings') required List strings, + }); + + @Get(path: 'https://test.com') + Future fullUrl(); + + @Get(path: '/list/string') + Future> listString(); + + @Post(path: 'no-body') + Future noBody(); + + @Get(path: '/query_param_include_null_query_vars', includeNullQueryVars: true) + Future getUsingQueryParamIncludeNullQueryVars({ + @Query('foo') String? foo, + @Query('bar') String? bar, + @Query('baz') String? baz, + }); + + @Get(path: '/list_query_param') + Future getUsingListQueryParam( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_brackets', useBrackets: true) + Future getUsingListQueryParamWithBrackets( + @Query('value') List value, + ); + + @Get(path: '/map_query_param') + Future getUsingMapQueryParam( + @Query('value') Map value, + ); + + @Get( + path: '/map_query_param_include_null_query_vars', + includeNullQueryVars: true, + ) + Future getUsingMapQueryParamIncludeNullQueryVars( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_brackets', useBrackets: true) + Future getUsingMapQueryParamWithBrackets( + @Query('value') Map value, + ); + + @Get(path: 'headers') + Future getHeaders({ + @Header('x-string') required String stringHeader, + @Header('x-boolean') bool? boolHeader, + @Header('x-int') int? intHeader, + @Header('x-double') double? doubleHeader, + @Header('x-enum') ExampleEnum? enumHeader, + }); +} + +Request customConvertRequest(Request req) { + final r = JsonConverter().convertRequest(req); + + return applyHeader(r, 'customConverter', 'true'); +} + +Response customConvertResponse(Response res) => + res.copyWith(body: json.decode(res.body)); + +Request convertForm(Request req) { + req = applyHeader(req, contentTypeKey, formEncodedHeaders); + + if (req.body is Map) { + final body = {}; + + req.body.forEach((key, val) { + if (val != null) { + body[key.toString()] = val.toString(); + } + }); + + req = req.copyWith(body: body); + } + + return req; +} + +enum ExampleEnum { + foo, + bar, + baz; + + @override + String toString() => name; +} diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 2d30f9e0..6a1cd984 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 7.1.0 + +- Add ability to omit `Response` in service ([#545](https://github.com/lejard-h/chopper/pull/545)) +- Fix `FactoryConverter` regression introduced in v7.0.7 ([#549](https://github.com/lejard-h/chopper/pull/549)) + ## 7.0.7 - Enable the user to specify non-String type header values by calling `.toString()` on any non-String Dart type. ([#538](https://github.com/lejard-h/chopper/pull/538)) diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 915b49f8..f35a341a 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -179,18 +179,34 @@ final class ChopperGenerator baseUrl, baseUrlVariableElement, ); - final DartType? responseType = _getResponseType(m.returnType); + + // Check if Response is present in the return type + final bool isResponseObject = _isResponse(m.returnType); + final DartType? responseType = + _getResponseType(m.returnType, isResponseObject); final DartType? responseInnerType = _getResponseInnerType(m.returnType) ?? responseType; + // Set Response with generic types + final Reference responseTypeReference = refer( + responseType?.getDisplayString(withNullability: false) ?? + responseType?.getDisplayString(withNullability: false) ?? + 'dynamic'); + // Set the return type + final returnType = isResponseObject + ? refer(m.returnType.getDisplayString(withNullability: false)) + : TypeReference( + (b) => b + ..symbol = 'Future' + ..types.add(responseTypeReference), + ); + return Method((MethodBuilder methodBuilder) { methodBuilder ..annotations.add(refer('override')) ..name = m.displayName // We don't support returning null Type - ..returns = refer( - m.returnType.getDisplayString(withNullability: false), - ) + ..returns = returnType // And null Typed parameters ..types.addAll( m.typeParameters.map( @@ -211,6 +227,12 @@ final class ChopperGenerator m.parameters.where((p) => p.isNamed).map(Utils.buildNamedParam), ); + // Make method async if Response is omitted. + // We need the await the response in order to return the body. + if (!isResponseObject) { + methodBuilder.modifier = MethodModifier.async; + } + final List blocks = [ declareFinal(Vars.url.toString(), type: refer('Uri')) .assign(url) @@ -410,18 +432,36 @@ final class ChopperGenerator ]); } - blocks.add( - refer(Vars.client.toString()) - .property('send') - .call( - [refer(Vars.request.toString())], - namedArguments, - typeArguments, - ) - .returned - .statement, + final returnStatement = + refer(Vars.client.toString()).property('send').call( + [refer(Vars.request.toString())], + namedArguments, + typeArguments, ); + if (isResponseObject) { + // Return the response object directly from chopper.send + blocks.add(returnStatement.returned.statement); + } else { + // Await the response object from chopper.send + blocks.add( + // generic types are not passed in the code_builder at the moment. + declareFinal( + Vars.response.toString(), + type: TypeReference( + (b) => b + ..symbol = 'Response' + ..types.add(responseTypeReference), + ), + ).assign(returnStatement.awaited).statement, + ); + // Return the body of the response object + blocks.add(refer(Vars.response.toString()) + .property('bodyOrThrow') + .returned + .statement); + } + methodBuilder.body = Block.of(blocks); }); } @@ -430,9 +470,7 @@ final class ChopperGenerator // ignore: deprecated_member_use function.enclosingElement is ClassElement // ignore: deprecated_member_use - ? refer(function.enclosingElement!.name!) - .property(function.name!) - .toString() + ? '${function.enclosingElement!.name}.${function.name}' : function.name!; static Map _getAnnotation( @@ -508,8 +546,15 @@ final class ChopperGenerator ? type.typeArguments.first : null; - static DartType? _getResponseType(DartType type) => - _genericOf(_genericOf(type)); + static bool _isResponse(DartType type) { + final DartType? responseType = _genericOf(type); + if (responseType == null) return false; + + return _typeChecker(chopper.Response).isExactlyType(responseType); + } + + static DartType? _getResponseType(DartType type, bool isResponseObject) => + isResponseObject ? _genericOf(_genericOf(type)) : _genericOf(type); static DartType? _getResponseInnerType(DartType type) { final DartType? generic = _genericOf(type); diff --git a/chopper_generator/lib/src/vars.dart b/chopper_generator/lib/src/vars.dart index cb7c5fdd..77562cec 100644 --- a/chopper_generator/lib/src/vars.dart +++ b/chopper_generator/lib/src/vars.dart @@ -1,5 +1,6 @@ enum Vars { client('client'), + response(r'$response'), baseUrl('baseUrl'), parameters(r'$params'), headers(r'$headers'), diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 6ee77f1c..ade9709e 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.0.7 +version: 7.1.0 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper diff --git a/chopper_generator/test/ensure_build_test.dart b/chopper_generator/test/ensure_build_test.dart index c708d8d1..84c677fc 100644 --- a/chopper_generator/test/ensure_build_test.dart +++ b/chopper_generator/test/ensure_build_test.dart @@ -6,18 +6,13 @@ import 'package:test/test.dart'; void main() { test( 'ensure_build', - () { - expectBuildClean( + () async { + await expectBuildClean( packageRelativeDirectory: 'chopper_generator', gitDiffPathArguments: [ 'test/test_service.chopper.dart', - ], - ); - - expectBuildClean( - packageRelativeDirectory: 'chopper_generator', - gitDiffPathArguments: [ 'test/test_service_variable.chopper.dart', + 'test/test_without_response_service.chopper.dart', ], ); }, diff --git a/chopper_generator/test/test_service.chopper.dart b/chopper_generator/test/test_service.chopper.dart index b94c2e9b..262dfb11 100644 --- a/chopper_generator/test/test_service.chopper.dart +++ b/chopper_generator/test/test_service.chopper.dart @@ -513,14 +513,14 @@ final class _$HttpTestService extends HttpTestService { } @override - Future fullUrl() { + Future> fullUrl() { final Uri $url = Uri.parse('https://test.com'); final Request $request = Request( 'GET', $url, client.baseUrl, ); - return client.send($request); + return client.send($request); } @override @@ -662,4 +662,30 @@ final class _$HttpTestService extends HttpTestService { ); return client.send($request); } + + @override + Future> publish( + String reviewId, + List negatives, + List positives, [ + String? signature, + ]) { + final Uri $url = Uri.parse('/test/publish'); + final $body = { + 'review_id': reviewId, + 'negatives': negatives, + 'positives': positives, + 'signature': signature, + }; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send( + $request, + requestConverter: FormUrlEncodedConverter.requestFactory, + ); + } } diff --git a/chopper_generator/test/test_service.dart b/chopper_generator/test/test_service.dart index 30d6a451..1fae94fc 100644 --- a/chopper_generator/test/test_service.dart +++ b/chopper_generator/test/test_service.dart @@ -146,7 +146,7 @@ abstract class HttpTestService extends ChopperService { }); @Get(path: 'https://test.com') - Future fullUrl(); + Future fullUrl(); @Get(path: '/list/string') Future>> listString(); @@ -197,6 +197,15 @@ abstract class HttpTestService extends ChopperService { @Header('x-double') double? doubleHeader, @Header('x-enum') ExampleEnum? enumHeader, }); + + @Post(path: 'publish') + @FactoryConverter(request: FormUrlEncodedConverter.requestFactory) + Future> publish( + @Field('review_id') final String reviewId, + @Field() final List negatives, + @Field() final List positives, [ + @Field() final String? signature, + ]); } Request customConvertRequest(Request req) { diff --git a/chopper_generator/test/test_service_variable.chopper.dart b/chopper_generator/test/test_service_variable.chopper.dart index e3611022..9c69ffa0 100644 --- a/chopper_generator/test/test_service_variable.chopper.dart +++ b/chopper_generator/test/test_service_variable.chopper.dart @@ -513,14 +513,14 @@ final class _$HttpTestServiceVariable extends HttpTestServiceVariable { } @override - Future fullUrl() { + Future> fullUrl() { final Uri $url = Uri.parse('https://test.com'); final Request $request = Request( 'GET', $url, client.baseUrl, ); - return client.send($request); + return client.send($request); } @override diff --git a/chopper_generator/test/test_service_variable.dart b/chopper_generator/test/test_service_variable.dart index 81532976..251b48cb 100644 --- a/chopper_generator/test/test_service_variable.dart +++ b/chopper_generator/test/test_service_variable.dart @@ -148,7 +148,7 @@ abstract class HttpTestServiceVariable extends ChopperService { }); @Get(path: 'https://test.com') - Future fullUrl(); + Future fullUrl(); @Get(path: '/list/string') Future>> listString(); diff --git a/chopper_generator/test/test_without_response_service.chopper.dart b/chopper_generator/test/test_without_response_service.chopper.dart new file mode 100644 index 00000000..c7b8462a --- /dev/null +++ b/chopper_generator/test/test_without_response_service.chopper.dart @@ -0,0 +1,705 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'test_without_response_service.dart'; + +// ************************************************************************** +// ChopperGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: type=lint +final class _$HttpTestService extends HttpTestService { + _$HttpTestService([ChopperClient? client]) { + if (client == null) return; + this.client = client; + } + + @override + final Type definitionType = HttpTestService; + + @override + Future getTest( + String id, { + required String dynamicHeader, + }) async { + final Uri $url = Uri.parse('/test/get/${id}'); + final Map $headers = { + 'test': dynamicHeader, + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + headers: $headers, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future headTest() async { + final Uri $url = Uri.parse('/test/head'); + final Request $request = Request( + 'HEAD', + $url, + client.baseUrl, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future optionsTest() async { + final Uri $url = Uri.parse('/test/options'); + final Request $request = Request( + 'OPTIONS', + $url, + client.baseUrl, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future>> getStreamTest() async { + final Uri $url = Uri.parse('/test/get'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + final Response $response = + await client.send>, int>($request); + return $response.bodyOrThrow; + } + + @override + Future getAll() async { + final Uri $url = Uri.parse('/test'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getAllWithTrailingSlash() async { + final Uri $url = Uri.parse('/test/'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getQueryTest({ + String name = '', + int? number, + int? def = 42, + }) async { + final Uri $url = Uri.parse('/test/query'); + final Map $params = { + 'name': name, + 'int': number, + 'default_value': def, + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getQueryMapTest(Map query) async { + final Uri $url = Uri.parse('/test/query_map'); + final Map $params = query; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getQueryMapTest2( + Map query, { + bool? test, + }) async { + final Uri $url = Uri.parse('/test/query_map'); + final Map $params = {'test': test}; + $params.addAll(query); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getQueryMapTest3({ + String name = '', + int? number, + Map filters = const {}, + }) async { + final Uri $url = Uri.parse('/test/query_map'); + final Map $params = { + 'name': name, + 'number': number, + }; + $params.addAll(filters); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getQueryMapTest4({ + String name = '', + int? number, + Map? filters, + }) async { + final Uri $url = Uri.parse('/test/query_map'); + final Map $params = { + 'name': name, + 'number': number, + }; + $params.addAll(filters ?? const {}); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getQueryMapTest5({Map? filters}) async { + final Uri $url = Uri.parse('/test/query_map'); + final Map $params = filters ?? const {}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getBody(dynamic body) async { + final Uri $url = Uri.parse('/test/get_body'); + final $body = body; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postTest(String data) async { + final Uri $url = Uri.parse('/test/post'); + final $body = data; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postStreamTest(Stream> byteStream) async { + final Uri $url = Uri.parse('/test/post'); + final $body = byteStream; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future putTest( + String test, + String data, + ) async { + final Uri $url = Uri.parse('/test/put/${test}'); + final $body = data; + final Request $request = Request( + 'PUT', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future deleteTest(String id) async { + final Uri $url = Uri.parse('/test/delete/${id}'); + final Map $headers = { + 'foo': 'bar', + }; + final Request $request = Request( + 'DELETE', + $url, + client.baseUrl, + headers: $headers, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future patchTest( + String id, + String data, + ) async { + final Uri $url = Uri.parse('/test/patch/${id}'); + final $body = data; + final Request $request = Request( + 'PATCH', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future mapTest(Map map) async { + final Uri $url = Uri.parse('/test/map'); + final $body = map; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postForm(Map fields) async { + final Uri $url = Uri.parse('/test/form/body'); + final $body = fields; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send( + $request, + requestConverter: convertForm, + ); + return $response.bodyOrThrow; + } + + @override + Future postFormUsingHeaders(Map fields) async { + final Uri $url = Uri.parse('/test/form/body'); + final Map $headers = { + 'content-type': 'application/x-www-form-urlencoded', + }; + final $body = fields; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + headers: $headers, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postFormFields( + String foo, + int bar, + ) async { + final Uri $url = Uri.parse('/test/form/body/fields'); + final $body = { + 'foo': foo, + 'bar': bar, + }; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send( + $request, + requestConverter: convertForm, + ); + return $response.bodyOrThrow; + } + + @override + Future forceJsonTest(Map map) async { + final Uri $url = Uri.parse('/test/map/json'); + final $body = map; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send( + $request, + requestConverter: customConvertRequest, + responseConverter: customConvertResponse, + ); + return $response.bodyOrThrow; + } + + @override + Future postResources( + Map a, + Map b, + ) async { + final Uri $url = Uri.parse('/test/multi'); + final List $parts = [ + PartValue>( + '1', + a, + ), + PartValue>( + '2', + b, + ), + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postFile(List bytes) async { + final Uri $url = Uri.parse('/test/file'); + final List $parts = [ + PartValueFile>( + 'file', + bytes, + ) + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postImage(List imageData) async { + final Uri $url = Uri.parse('/test/image'); + final List $parts = [ + PartValueFile>( + 'image', + imageData, + ) + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postMultipartFile( + MultipartFile file, { + String? id, + }) async { + final Uri $url = Uri.parse('/test/file'); + final List $parts = [ + PartValue( + 'id', + id, + ), + PartValueFile( + 'file', + file, + ), + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postListFiles(List files) async { + final Uri $url = Uri.parse('/test/files'); + final List $parts = [ + PartValueFile>( + 'files', + files, + ) + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postMultipartList({ + required List ints, + required List doubles, + required List nums, + required List strings, + }) async { + final Uri $url = Uri.parse('/test/multipart_list'); + final List $parts = [ + PartValue>( + 'ints', + ints, + ), + PartValue>( + 'doubles', + doubles, + ), + PartValue>( + 'nums', + nums, + ), + PartValue>( + 'strings', + strings, + ), + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future fullUrl() async { + final Uri $url = Uri.parse('https://test.com'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future> listString() async { + final Uri $url = Uri.parse('/test/list/string'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + final Response $response = + await client.send, String>($request); + return $response.bodyOrThrow; + } + + @override + Future noBody() async { + final Uri $url = Uri.parse('/test/no-body'); + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingQueryParamIncludeNullQueryVars({ + String? foo, + String? bar, + String? baz, + }) async { + final Uri $url = Uri.parse('/test/query_param_include_null_query_vars'); + final Map $params = { + 'foo': foo, + 'bar': bar, + 'baz': baz, + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + includeNullQueryVars: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingListQueryParam(List value) async { + final Uri $url = Uri.parse('/test/list_query_param'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingListQueryParamWithBrackets(List value) async { + final Uri $url = Uri.parse('/test/list_query_param_with_brackets'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingMapQueryParam(Map value) async { + final Uri $url = Uri.parse('/test/map_query_param'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingMapQueryParamIncludeNullQueryVars( + Map value) async { + final Uri $url = Uri.parse('/test/map_query_param_include_null_query_vars'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + includeNullQueryVars: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingMapQueryParamWithBrackets( + Map value) async { + final Uri $url = Uri.parse('/test/map_query_param_with_brackets'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getHeaders({ + required String stringHeader, + bool? boolHeader, + int? intHeader, + double? doubleHeader, + ExampleEnum? enumHeader, + }) async { + final Uri $url = Uri.parse('/test/headers'); + final Map $headers = { + 'x-string': stringHeader, + if (boolHeader != null) 'x-boolean': boolHeader.toString(), + if (intHeader != null) 'x-int': intHeader.toString(), + if (doubleHeader != null) 'x-double': doubleHeader.toString(), + if (enumHeader != null) 'x-enum': enumHeader.toString(), + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + headers: $headers, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } +} diff --git a/chopper_generator/test/test_without_response_service.dart b/chopper_generator/test/test_without_response_service.dart new file mode 100644 index 00000000..d8a95417 --- /dev/null +++ b/chopper_generator/test/test_without_response_service.dart @@ -0,0 +1,236 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:chopper/chopper.dart'; +import 'package:http/http.dart' show MultipartFile; + +part 'test_without_response_service.chopper.dart'; + +@ChopperApi(baseUrl: '/test') +abstract class HttpTestService extends ChopperService { + static HttpTestService create([ChopperClient? client]) => + _$HttpTestService(client); + + @Get(path: 'get/{id}') + Future getTest( + @Path() String id, { + @Header('test') required String dynamicHeader, + }); + + @Head(path: 'head') + Future headTest(); + + @Options(path: 'options') + Future optionsTest(); + + @Get(path: 'get') + Future>> getStreamTest(); + + @Get(path: '') + Future getAll(); + + @Get(path: '/') + Future getAllWithTrailingSlash(); + + @Get(path: 'query') + Future getQueryTest({ + @Query('name') String name = '', + @Query('int') int? number, + @Query('default_value') int? def = 42, + }); + + @Get(path: 'query_map') + Future getQueryMapTest(@QueryMap() Map query); + + @Get(path: 'query_map') + Future getQueryMapTest2( + @QueryMap() Map query, { + @Query('test') bool? test, + }); + + @Get(path: 'query_map') + Future getQueryMapTest3({ + @Query('name') String name = '', + @Query('number') int? number, + @QueryMap() Map filters = const {}, + }); + + @Get(path: 'query_map') + Future getQueryMapTest4({ + @Query('name') String name = '', + @Query('number') int? number, + @QueryMap() Map? filters, + }); + + @Get(path: 'query_map') + Future getQueryMapTest5({ + @QueryMap() Map? filters, + }); + + @Get(path: 'get_body') + Future getBody(@Body() dynamic body); + + @Post(path: 'post') + Future postTest(@Body() String data); + + @Post(path: 'post') + Future postStreamTest(@Body() Stream> byteStream); + + @Put(path: 'put/{id}') + Future putTest(@Path('id') String test, @Body() String data); + + @Delete(path: 'delete/{id}', headers: {'foo': 'bar'}) + Future deleteTest(@Path() String id); + + @Patch(path: 'patch/{id}') + Future patchTest(@Path() String id, @Body() String data); + + @Post(path: 'map') + Future mapTest(@Body() Map map); + + @FactoryConverter(request: convertForm) + @Post(path: 'form/body') + Future postForm(@Body() Map fields); + + @Post(path: 'form/body', headers: {contentTypeKey: formEncodedHeaders}) + Future postFormUsingHeaders(@Body() Map fields); + + @FactoryConverter(request: convertForm) + @Post(path: 'form/body/fields') + Future postFormFields(@Field() String foo, @Field() int bar); + + @Post(path: 'map/json') + @FactoryConverter( + request: customConvertRequest, + response: customConvertResponse, + ) + Future forceJsonTest(@Body() Map map); + + @Post(path: 'multi') + @multipart + Future postResources( + @Part('1') Map a, + @Part('2') Map b, + ); + + @Post(path: 'file') + @multipart + Future postFile( + @PartFile('file') List bytes, + ); + + @Post(path: 'image') + @multipart + Future postImage( + @PartFile('image') List imageData, + ); + + @Post(path: 'file') + @multipart + Future postMultipartFile( + @PartFile() MultipartFile file, { + @Part() String? id, + }); + + @Post(path: 'files') + @multipart + Future postListFiles(@PartFile() List files); + + @Post(path: 'multipart_list') + @multipart + Future postMultipartList({ + @Part('ints') required List ints, + @Part('doubles') required List doubles, + @Part('nums') required List nums, + @Part('strings') required List strings, + }); + + @Get(path: 'https://test.com') + Future fullUrl(); + + @Get(path: '/list/string') + Future> listString(); + + @Post(path: 'no-body') + Future noBody(); + + @Get(path: '/query_param_include_null_query_vars', includeNullQueryVars: true) + Future getUsingQueryParamIncludeNullQueryVars({ + @Query('foo') String? foo, + @Query('bar') String? bar, + @Query('baz') String? baz, + }); + + @Get(path: '/list_query_param') + Future getUsingListQueryParam( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_brackets', useBrackets: true) + Future getUsingListQueryParamWithBrackets( + @Query('value') List value, + ); + + @Get(path: '/map_query_param') + Future getUsingMapQueryParam( + @Query('value') Map value, + ); + + @Get( + path: '/map_query_param_include_null_query_vars', + includeNullQueryVars: true, + ) + Future getUsingMapQueryParamIncludeNullQueryVars( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_brackets', useBrackets: true) + Future getUsingMapQueryParamWithBrackets( + @Query('value') Map value, + ); + + @Get(path: 'headers') + Future getHeaders({ + @Header('x-string') required String stringHeader, + @Header('x-boolean') bool? boolHeader, + @Header('x-int') int? intHeader, + @Header('x-double') double? doubleHeader, + @Header('x-enum') ExampleEnum? enumHeader, + }); +} + +Request customConvertRequest(Request req) { + final r = JsonConverter().convertRequest(req); + + return applyHeader(r, 'customConverter', 'true'); +} + +Response customConvertResponse(Response res) => + res.copyWith(body: json.decode(res.body)); + +Request convertForm(Request req) { + req = applyHeader(req, contentTypeKey, formEncodedHeaders); + + if (req.body is Map) { + final body = {}; + + req.body.forEach((key, val) { + if (val != null) { + body[key.toString()] = val.toString(); + } + }); + + req = req.copyWith(body: body); + } + + return req; +} + +enum ExampleEnum { + foo, + bar, + baz; + + @override + String toString() => name; +} diff --git a/faq.md b/faq.md index 796a1eac..a5ffeef3 100644 --- a/faq.md +++ b/faq.md @@ -172,7 +172,7 @@ The actual implementation of the algorithm above may vary based on how the backe ### Authorized HTTP requests using the special Authenticator interceptor Similar to OkHTTP's [authenticator](https://github.com/square/okhttp/blob/480c20e46bb1745e280e42607bbcc73b2c953d97/okhttp/src/main/kotlin/okhttp3/Authenticator.kt), -the idea here is to provide a reactive authentication in the event that an auth challenge is raised. It returns a +the idea here is to provide a reactive authentication in the event that an auth challenge is raised. It returns a nullable Request that contains a possible update to the original Request to satisfy the authentication challenge. ```dart @@ -223,7 +223,7 @@ final client = ChopperClient( ## Decoding JSON using Isolates Sometimes you want to decode JSON outside the main thread in order to reduce janking. In this example we're going to go -even further and implement a Worker Pool using [Squadron](https://pub.dev/packages/squadron/install) which can +even further and implement a Worker Pool using [Squadron](https://pub.dev/packages/squadron/install) which can dynamically spawn a maximum number of Workers as they become needed. #### Install the dependencies @@ -259,7 +259,7 @@ Extracted from the [full example here](example/lib/json_decode_service.dart). #### Write a custom JsonConverter -Using [json_serializable](https://pub.dev/packages/json_serializable) we'll create a [JsonConverter](https://github.com/lejard-h/chopper/blob/master/chopper/lib/src/interceptor.dart#L228) +Using [json_serializable](https://pub.dev/packages/json_serializable) we'll create a [JsonConverter](https://github.com/lejard-h/chopper/blob/master/chopper/lib/src/interceptor.dart#L228) which works with or without a [WorkerPool](https://github.com/d-markey/squadron#features). ```dart @@ -412,4 +412,40 @@ This barely scratches the surface. If you want to know more about [squadron](htt [squadron_builder](https://github.com/d-markey/squadron_builder) make sure to head over to their respective repositories. [David Markey](https://github.com/d-markey]), the author of squadron, was kind enough as to provide us with an [excellent Flutter example](https://github.com/d-markey/squadron_builder) using -both packages. \ No newline at end of file +both packages. + +## How to use Chopper with [Injectable](https://pub.dev/packages/injectable) + +### Create a module for your ChopperClient + +Define a module for your ChopperClient. You can use the `@lazySingleton` (or other type if preferred) annotation to make sure that only one is created. + +```dart +@module +abstract class ChopperModule { + + @lazySingleton + ChopperClient get chopperClient => + ChopperClient( + baseUrl: 'https://base-url.com', + converter: JsonConverter(), + ); +} +``` + +### Create ChopperService with Injectable + +Define your ChopperService as usual. Annotate the class with `@lazySingleton` (or other type if preferred) and use the `@factoryMethod` annotation to specify the factory method for the service. This would normally be the static create method. + +```dart +@lazySingleton +@ChopperApi(baseUrl: '/todos') +abstract class TodosListService extends ChopperService { + + @factoryMethod + static TodosListService create(ChopperClient client) => _$TodosListService(client); + + @Get() + Future>> getTodos(); +} +``` diff --git a/getting-started.md b/getting-started.md index fe9cbd01..75030c20 100644 --- a/getting-started.md +++ b/getting-started.md @@ -25,8 +25,7 @@ Run `pub get` to start using Chopper in your project. ### ChopperApi -To define a client, use the `@ -ChopperApi` annotation on an abstract class that extends the `ChopperService` class. +To define a client, use the `@ChopperApi` annotation on an abstract class that extends the `ChopperService` class. ```dart // YOUR_FILE.dart @@ -66,7 +65,9 @@ Use one of the following annotations on abstract methods of a service class to d * `@Head` -Request methods must return with values of the type `Future` or `Future>`. +Request methods must return with values of the type `Future`, `Future>` or `Future`. +The `Response` class is a wrapper around the HTTP response that contains the response body, the status code and the error (if any) of the request. +This class can be omitted if only the response body is needed. When omitting the `Response` class, the request will throw an exception if the response status code is not in the range of `< 200` to ` > 300`. To define a `GET` request to the endpoint `/todos` in the service class above, add one of the following method declarations to the class: @@ -82,6 +83,13 @@ or Future>> getTodos(); ``` +or + +```dart +@Get() +Future> getTodos(); +``` + URL manipulation with dynamic path, and query parameters is also supported. To learn more about URL manipulation with Chopper, have a look at the [Requests](requests.md) section of the documentation. ## Defining a ChopperClient diff --git a/requests.md b/requests.md index cee349a1..60d7f2f3 100644 --- a/requests.md +++ b/requests.md @@ -1,63 +1,105 @@ # Requests +## Available Request annotations + +| Annotation | HTTP verb | Description | +|--------------------------------------------|-----------|-----------------------------------------------| +| `@Get()`, `@get` | `GET` | Defines a `GET` request. | +| `@Post()`, `@post` | `POST` | Defines a `POST` request. | +| `@Put()`, `@put` | `PUT` | Defines a `PUT` request. | +| `@Patch()`, `@patch` | `PATCH` | Defines a `PATCH` request. | +| `@Delete()`, `@delete` | `DELETE` | Defines a `DELETE` request. | +| `@Head()`, `@head` | `HEAD` | Defines a `HEAD` request. | +| `@Body()`, `@body` | - | Defines the request's body. | +| `@Multipart()`, `@multipart` | - | Defines a `multipart/form-data` request. | +| `@Query()`, `@query` | - | Defines a query parameter. | +| `@QueryMap()`, `@queryMap` | - | Defines a query parameter map. | +| `@FactoryConverter()`, `@factoryConverter` | - | Defines a request/response converter factory. | +| `@Field()`, `@field` | - | Defines a form field. | +| `@FieldMap()`, `@fieldMap` | - | Defines a form field map. | +| `@Part()`, `@part` | - | Defines a multipart part. | +| `@PartMap()`, `@partMap` | - | Defines a multipart part map. | +| `@PartFile()`, `@partFile` | - | Defines a multipart file part. | +| `@PartFileMap()`, `@partFileMap` | - | Defines a multipart file part map. | + ## Path resolution Chopper handles paths passed to HTTP verb annotations' `path` parameter based on the path's content. -If the `path` value is a relative path, it will be concatenated to the URL composed of the `baseUrl` of the `ChopperClient` and the `baseUrl` of the enclosing service class (provided as a parameter of the `@ChopperApi` annotation). +If the `path` value is a relative path, it will be concatenated to the URL composed of the `baseUrl` of +the `ChopperClient` and the `baseUrl` of the enclosing service class (provided as a parameter of the `@ChopperApi` +annotation). Here are a few examples of the described behavior: -* `ChopperClient` base URL: https://example.com/ - Path: profile - Result: https://example.com/profile - -* `ChopperClient` base URL: https://example.com/ - Service base URL: profile - Path: /image - Result: https://example.com/profile/image - -* `ChopperClient` base URL: https://example.com/ - Service base URL: profile - Path: image - Result: https://example.com/profile/image - -> Chopper detects and handles missing slash (`/`) characters on URL segment borders, but *does not* handle duplicate slashes. - -If the service's `baseUrl` concatenated with the request's `path` results in a full URL, the `ChopperClient`'s `baseUrl` is ignored. - -* `ChopperClient` base URL: https://example.com/ -Service base URL: https://api.github.com/ -Path: user -Result: https://api.github.com/user - -A `path` containing a full URL replaces the base URLs of both the `ChopperClient` and the service class entirely for a request. - -* `ChopperClient` base URL: https://example.com/ - Path: https://api.github.com/user - Result: https://api.github.com/user - -* `ChopperClient` base URL: https://example.com/ - Service base URL: profile - Path: https://api.github.com/user - Result: https://api.github.com/user +| Variable | URI | +|------------|-----------------------------| +| base URL | https://example.com/ | +| Path | profile | +| **Result** | https://example.com/profile | + +| Variable | URI | +|------------------|-----------------------------------| +| base URL | https://example.com/ | +| Service base URL | profile | +| Path | /image | +| **Result** | https://example.com/profile/image | + +| Variable | URI | +|------------------|-----------------------------------| +| base URL | https://example.com/ | +| Service base URL | profile | +| Path | image | +| **Result** | https://example.com/profile/image | + +> Chopper detects and handles missing slash (`/`) characters on URL segment borders, but *does not* handle duplicate +> slashes. + +If the service's `baseUrl` concatenated with the request's `path` results in a full URL, the `ChopperClient`'s `baseUrl` +is ignored. + +| Variable | URI | +|------------------|-----------------------------| +| base URL | https://example.com/ | +| Service base URL | https://api.github.com/ | +| Path | user | +| **Result** | https://api.github.com/user | + +A `path` containing a full URL replaces the base URLs of both the `ChopperClient` and the service class entirely for a +request. + +| Variable | URI | +|------------|-----------------------------| +| base URL | https://example.com/ | +| Path | https://api.github.com/user | +| **Result** | https://api.github.com/user | + +| Variable | URI | +|------------------|-----------------------------| +| base URL | https://example.com/ | +| Service base URL | profile | +| Path | https://api.github.com/user | +| **Result** | https://api.github.com/user | ## Path parameters -Dynamic path parameters can be defined in the URL with replacement blocks. A replacement block is an alphanumeric substring of the path surrounded by `{` and `}`. In the following example `{id}` is a replacement block. +Dynamic path parameters can be defined in the URL with replacement blocks. A replacement block is an alphanumeric +substring of the path surrounded by `{` and `}`. In the following example `{id}` is a replacement block. ```dart @Get(path: "/{id}") ``` -Use the `@Path()` annotation to bind a parameter to a replacement block. This way the parameter's name must match a replacement block's string. +Use the `@Path()` annotation to bind a parameter to a replacement block. This way the parameter's name must match a +replacement block's string. ```dart @Get(path: "/{id}") Future getItemById(@Path() String id); ``` -As an alternative, you can set the `@Path` annotation's `name` parameter to match a replacement block's string while using a different parameter name, like in the following example: +As an alternative, you can set the `@Path` annotation's `name` parameter to match a replacement block's string while +using a different parameter name, like in the following example: ```dart @Get(path: "/{id}") @@ -68,7 +110,8 @@ Future getItemById(@Path("id") int itemId); ## Query parameters -Dynamic query parameters can be added to the URL by adding parameters to a request method annotated with the `@Query` annotation. Default values are supported. +Dynamic query parameters can be added to the URL by adding parameters to a request method annotated with the `@Query` +annotation. Default values are supported. ```dart Future search( @@ -77,7 +120,8 @@ Future search( }); ``` -If the parameter of the `@Query` annotation is not set, Chopper will use the actual name of the annotated parameter as the key for the query parameter in the URL. +If the parameter of the `@Query` annotation is not set, Chopper will use the actual name of the annotated parameter as +the key for the query parameter in the URL. If you prefer to pass a `Map` of query parameters, you can do so with the `@QueryMap` annotation. @@ -97,50 +141,58 @@ Future postData(@Body() String data); {% hint style="warning" %} Chopper does not automatically convert `Object`s to `Map`then `JSON`. -You have to pass a [Converter](converters/converters.md) instance to a `ChopperClient` for JSON conversion to happen. See [built\_value\_converter](converters/built-value-converter.md#built-value) for an example Converter implementation. +You have to pass a [Converter](converters/converters.md) instance to a `ChopperClient` for JSON conversion to happen. +See [built\_value\_converter](converters/built-value-converter.md#built-value) for an example Converter implementation. {% endhint %} ## Headers -Request headers can be set by providing a `Map` object to the `headers` parameter each of the HTTP verb annotations have. +Request headers can be set by providing a `Map` object to the `headers` parameter each of the HTTP verb +annotations have. ```dart @Get(path: "/", headers: {"foo": "bar"}) Future fetch(); ``` -The `@Header` annotation can be used on method parameters to set headers dynamically for each request call. +The `@Header` annotation can be used on method parameters to set headers dynamically for each request call. ```dart @Get(path: "/") Future fetch(@Header("foo") String bar); ``` -> Setting request headers dynamically is also supported by [Interceptors](interceptors.md) and [Converters](converters/converters.md). +> Setting request headers dynamically is also supported by [Interceptors](interceptors.md) +> and [Converters](converters/converters.md). > -> As Chopper invokes Interceptors and Converter(s) *after* creating a Request, Interceptors and Converters *can* override headers set with the `headers` parameter or `@Header` annotations. +> As Chopper invokes Interceptors and Converter(s) *after* creating a Request, Interceptors and Converters *can* +> override headers set with the `headers` parameter or `@Header` annotations. ## Sending `application/x-www-form-urlencoded` data -If no Converter is specified for a request (neither on a `ChopperClient` nor with the `@FactoryConverter` annotation) and the request body is of type `Map`, the body will be sent as form URL encoded data. +If no Converter is specified for a request (neither on a `ChopperClient` nor with the `@FactoryConverter` annotation) +and the request body is of type `Map`, the body will be sent as form URL encoded data. > This is the default behavior of the http package. -You can also use `FormUrlEncodedConverter` that will add the correct `content-type` and convert a `Map` into `Map` for requests. +You can also use `FormUrlEncodedConverter` that will add the correct `content-type` and convert a `Map` +into `Map` for requests. ```dart + final chopper = ChopperClient( - converter: FormUrlEncodedConverter(), + converter: FormUrlEncodedConverter(), ); ``` ### On a single method -To do only a single type of request with form encoding in a service, use the provided `FormUrlEncodedConverter`'s `requestFactory` method with the `@FactoryConverter` annotation. +To do only a single type of request with form encoding in a service, use the provided `FormUrlEncodedConverter`' +s `requestFactory` method with the `@FactoryConverter` annotation. ```dart @Post( - path: "form", + path: "form", headers: {contentTypeKey: formEncodedHeaders}, ) @FactoryConverter( @@ -151,7 +203,8 @@ Future postForm(@Body() Map fields); ### Defining fields individually -To specify fields individually, use the `@Field` annotation on method parameters. If the field's name is not provided, the parameter's name is used as the field's name. +To specify fields individually, use the `@Field` annotation on method parameters. If the field's name is not provided, +the parameter's name is used as the field's name. ```dart @Post(path: "form") @@ -161,5 +214,53 @@ To specify fields individually, use the `@Field` annotation on method parameters Future post(@Field() String foo, @Field("b") int bar); ``` -## Sending files +## Sending files with `@multipart` + +### Sending a file in bytes as `List` using `@PartFile` + +```dart +@Post(path: 'file') +@multipart +Future postFile(@PartFile('file') List bytes,); +``` + +### Sending a file as `MultipartFile` using `@PartFile` with extra parameters via `@Part` + +```dart +@Post(path: 'file') +@multipart +Future postMultipartFile(@PartFile() MultipartFile file, { + @Part() String? id, +}); +``` + +### Sending multiple files as `List` using `@PartFile` + +```dart +@Post(path: 'files') +@multipart +Future postListFiles(@PartFile() List files); +``` + +## Defining Responses + +ChopperService methods need to return a `Future`. Its possible to define return types of `Future` or `Future>` where `T` is the type of the response body. +When `Response` is not needed for a request its also possible to define a return type of `Future` where `T` is the type of the response body. + +Chopper will generate a client which will return the specified return type. When the method doesn't directly returns `Response` and the HTTP call fails a exception is thrown. + +```dart +// Returns a Response +@Get(path: "/") +Future fetch(); + +// Returns a Response +@Get(path: "/") +Future> fetch(); + +// Returns a MyClass +@Get(path: "/") +Future fetch(); +``` +> Note: Chopper doesn't convert response bodies by itself to dart object. You need to use a [Converter](converters/converters.md) for that. \ No newline at end of file