From 648cdfcbad9ab77d30b69c82b3097e1cb27d1bf7 Mon Sep 17 00:00:00 2001 From: Job Guldemeester Date: Wed, 3 Jan 2024 14:23:52 +0100 Subject: [PATCH 1/4] :sparkles: Added possibility to omit Response when creating a service. --- chopper/lib/src/response.dart | 15 + chopper/test/base_test.dart | 49 -- chopper/test/ensure_build_test.dart | 1 + chopper/test/response_test.dart | 105 +++ chopper/test/test_service.chopper.dart | 4 +- chopper/test/test_service.dart | 2 +- .../test/test_service_variable.chopper.dart | 4 +- chopper/test/test_service_variable.dart | 2 +- ...test_without_response_service.chopper.dart | 705 ++++++++++++++++++ .../test/test_without_response_service.dart | 236 ++++++ chopper_generator/lib/src/generator.dart | 79 +- chopper_generator/lib/src/vars.dart | 1 + chopper_generator/test/ensure_build_test.dart | 11 +- .../test/test_service.chopper.dart | 4 +- chopper_generator/test/test_service.dart | 2 +- .../test/test_service_variable.chopper.dart | 4 +- .../test/test_service_variable.dart | 2 +- ...test_without_response_service.chopper.dart | 705 ++++++++++++++++++ .../test/test_without_response_service.dart | 236 ++++++ 19 files changed, 2082 insertions(+), 85 deletions(-) create mode 100644 chopper/test/response_test.dart create mode 100644 chopper/test/test_without_response_service.chopper.dart create mode 100644 chopper/test/test_without_response_service.dart create mode 100644 chopper_generator/test/test_without_response_service.chopper.dart create mode 100644 chopper_generator/test/test_without_response_service.dart diff --git a/chopper/lib/src/response.dart b/chopper/lib/src/response.dart index 1dd2d7ab..22e07c27 100644 --- a/chopper/lib/src/response.dart +++ b/chopper/lib/src/response.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'dart:typed_data'; import 'package:equatable/equatable.dart' show EquatableMixin; @@ -76,6 +77,20 @@ base class Response with EquatableMixin { } } + /// 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 HttpException('Could not fetch response $statusCode: $error'); + } + } + @override List get props => [ base, diff --git a/chopper/test/base_test.dart b/chopper/test/base_test.dart index 597bf952..8de582a8 100644 --- a/chopper/test/base_test.dart +++ b/chopper/test/base_test.dart @@ -9,7 +9,6 @@ import 'package:http/testing.dart'; import 'package:test/test.dart'; import 'package:transparent_image/transparent_image.dart'; -import 'fixtures/error_fixtures.dart'; import 'fixtures/example_enum.dart'; import 'test_service.dart'; import 'test_service_base_url.dart'; @@ -1650,52 +1649,4 @@ void main() { httpClient.close(); }); - - 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()); - }); - }); } 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/response_test.dart b/chopper/test/response_test.dart new file mode 100644 index 00000000..b612cef1 --- /dev/null +++ b/chopper/test/response_test.dart @@ -0,0 +1,105 @@ +import 'dart:io'; + +import 'package:chopper/src/response.dart'; +import 'package:test/test.dart'; +import 'package:http/http.dart' as http; + +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 HttpException]', + () { + 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 HttpException]', + () { + final base = http.Response('Foobar', 200); + final response = Response(base, null); + + expect(() => response.bodyOrThrow, throwsA(isA())); + }); + }); +} diff --git a/chopper/test/test_service.chopper.dart b/chopper/test/test_service.chopper.dart index 0db60af9..ed9a98ed 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 diff --git a/chopper/test/test_service.dart b/chopper/test/test_service.dart index 1ed74368..ff34b785 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(); 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..5501d0b5 --- /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..1a0ac821 --- /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/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 915b49f8..a39c1181 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); }); } @@ -508,8 +548,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/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..16004b15 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 diff --git a/chopper_generator/test/test_service.dart b/chopper_generator/test/test_service.dart index 30d6a451..58dce35a 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(); 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..5501d0b5 --- /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..1a0ac821 --- /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; +} From 8790207ff367e77e8920495c91dae2a3b9b30d4b Mon Sep 17 00:00:00 2001 From: Job Guldemeester Date: Thu, 4 Jan 2024 11:54:14 +0100 Subject: [PATCH 2/4] :art: Updated exception based on feedback. Added test for void. --- chopper/lib/src/chopper_http_exception.dart | 13 +++++++++ chopper/lib/src/response.dart | 3 ++- chopper/test/response_test.dart | 27 ++++++++++++------- ...test_without_response_service.chopper.dart | 4 +-- .../test/test_without_response_service.dart | 2 +- ...test_without_response_service.chopper.dart | 4 +-- .../test/test_without_response_service.dart | 2 +- 7 files changed, 39 insertions(+), 16 deletions(-) create mode 100644 chopper/lib/src/chopper_http_exception.dart 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/response.dart b/chopper/lib/src/response.dart index 22e07c27..68d1d340 100644 --- a/chopper/lib/src/response.dart +++ b/chopper/lib/src/response.dart @@ -1,6 +1,7 @@ import 'dart:io'; 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'; @@ -87,7 +88,7 @@ base class Response with EquatableMixin { if (error is Exception) { throw error!; } - throw HttpException('Could not fetch response $statusCode: $error'); + throw ChopperHttpException(this); } } diff --git a/chopper/test/response_test.dart b/chopper/test/response_test.dart index b612cef1..34167a95 100644 --- a/chopper/test/response_test.dart +++ b/chopper/test/response_test.dart @@ -1,8 +1,7 @@ -import 'dart:io'; - +import 'package:chopper/src/chopper_http_exception.dart'; import 'package:chopper/src/response.dart'; -import 'package:test/test.dart'; import 'package:http/http.dart' as http; +import 'package:test/test.dart'; import 'fixtures/error_fixtures.dart'; @@ -81,25 +80,35 @@ void main() { final base = http.Response('Foobar', 400); final response = Response(base, '', error: 'Error occurred'); - expect(() => response.bodyOrThrow, throwsA(isA())); + expect(() => response.bodyOrThrow, throwsA(isA())); }); test( - 'Response is unsuccessful and has no error, [bodyOrThrow throws HttpException]', + '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())); + expect(() => response.bodyOrThrow, throwsA(isA())); }); test( - 'Response is successful and has no body, [bodyOrThrow throws HttpException]', + '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); - final response = Response(base, null); + // Ignoring void checks for testing purposes + //ignore: void_checks + final Response response = Response(base, Null); - expect(() => response.bodyOrThrow, throwsA(isA())); + expect(() => response.bodyOrThrow, returnsNormally); }); }); } diff --git a/chopper/test/test_without_response_service.chopper.dart b/chopper/test/test_without_response_service.chopper.dart index 5501d0b5..c7b8462a 100644 --- a/chopper/test/test_without_response_service.chopper.dart +++ b/chopper/test/test_without_response_service.chopper.dart @@ -269,7 +269,7 @@ final class _$HttpTestService extends HttpTestService { } @override - Future deleteTest(String id) async { + Future deleteTest(String id) async { final Uri $url = Uri.parse('/test/delete/${id}'); final Map $headers = { 'foo': 'bar', @@ -280,7 +280,7 @@ final class _$HttpTestService extends HttpTestService { client.baseUrl, headers: $headers, ); - final Response $response = await client.send($request); + 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 index 1a0ac821..d8a95417 100644 --- a/chopper/test/test_without_response_service.dart +++ b/chopper/test/test_without_response_service.dart @@ -80,7 +80,7 @@ abstract class HttpTestService extends ChopperService { Future putTest(@Path('id') String test, @Body() String data); @Delete(path: 'delete/{id}', headers: {'foo': 'bar'}) - Future deleteTest(@Path() String id); + Future deleteTest(@Path() String id); @Patch(path: 'patch/{id}') Future patchTest(@Path() String id, @Body() String data); diff --git a/chopper_generator/test/test_without_response_service.chopper.dart b/chopper_generator/test/test_without_response_service.chopper.dart index 5501d0b5..c7b8462a 100644 --- a/chopper_generator/test/test_without_response_service.chopper.dart +++ b/chopper_generator/test/test_without_response_service.chopper.dart @@ -269,7 +269,7 @@ final class _$HttpTestService extends HttpTestService { } @override - Future deleteTest(String id) async { + Future deleteTest(String id) async { final Uri $url = Uri.parse('/test/delete/${id}'); final Map $headers = { 'foo': 'bar', @@ -280,7 +280,7 @@ final class _$HttpTestService extends HttpTestService { client.baseUrl, headers: $headers, ); - final Response $response = await client.send($request); + 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 index 1a0ac821..d8a95417 100644 --- a/chopper_generator/test/test_without_response_service.dart +++ b/chopper_generator/test/test_without_response_service.dart @@ -80,7 +80,7 @@ abstract class HttpTestService extends ChopperService { Future putTest(@Path('id') String test, @Body() String data); @Delete(path: 'delete/{id}', headers: {'foo': 'bar'}) - Future deleteTest(@Path() String id); + Future deleteTest(@Path() String id); @Patch(path: 'patch/{id}') Future patchTest(@Path() String id, @Body() String data); From 76116a76ba85ff73ed0b4ec6eadfd9b5ae833c49 Mon Sep 17 00:00:00 2001 From: Job Guldemeester Date: Thu, 4 Jan 2024 12:10:00 +0100 Subject: [PATCH 3/4] :white_check_mark: Updated bodyOrThrow test, added ChopperHttpException test --- chopper/test/chopper_http_exception_test.dart | 21 +++++++++++++++++++ chopper/test/response_test.dart | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 chopper/test/chopper_http_exception_test.dart 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/response_test.dart b/chopper/test/response_test.dart index 34167a95..201d3cb4 100644 --- a/chopper/test/response_test.dart +++ b/chopper/test/response_test.dart @@ -106,7 +106,7 @@ void main() { final base = http.Response('Foobar', 200); // Ignoring void checks for testing purposes //ignore: void_checks - final Response response = Response(base, Null); + final Response response = Response(base, ''); expect(() => response.bodyOrThrow, returnsNormally); }); From d6170ea14d3bc7cc17922422716e8c97f5de2e41 Mon Sep 17 00:00:00 2001 From: Job Guldemeester Date: Fri, 5 Jan 2024 07:33:27 +0100 Subject: [PATCH 4/4] :fire: removed dart:io import --- chopper/lib/src/response.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/chopper/lib/src/response.dart b/chopper/lib/src/response.dart index 68d1d340..e5a18d4e 100644 --- a/chopper/lib/src/response.dart +++ b/chopper/lib/src/response.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'dart:typed_data'; import 'package:chopper/src/chopper_http_exception.dart';