From df374c7a12f5ef276f1b26a6205235162b36b556 Mon Sep 17 00:00:00 2001 From: via-guy <74960121+via-guy@users.noreply.github.com> Date: Sat, 13 Nov 2021 10:24:44 +0100 Subject: [PATCH] Use Flutter compute for parsing models (#408) * Format code * Fix pubspec * Generate compute calls * Add tests * Update parser name * Format code * Fix merge conflict * Add examples for post methods * Define a new parser annotation for using the Flutter function * Increment to version 3.0.0 with documentation for implementing the FlutterCompute parser option * Upgrade to version 3.0.0 * Use serialize and deserialize functions * Update documentation to match implementation better * Fix generating compute for requests body * Handle queries * Add tests for FlutterCompute parser * Update documentation * Ignore computing unserialised objects * Fix nullable bodies and add warnings for spawning for collections * Serialise lists * Handle lists for objects in one function * Fix casting lists * Fix tests * Remove failed check * Revert "Remove failed check" This reverts commit 91b80374b6f0b2c2c02c1f6b28858b4736b12963. * Fix tests * Update CHANGELOG Co-authored-by: Trevor Wang --- README.md | 43 ++- annotation/lib/http.dart | 13 +- example_dartmapper/pubspec.yaml | 2 +- flutter_example/lib/example.dart | 74 +++- flutter_example/lib/example.g.dart | 392 ++++++++++++++++++++- flutter_example/pubspec.yaml | 2 +- generator/CHANGELOG.md | 4 + generator/lib/src/generator.dart | 340 +++++++++++++----- generator/pubspec.yaml | 4 +- generator/test/src/generator_test_src.dart | 258 ++++++++++++++ 10 files changed, 1020 insertions(+), 112 deletions(-) diff --git a/README.md b/README.md index 36337d548..eba41f2e7 100644 --- a/README.md +++ b/README.md @@ -223,7 +223,14 @@ If you want to parse models on a separate thread, you can take advantage of the For each model that you use you will need to define 2 top-level functions: ```dart FutureOr deserializeTask(Map json); -FutureOr> serializeTask(Task object); +FutureOr serializeTask(Task object); +``` + +If you want to handle lists of objects, either as return types or parameters, you should provide List counterparts. + +```dart +FutureOr> deserializeTaskList(Map json); +FutureOr serializeTaskList(List objects); ``` E.g. @@ -235,40 +242,54 @@ E.g. abstract class RestClient { factory RestClient(Dio dio, {String baseUrl}) = _RestClient; + @GET("/task") + Future getTask(); @GET("/tasks") Future> getTasks(); + @POST("/task") + Future updateTasks(Task task); @POST("/tasks") Future updateTasks(List tasks); } Task deserializeTask(Map json) => Task.fromJson(json); -Map serializeTask(User object) => object.toJson(); +List deserializeTaskList(List> json) => + json.map((e) => Task.fromJson(e)).toList(); +Map serializeTask(Task object) => object.toJson(); +List> serializeTaskList(List objects) => + objects.map((e) => e.toJson()).toList(); ``` N.B. -It is recommended to use just a single object, if possible, as then only one background thread will be spawned to perform the computation. If you use a list or a map it will spawn a thread for each element. +Avoid using Map values, otherwise multiple background isolates will be spawned to perform the computation, which is extremely intensive for Dart. ```dart abstract class RestClient { factory RestClient(Dio dio, {String baseUrl}) = _RestClient; + // BAD @GET("/tasks") - Future> getTasks(); // BAD + Future> getTasks(); + @POST("/tasks") + Future updateTasks(Map tasks); - @GET("/tasks_list") - Future getTasksList(); // GOOD + // GOOD + @GET("/tasks_names") + Future getTaskNames(); + @POST("/tasks_names") + Future updateTasks(TaskNames tasks); } -TaskList deserializeTaskList(Map json) => TaskList.fromJson(json); +TaskNames deserializeTaskNames(Map json) => TaskNames.fromJson(json); @JsonSerializable -class TaskList { - const TaskList({required this.tasks}); +class TaskNames { + const TaskNames({required this.tasks}); - final List tasks; + final Map taskNames; - factory TaskList.fromJson(Map json) => _$TaskListFromJson(json); + factory TaskNames.fromJson(Map json) => _$TaskNamesFromJson(json); } ``` diff --git a/annotation/lib/http.dart b/annotation/lib/http.dart index ad4aa531c..dc4c171da 100644 --- a/annotation/lib/http.dart +++ b/annotation/lib/http.dart @@ -30,7 +30,14 @@ enum Parser { /// Each model class must define a top-level function, taking the form /// ``` /// FutureOr deserializeT(Map json); - /// FutureOr> serializeTask(T object); + /// FutureOr serializeTask(T object); + /// ``` + /// + /// If you want to handle lists of objects, either as return types or parameters, you should provide List counterparts. + /// + /// ``` + /// FutureOr> deserializeTList(Map json); + /// FutureOr serializeTList(List objects); /// ``` /// /// E.g. @@ -38,7 +45,11 @@ enum Parser { /// _In file user.dart_ /// ``` /// User deserializeUser(Map json) => User.fromJson(json); + /// List deserializeUserList(List> json) => + /// json.map((e) => User.fromJson(e)).toList(); /// Map serializeUser(User object) => object.toJson(); + /// List> serializeUserList(List objects) => + /// objects.map((e) => e.toJson()).toList(); /// /// @JsonSerializable() /// class User { diff --git a/example_dartmapper/pubspec.yaml b/example_dartmapper/pubspec.yaml index 30762eeef..5cc3b62c4 100644 --- a/example_dartmapper/pubspec.yaml +++ b/example_dartmapper/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: dev_dependencies: test: 1.16.5 retrofit_generator: - build_runner: ^1.12.2 + build_runner: ^2.0.1 json_serializable: ^4.0.3 mock_web_server: diff --git a/flutter_example/lib/example.dart b/flutter_example/lib/example.dart index 63596a151..f4e050460 100644 --- a/flutter_example/lib/example.dart +++ b/flutter_example/lib/example.dart @@ -1,14 +1,80 @@ -import 'mock_adapter.dart'; -import 'package:retrofit/retrofit.dart'; import 'package:dio/dio.dart' hide Headers; +import 'package:flutter/foundation.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:retrofit/retrofit.dart'; + +import 'mock_adapter.dart'; part 'example.g.dart'; -@RestApi(baseUrl: "http://baidu.com") +User deserializeUser(Map json) => User.fromJson(json); +List deserializeUserList(List> json) => + json.map((e) => User.fromJson(e)).toList(); +Map serializeUser(User object) => object.toJson(); +List> serializeUserList(List objects) => + objects.map((e) => e.toJson()).toList(); + +@JsonSerializable() +class User { + User({required this.id}); + + final String id; + + factory User.fromJson(Map json) => _$UserFromJson(json); + Map toJson() => _$UserToJson(this); +} + +@RestApi(baseUrl: "http://baidu.com", parser: Parser.FlutterCompute) abstract class RestClient { factory RestClient(Dio dio, {String baseUrl}) = _RestClient; + @GET('/tags') Future> getTags({@DioOptions() Options? options}); + @GET('/tagsNullable') + Future?> getTagsNullable({@DioOptions() Options? options}); + @GET('/tagByKey') + Future> getTagByKey({@DioOptions() Options? options}); + @GET('/tagByKeyNullable') + Future?> getTagByKeyNullable( + {@DioOptions() Options? options}); + @GET('/tag') + Future getTag({@DioOptions() Options? options}); + @GET('/tagNullable') + Future getTagNullable({@DioOptions() Options? options}); + + @GET('/users') + Future> getUsers({@DioOptions() Options? options}); + @GET('/usersNullable') + Future?> getUsersNullable({@DioOptions() Options? options}); + @GET('/userByKey') + Future> getUserByKey({@DioOptions() Options? options}); + @GET('/userByKeyNullable') + Future?> getUserByKeyNullable( + {@DioOptions() Options? options}); + @GET('/usersByKey') + Future>> getUsersByKey( + {@DioOptions() Options? options}); + @GET('/user') + Future getUser({@DioOptions() Options? options}); + @GET('/userNullable') + Future getUserNullable({@DioOptions() Options? options}); + + @PATCH('/user/{user}') + Future patchUser( + {@Query('u') required User user, @DioOptions() Options? options}); + @PATCH('/userMap/{user}') + Future patchUserMap( + {@Queries() required User user, @DioOptions() Options? options}); + + @POST('/users') + Future postUsers( + {@Body() required List users, @DioOptions() Options? options}); + @POST('/user') + Future postUser( + {@Body() required User user, @DioOptions() Options? options}); + @POST('/userNullable') + Future postUserNullable( + {@Body() required User? user, @DioOptions() Options? options}); } void test() { @@ -23,7 +89,7 @@ void test() { handler.next(options); })); final api = RestClient(dio, baseUrl: MockAdapter.mockBase); - api.getTags().then((it) { + api.getUsers().then((it) { print(it.length); }); } diff --git a/flutter_example/lib/example.g.dart b/flutter_example/lib/example.g.dart index 6bf04608e..4889fd47c 100644 --- a/flutter_example/lib/example.g.dart +++ b/flutter_example/lib/example.g.dart @@ -2,6 +2,18 @@ part of 'example.dart'; +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +User _$UserFromJson(Map json) => User( + id: json['id'] as String, + ); + +Map _$UserToJson(User instance) => { + 'id': instance.id, + }; + // ************************************************************************** // RetrofitGenerator // ************************************************************************** @@ -20,11 +32,12 @@ class _RestClient implements RestClient { const _extra = {}; final queryParameters = {}; queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; final _data = {}; final newOptions = newRequestOptions(options); newOptions.extra.addAll(_extra); newOptions.headers.addAll(_dio.options.headers); - newOptions.headers.addAll({}); + newOptions.headers.addAll(_headers); final _result = await _dio.fetch>(newOptions.copyWith( method: 'GET', baseUrl: baseUrl ?? _dio.options.baseUrl, @@ -35,6 +48,383 @@ class _RestClient implements RestClient { return value; } + @override + Future?> getTagsNullable({options}) async { + const _extra = {}; + final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + final _data = {}; + final newOptions = newRequestOptions(options); + newOptions.extra.addAll(_extra); + newOptions.headers.addAll(_dio.options.headers); + newOptions.headers.addAll(_headers); + final _result = await _dio.fetch>(newOptions.copyWith( + method: 'GET', + baseUrl: baseUrl ?? _dio.options.baseUrl, + queryParameters: queryParameters, + path: '/tagsNullable') + ..data = _data); + final value = _result.data?.cast(); + return value; + } + + @override + Future> getTagByKey({options}) async { + const _extra = {}; + final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + final _data = {}; + final newOptions = newRequestOptions(options); + newOptions.extra.addAll(_extra); + newOptions.headers.addAll(_dio.options.headers); + newOptions.headers.addAll(_headers); + final _result = await _dio.fetch>(newOptions.copyWith( + method: 'GET', + baseUrl: baseUrl ?? _dio.options.baseUrl, + queryParameters: queryParameters, + path: '/tagByKey') + ..data = _data); + final value = _result.data!.cast(); + return value; + } + + @override + Future?> getTagByKeyNullable({options}) async { + const _extra = {}; + final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + final _data = {}; + final newOptions = newRequestOptions(options); + newOptions.extra.addAll(_extra); + newOptions.headers.addAll(_dio.options.headers); + newOptions.headers.addAll(_headers); + final _result = await _dio.fetch>(newOptions.copyWith( + method: 'GET', + baseUrl: baseUrl ?? _dio.options.baseUrl, + queryParameters: queryParameters, + path: '/tagByKeyNullable') + ..data = _data); + final value = _result.data?.cast(); + return value; + } + + @override + Future getTag({options}) async { + const _extra = {}; + final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + final _data = {}; + final newOptions = newRequestOptions(options); + newOptions.extra.addAll(_extra); + newOptions.headers.addAll(_dio.options.headers); + newOptions.headers.addAll(_headers); + final _result = await _dio.fetch(newOptions.copyWith( + method: 'GET', + baseUrl: baseUrl ?? _dio.options.baseUrl, + queryParameters: queryParameters, + path: '/tag') + ..data = _data); + final value = _result.data!; + return value; + } + + @override + Future getTagNullable({options}) async { + const _extra = {}; + final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + final _data = {}; + final newOptions = newRequestOptions(options); + newOptions.extra.addAll(_extra); + newOptions.headers.addAll(_dio.options.headers); + newOptions.headers.addAll(_headers); + final _result = await _dio.fetch(newOptions.copyWith( + method: 'GET', + baseUrl: baseUrl ?? _dio.options.baseUrl, + queryParameters: queryParameters, + path: '/tagNullable') + ..data = _data); + final value = _result.data; + return value; + } + + @override + Future> getUsers({options}) async { + const _extra = {}; + final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + final _data = {}; + final newOptions = newRequestOptions(options); + newOptions.extra.addAll(_extra); + newOptions.headers.addAll(_dio.options.headers); + newOptions.headers.addAll(_headers); + final _result = await _dio.fetch>(newOptions.copyWith( + method: 'GET', + baseUrl: baseUrl ?? _dio.options.baseUrl, + queryParameters: queryParameters, + path: '/users') + ..data = _data); + var value = await compute( + deserializeUserList, _result.data!.cast>()); + return value; + } + + @override + Future?> getUsersNullable({options}) async { + const _extra = {}; + final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + final _data = {}; + final newOptions = newRequestOptions(options); + newOptions.extra.addAll(_extra); + newOptions.headers.addAll(_dio.options.headers); + newOptions.headers.addAll(_headers); + final _result = await _dio.fetch>(newOptions.copyWith( + method: 'GET', + baseUrl: baseUrl ?? _dio.options.baseUrl, + queryParameters: queryParameters, + path: '/usersNullable') + ..data = _data); + var value = _result.data == null + ? null + : await compute( + deserializeUserList, _result.data!.cast>()); + return value; + } + + @override + Future> getUserByKey({options}) async { + const _extra = {}; + final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + final _data = {}; + final newOptions = newRequestOptions(options); + newOptions.extra.addAll(_extra); + newOptions.headers.addAll(_dio.options.headers); + newOptions.headers.addAll(_headers); + final _result = await _dio.fetch>(newOptions.copyWith( + method: 'GET', + baseUrl: baseUrl ?? _dio.options.baseUrl, + queryParameters: queryParameters, + path: '/userByKey') + ..data = _data); + var value = Map.fromEntries(await Future.wait(_result.data!.entries.map( + (e) async => MapEntry(e.key, + await compute(deserializeUser, e.value as Map))))); + return value; + } + + @override + Future?> getUserByKeyNullable({options}) async { + const _extra = {}; + final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + final _data = {}; + final newOptions = newRequestOptions(options); + newOptions.extra.addAll(_extra); + newOptions.headers.addAll(_dio.options.headers); + newOptions.headers.addAll(_headers); + final _result = await _dio.fetch>(newOptions.copyWith( + method: 'GET', + baseUrl: baseUrl ?? _dio.options.baseUrl, + queryParameters: queryParameters, + path: '/userByKeyNullable') + ..data = _data); + var value = _result.data == null + ? null + : Map.fromEntries(await Future.wait(_result.data!.entries.map( + (e) async => MapEntry( + e.key, + await compute( + deserializeUser, e.value as Map))))); + return value; + } + + @override + Future>> getUsersByKey({options}) async { + const _extra = {}; + final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + final _data = {}; + final newOptions = newRequestOptions(options); + newOptions.extra.addAll(_extra); + newOptions.headers.addAll(_dio.options.headers); + newOptions.headers.addAll(_headers); + final _result = await _dio.fetch>(newOptions.copyWith( + method: 'GET', + baseUrl: baseUrl ?? _dio.options.baseUrl, + queryParameters: queryParameters, + path: '/usersByKey') + ..data = _data); + var value = Map.fromEntries(await Future.wait(_result.data!.entries.map( + (e) async => MapEntry( + e.key, + await compute(deserializeUserList, + (e.value as List).cast>()))))); + return value; + } + + @override + Future getUser({options}) async { + const _extra = {}; + final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + final _data = {}; + final newOptions = newRequestOptions(options); + newOptions.extra.addAll(_extra); + newOptions.headers.addAll(_dio.options.headers); + newOptions.headers.addAll(_headers); + final _result = await _dio.fetch>(newOptions.copyWith( + method: 'GET', + baseUrl: baseUrl ?? _dio.options.baseUrl, + queryParameters: queryParameters, + path: '/user') + ..data = _data); + final value = await compute(deserializeUser, _result.data!); + return value; + } + + @override + Future getUserNullable({options}) async { + const _extra = {}; + final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + final _data = {}; + final newOptions = newRequestOptions(options); + newOptions.extra.addAll(_extra); + newOptions.headers.addAll(_dio.options.headers); + newOptions.headers.addAll(_headers); + final _result = await _dio.fetch>(newOptions.copyWith( + method: 'GET', + baseUrl: baseUrl ?? _dio.options.baseUrl, + queryParameters: queryParameters, + path: '/userNullable') + ..data = _data); + final value = _result.data == null + ? null + : await compute(deserializeUser, _result.data!); + return value; + } + + @override + Future patchUser({required user, options}) async { + const _extra = {}; + final queryParameters = { + r'u': await compute(serializeUser, user) + }; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + final _data = {}; + final newOptions = newRequestOptions(options); + newOptions.extra.addAll(_extra); + newOptions.headers.addAll(_dio.options.headers); + newOptions.headers.addAll(_headers); + await _dio.fetch(newOptions.copyWith( + method: 'PATCH', + baseUrl: baseUrl ?? _dio.options.baseUrl, + queryParameters: queryParameters, + path: '/user/{user}') + ..data = _data); + return null; + } + + @override + Future patchUserMap({required user, options}) async { + const _extra = {}; + final queryParameters = {}; + queryParameters.addAll(await compute(serializeUser, user)); + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + final _data = {}; + final newOptions = newRequestOptions(options); + newOptions.extra.addAll(_extra); + newOptions.headers.addAll(_dio.options.headers); + newOptions.headers.addAll(_headers); + await _dio.fetch(newOptions.copyWith( + method: 'PATCH', + baseUrl: baseUrl ?? _dio.options.baseUrl, + queryParameters: queryParameters, + path: '/userMap/{user}') + ..data = _data); + return null; + } + + @override + Future postUsers({required users, options}) async { + const _extra = {}; + final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + final _data = await compute(serializeUserList, users); + final newOptions = newRequestOptions(options); + newOptions.extra.addAll(_extra); + newOptions.headers.addAll(_dio.options.headers); + newOptions.headers.addAll(_headers); + await _dio.fetch(newOptions.copyWith( + method: 'POST', + baseUrl: baseUrl ?? _dio.options.baseUrl, + queryParameters: queryParameters, + path: '/users') + ..data = _data); + return null; + } + + @override + Future postUser({required user, options}) async { + const _extra = {}; + final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + final _data = {}; + _data.addAll(await compute(serializeUser, user)); + final newOptions = newRequestOptions(options); + newOptions.extra.addAll(_extra); + newOptions.headers.addAll(_dio.options.headers); + newOptions.headers.addAll(_headers); + await _dio.fetch(newOptions.copyWith( + method: 'POST', + baseUrl: baseUrl ?? _dio.options.baseUrl, + queryParameters: queryParameters, + path: '/user') + ..data = _data); + return null; + } + + @override + Future postUserNullable({user, options}) async { + const _extra = {}; + final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + final _data = {}; + _data.addAll(user == null + ? {} + : await compute(serializeUser, user)); + final newOptions = newRequestOptions(options); + newOptions.extra.addAll(_extra); + newOptions.headers.addAll(_dio.options.headers); + newOptions.headers.addAll(_headers); + await _dio.fetch(newOptions.copyWith( + method: 'POST', + baseUrl: baseUrl ?? _dio.options.baseUrl, + queryParameters: queryParameters, + path: '/userNullable') + ..data = _data); + return null; + } + RequestOptions newRequestOptions(Options? options) { if (options is RequestOptions) { return options as RequestOptions; diff --git a/flutter_example/pubspec.yaml b/flutter_example/pubspec.yaml index 4f375ec54..9c04fe580 100644 --- a/flutter_example/pubspec.yaml +++ b/flutter_example/pubspec.yaml @@ -24,7 +24,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 retrofit: any - json_annotation: + json_annotation: ^4.3.0 logger: any dev_dependencies: diff --git a/generator/CHANGELOG.md b/generator/CHANGELOG.md index 10f3f6625..dcd84b49e 100644 --- a/generator/CHANGELOG.md +++ b/generator/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog +## 3.0.0 + +- Support `Parser.FlutterCompute` for generating data on separate isolates (#408) + ## 2.2.0 - rollback to dio from dip_http diff --git a/generator/lib/src/generator.dart b/generator/lib/src/generator.dart index ea60523f4..bccd230e2 100644 --- a/generator/lib/src/generator.dart +++ b/generator/lib/src/generator.dart @@ -488,33 +488,45 @@ class RetrofitGenerator extends GeneratorForAnnotation { .assignFinal(_resultVar) .statement, ); - Reference mapperCode; - switch (clientAnnotation.parser) { - case retrofit.Parser.MapSerializable: - mapperCode = refer( - '(dynamic i) => ${_displayString(innerReturnType)}.fromMap(i as Map)'); - break; - case retrofit.Parser.JsonSerializable: - mapperCode = refer( - '(dynamic i) => ${_displayString(innerReturnType)}.fromJson(i as Map)'); - break; - case retrofit.Parser.DartJsonMapper: - mapperCode = refer( - '(dynamic i) => JsonMapper.fromMap<${_displayString(innerReturnType)}>(i as Map)!'); - break; - default: - throw ArgumentError( - 'No parser set. Use either MapSerializable, JsonSerializable or DartJsonMapper'); - } - blocks.add( - refer('$_resultVar.data') - .propertyIf(thisNullable: returnType.isNullable, name: 'map') - .call([mapperCode]) - .property('toList') - .call([]) + if (clientAnnotation.parser == retrofit.Parser.FlutterCompute) { + blocks.add(refer('$_resultVar.data') + .conditionalIsNullIf( + thisNullable: returnType.isNullable, + whenFalse: refer('await compute').call([ + refer( + 'deserialize${_displayString(innerReturnType)}List'), + refer('$_resultVar.data!.cast>()') + ])) .assignVar('value') - .statement, - ); + .statement); + } else { + final Reference mapperCode; + switch (clientAnnotation.parser) { + case retrofit.Parser.MapSerializable: + mapperCode = refer( + '(dynamic i) => ${_displayString(innerReturnType)}.fromMap(i as Map)'); + break; + case retrofit.Parser.JsonSerializable: + mapperCode = refer( + '(dynamic i) => ${_displayString(innerReturnType)}.fromJson(i as Map)'); + break; + case retrofit.Parser.DartJsonMapper: + mapperCode = refer( + '(dynamic i) => JsonMapper.fromMap<${_displayString(innerReturnType)}>(i as Map)!'); + break; + case retrofit.Parser.FlutterCompute: + throw Exception('Unreachable code'); + } + blocks.add( + refer('$_resultVar.data') + .propertyIf(thisNullable: returnType.isNullable, name: 'map') + .call([mapperCode]) + .property('toList') + .call([]) + .assignVar('value') + .statement, + ); + } } } else if (_typeChecker(Map).isExactlyType(returnType) || _typeChecker(BuiltMap).isExactlyType(returnType)) { @@ -528,11 +540,13 @@ class RetrofitGenerator extends GeneratorForAnnotation { /// assume the first type is a basic type if (types.length > 1) { + final firstType = types[0]; final secondType = types[1]; if (_typeChecker(List).isExactlyType(secondType) || _typeChecker(BuiltList).isExactlyType(secondType)) { final type = _getResponseType(secondType); - Reference mapperCode; + final Reference mapperCode; + var future = false; switch (clientAnnotation.parser) { case retrofit.Parser.MapSerializable: mapperCode = refer(""" @@ -564,17 +578,39 @@ class RetrofitGenerator extends GeneratorForAnnotation { ) """); break; - default: - throw ArgumentError( - 'No parser set. Use either MapSerializable, JsonSerializable or DartJsonMapper'); + case retrofit.Parser.FlutterCompute: + log.warning(''' +Return types should not be a map when running `Parser.FlutterCompute`, as spawning an isolate per entry is extremely intensive. +You should create a new class to encapsulate the response. +'''); + future = true; + mapperCode = refer(""" + (e) async => MapEntry( + e.key, + await compute(deserialize${_displayString(type)}List, + (e.value as List).cast>())) + """); + break; + } + if (future) { + blocks.add(refer('Map.fromEntries') + .call([ + refer('await Future.wait').call([ + refer('$_resultVar.data!.entries.map').call([mapperCode]) + ]) + ]) + .assignVar('value') + .statement); + } else { + blocks.add(refer('$_resultVar.data') + .propertyIf(thisNullable: returnType.isNullable, name: 'map') + .call([mapperCode]) + .assignVar('value') + .statement); } - blocks.add(refer('$_resultVar.data') - .propertyIf(thisNullable: returnType.isNullable, name: 'map') - .call([mapperCode]) - .assignVar('value') - .statement); } else if (!_isBasicType(secondType)) { - Reference mapperCode; + final Reference mapperCode; + var future = false; switch (clientAnnotation.parser) { case retrofit.Parser.MapSerializable: mapperCode = refer( @@ -589,14 +625,45 @@ class RetrofitGenerator extends GeneratorForAnnotation { mapperCode = refer( '(k, dynamic v) => MapEntry(k, JsonMapper.fromMap<${_displayString(secondType)}>(v as Map)!)'); break; - default: - throw ArgumentError( - 'No parser set. Use either MapSerializable, JsonSerializable or DartJsonMapper'); + case retrofit.Parser.FlutterCompute: + log.warning(''' +Return types should not be a map when running `Parser.FlutterCompute`, as spawning an isolate per entry is extremely intensive. +You should create a new class to encapsulate the response. +'''); + future = true; + mapperCode = refer(""" + (e) async => MapEntry( + e.key, await compute(deserialize${_displayString(secondType)}, e.value as Map)) + """); + break; } + if (future) { + blocks.add(refer('$_resultVar.data') + .conditionalIsNullIf( + thisNullable: returnType.isNullable, + whenFalse: refer('Map.fromEntries').call([ + refer('await Future.wait').call([ + refer('$_resultVar.data!.entries.map') + .call([mapperCode]) + ]) + ])) + .assignVar('value') + .statement); + } else { + blocks.add(refer('$_resultVar.data') + .propertyIf(thisNullable: returnType.isNullable, name: 'map') + .call([mapperCode]) + .assignVar('value') + .statement); + } + } else { blocks.add(refer('$_resultVar.data') - .propertyIf(thisNullable: returnType.isNullable, name: 'map') - .call([mapperCode]) - .assignVar('value') + .propertyIf(thisNullable: returnType.isNullable, name: 'cast') + .call([], {}, [ + refer('${_displayString(firstType)}'), + refer('${_displayString(secondType)}'), + ]) + .assignFinal('value') .statement); } } else { @@ -639,7 +706,6 @@ class RetrofitGenerator extends GeneratorForAnnotation { final genericArgumentFactories = isGenericArgumentFactories(returnType); - // print('genericArgumentFactories:$genericArgumentFactories'); var typeArgs = returnType is ParameterizedType ? returnType.typeArguments : []; @@ -656,9 +722,10 @@ class RetrofitGenerator extends GeneratorForAnnotation { mapperCode = refer( 'JsonMapper.fromMap<${_displayString(returnType)}>($_resultVar.data!)!'); break; - default: - throw ArgumentError( - 'No parser set. Use either MapSerializable, JsonSerializable or DartJsonMapper'); + case retrofit.Parser.FlutterCompute: + mapperCode = refer( + 'await compute(deserialize${_displayString(returnType)}, $_resultVar.data!)'); + break; } blocks.add(refer('$_resultVar.data') .conditionalIsNullIf( @@ -1000,19 +1067,32 @@ class RetrofitGenerator extends GeneratorForAnnotation { final queries = _getAnnotations(m, retrofit.Query); final queryParameters = queries.map((p, ConstantReader r) { final key = r.peek("value")?.stringValue ?? p.displayName; - final value = (_isBasicType(p.type) || - p.type.isDartCoreList || - p.type.isDartCoreMap) - ? refer(p.displayName) - : clientAnnotation.parser == retrofit.Parser.DartJsonMapper - ? refer(p.displayName) - : clientAnnotation.parser == retrofit.Parser.JsonSerializable - ? p.type.nullabilitySuffix == NullabilitySuffix.question - ? refer(p.displayName).nullSafeProperty('toJson').call([]) - : refer(p.displayName).property('toJson').call([]) - : p.type.nullabilitySuffix == NullabilitySuffix.question - ? refer(p.displayName).nullSafeProperty('toMap').call([]) - : refer(p.displayName).property('toMap').call([]); + final Expression value; + if (_isBasicType(p.type) || + p.type.isDartCoreList || + p.type.isDartCoreMap) { + value = refer(p.displayName); + } else { + switch (clientAnnotation.parser) { + case retrofit.Parser.JsonSerializable: + value = p.type.nullabilitySuffix == NullabilitySuffix.question + ? refer(p.displayName).nullSafeProperty('toJson').call([]) + : refer(p.displayName).property('toJson').call([]); + break; + case retrofit.Parser.MapSerializable: + value = p.type.nullabilitySuffix == NullabilitySuffix.question + ? refer(p.displayName).nullSafeProperty('toMap').call([]) + : refer(p.displayName).property('toMap').call([]); + break; + case retrofit.Parser.DartJsonMapper: + value = refer(p.displayName); + break; + case retrofit.Parser.FlutterCompute: + value = refer( + 'await compute(serialize${_displayString(p.type)}, ${p.displayName})'); + break; + } + } return MapEntry(literalString(key, raw: true), value); }); @@ -1023,19 +1103,30 @@ class RetrofitGenerator extends GeneratorForAnnotation { for (final p in queryMap.keys) { final type = p.type; final displayName = p.displayName; - final value = (_isBasicType(type) || - type.isDartCoreList || - type.isDartCoreMap) - ? refer(displayName) - : clientAnnotation.parser == retrofit.Parser.DartJsonMapper - ? refer(displayName) - : clientAnnotation.parser == retrofit.Parser.JsonSerializable - ? type.nullabilitySuffix == NullabilitySuffix.question - ? refer(displayName).nullSafeProperty('toJson').call([]) - : refer(displayName).property('toJson').call([]) - : type.nullabilitySuffix == NullabilitySuffix.question - ? refer(displayName).nullSafeProperty('toMap').call([]) - : refer(displayName).property('toMap').call([]); + final Expression value; + if (_isBasicType(type) || type.isDartCoreList || type.isDartCoreMap) { + value = refer(displayName); + } else { + switch (clientAnnotation.parser) { + case retrofit.Parser.JsonSerializable: + value = p.type.nullabilitySuffix == NullabilitySuffix.question + ? refer(displayName).nullSafeProperty('toJson').call([]) + : refer(displayName).property('toJson').call([]); + break; + case retrofit.Parser.MapSerializable: + value = p.type.nullabilitySuffix == NullabilitySuffix.question + ? refer(displayName).nullSafeProperty('toMap').call([]) + : refer(displayName).property('toMap').call([]); + break; + case retrofit.Parser.DartJsonMapper: + value = refer(displayName); + break; + case retrofit.Parser.FlutterCompute: + value = refer( + 'await compute(serialize${_displayString(p.type)}, ${p.displayName})'); + break; + } + } /// workaround until this is merged in code_builder /// https://github.com/dart-lang/code_builder/pull/269 @@ -1051,8 +1142,7 @@ class RetrofitGenerator extends GeneratorForAnnotation { } if (m.parameters - .where((p) => (p.type.nullabilitySuffix == NullabilitySuffix.question)) - .isNotEmpty) { + .any((p) => (p.type.nullabilitySuffix == NullabilitySuffix.question))) { blocks.add(Code("$_queryParamsVar.removeWhere((k, v) => v == null);")); } } @@ -1087,9 +1177,24 @@ class RetrofitGenerator extends GeneratorForAnnotation { ((_typeChecker(List).isExactly(bodyTypeElement) || _typeChecker(BuiltList).isExactly(bodyTypeElement)) && !_isBasicInnerType(_bodyName.type))) { - blocks.add(refer(''' + switch (clientAnnotation.parser) { + case retrofit.Parser.JsonSerializable: + case retrofit.Parser.DartJsonMapper: + blocks.add(refer(''' ${_bodyName.displayName}.map((e) => e.toJson()).toList() ''').assignFinal(_dataVar).statement); + break; + case retrofit.Parser.MapSerializable: + blocks.add(refer(''' + ${_bodyName.displayName}.map((e) => e.toMap()).toList() + ''').assignFinal(_dataVar).statement); + break; + case retrofit.Parser.FlutterCompute: + blocks.add(refer(''' + await compute(serialize${_displayString(_genericOf(_bodyName.type))}List, ${_bodyName.displayName}) + ''').assignFinal(_dataVar).statement); + break; + } } else if (bodyTypeElement != null && _typeChecker(File).isExactly(bodyTypeElement)) { blocks.add(refer("Stream") @@ -1118,13 +1223,18 @@ class RetrofitGenerator extends GeneratorForAnnotation { ]).statement); } } else { - final toJson = ele.lookUpMethod('toJson', ele.library); - if (toJson == null) { + if (_missingToJson(ele)) { log.warning( "${_displayString(_bodyName.type)} must provide a `toJson()` method which return a Map.\n" "It is programmer's responsibility to make sure the ${_displayString(_bodyName.type)} is properly serialized"); blocks.add( refer(_bodyName.displayName).assignFinal(_dataVar).statement); + } else if (_missingSerialize(ele.enclosingElement, _bodyName.type)) { + log.warning( + "${_displayString(_bodyName.type)} must provide a `serialize${_displayString(_bodyName.type)}()` method which returns a Map.\n" + "It is programmer's responsibility to make sure the ${_displayString(_bodyName.type)} is properly serialized"); + blocks.add( + refer(_bodyName.displayName).assignFinal(_dataVar).statement); } else { blocks.add(literalMap({}, refer("String"), refer("dynamic")) .assignFinal(_dataVar) @@ -1142,16 +1252,40 @@ class RetrofitGenerator extends GeneratorForAnnotation { toJsonCode = _getInnerJsonDeSerializableMapperFn(_bodyType); } - if (_bodyName.type.nullabilitySuffix != - NullabilitySuffix.question) { - blocks.add(refer("$_dataVar.addAll").call([ - refer('${_bodyName.displayName}.toJson($toJsonCode)') - ]).statement); - } else { - blocks.add(refer("$_dataVar.addAll").call([ - refer( - '${_bodyName.displayName}?.toJson($toJsonCode) ?? {}') - ]).statement); + switch (clientAnnotation.parser) { + case retrofit.Parser.JsonSerializable: + case retrofit.Parser.DartJsonMapper: + if (_bodyName.type.nullabilitySuffix != + NullabilitySuffix.question) { + blocks.add(refer("$_dataVar.addAll").call([ + refer('${_bodyName.displayName}.toJson($toJsonCode)') + ]).statement); + } else { + blocks.add(refer("$_dataVar.addAll").call([ + refer( + '${_bodyName.displayName}?.toJson($toJsonCode) ?? {}') + ]).statement); + } + break; + case retrofit.Parser.FlutterCompute: + if (_bodyName.type.nullabilitySuffix != + NullabilitySuffix.question) { + blocks.add(refer("$_dataVar.addAll").call([ + refer( + 'await compute(serialize${_displayString(_bodyName.type)}, ${_bodyName.displayName})') + ]).statement); + } else { + blocks.add(refer("$_dataVar.addAll").call([ + refer('''${_bodyName.displayName} == null + ? {} + : await compute(serialize${_displayString(_bodyName.type)}, ${_bodyName.displayName}) + ''') + ]).statement); + } + break; + case retrofit.Parser.MapSerializable: + // Unreachable code + break; } if (nullToAbsent) @@ -1342,8 +1476,7 @@ class RetrofitGenerator extends GeneratorForAnnotation { ]).statement); } else if (innerType?.element is ClassElement) { final ele = innerType!.element as ClassElement; - final toJson = ele.lookUpMethod('toJson', ele.library); - if (toJson == null) { + if (_missingToJson(ele)) { throw Exception("toJson() method have to add to ${p.type}"); } else { blocks @@ -1379,8 +1512,7 @@ class RetrofitGenerator extends GeneratorForAnnotation { ]).statement); } else if (p.type.element is ClassElement) { final ele = p.type.element as ClassElement; - final toJson = ele.lookUpMethod('toJson', ele.library); - if (toJson == null) { + if (_missingToJson(ele)) { throw Exception("toJson() method have to add to ${p.type}"); } else { blocks.add(refer(_dataVar).property('fields').property("add").call([ @@ -1507,6 +1639,32 @@ class RetrofitGenerator extends GeneratorForAnnotation { ).assignConst(localExtraVar).statement); } } + + bool _missingToJson(ClassElement ele) { + switch (clientAnnotation.parser) { + case retrofit.Parser.JsonSerializable: + case retrofit.Parser.DartJsonMapper: + final toJson = ele.lookUpMethod('toJson', ele.library); + return toJson == null; + case retrofit.Parser.MapSerializable: + case retrofit.Parser.FlutterCompute: + return false; + } + } + + bool _missingSerialize(CompilationUnitElement ele, DartType type) { + switch (clientAnnotation.parser) { + case retrofit.Parser.JsonSerializable: + case retrofit.Parser.DartJsonMapper: + case retrofit.Parser.MapSerializable: + return false; + case retrofit.Parser.FlutterCompute: + return !ele.functions.any((element) => + element.name == 'serialize${_displayString(type)}' && + element.parameters.length == 1 && + _displayString(element.parameters[0].type) == _displayString(type)); + } + } } Builder generatorFactoryBuilder(BuilderOptions options) => SharedPartBuilder( diff --git a/generator/pubspec.yaml b/generator/pubspec.yaml index b8f2fa38f..48d95747d 100644 --- a/generator/pubspec.yaml +++ b/generator/pubspec.yaml @@ -1,6 +1,6 @@ name: retrofit_generator description: retrofit generator is an dio client generator using source_gen and inspired by Chopper and Retrofit. -version: 2.2.0 +version: 3.0.0 homepage: https://mings.in/retrofit.dart/ repository: https://github.com/trevorwang/retrofit.dart/ @@ -14,7 +14,7 @@ dependencies: built_collection: ^5.0.0 code_builder: ^4.0.0 tuple: ^2.0.0 - retrofit: ^2.2.0 + retrofit: ^3.0.0 analyzer: ^2.0.0 dart_style: ^2.0.1 build: ^2.0.1 diff --git a/generator/test/src/generator_test_src.dart b/generator/test/src/generator_test_src.dart index df08abbdf..91cd8ebc9 100644 --- a/generator/test/src/generator_test_src.dart +++ b/generator/test/src/generator_test_src.dart @@ -290,6 +290,8 @@ abstract class AbstractUser with AbstractUserMixin { User.fromJson(json); } +Map serializeUser(User object) => object.toJson(); + @ShouldGenerate( r''' final value = _result.data!; @@ -1004,6 +1006,262 @@ abstract class NullableMapSerializableTestMapBody2 { Future?> getResult(); } +@ShouldGenerate( + r''' + final value = await compute(deserializeUser, _result.data!); + return value; +''', + contains: true, +) +@RestApi( + baseUrl: "https://httpbin.org/", + parser: Parser.FlutterCompute, +) +abstract class ComputeGenericCast { + @POST("/xx") + Future getUser(); +} + +@ShouldGenerate( + r''' + final value = _result.data == null + ? null + : await compute(deserializeUser, _result.data!); + return value; +''', + contains: true, +) +@RestApi( + baseUrl: "https://httpbin.org/", + parser: Parser.FlutterCompute, +) +abstract class NullableComputeGenericCast { + @POST("/xx") + Future getUser(); +} + +@ShouldGenerate( + r''' + var value = await compute( + deserializeUserList, _result.data!.cast>()); + return value; +''', + contains: true, +) +@RestApi( + baseUrl: "https://httpbin.org/", + parser: Parser.FlutterCompute, +) +abstract class ComputeTestListBody { + @GET("/xx") + Future> getResult(); +} + +@ShouldGenerate( + r''' + var value = _result.data == null + ? null + : await compute( + deserializeUserList, _result.data!.cast>()); + return value; +''', + contains: true, +) +@RestApi( + baseUrl: "https://httpbin.org/", + parser: Parser.FlutterCompute, +) +abstract class NullableComputeTestListBody { + @GET("/xx") + Future?> getResult(); +} + +@ShouldGenerate( + r''' + var value = Map.fromEntries(await Future.wait(_result.data!.entries.map( + (e) async => MapEntry( + e.key, + await compute(deserializeUserList, + (e.value as List).cast>()))))); + return value; +''', + contains: true, + expectedLogItems: [ + ''' +Return types should not be a map when running `Parser.FlutterCompute`, as spawning an isolate per entry is extremely intensive. +You should create a new class to encapsulate the response. +''' + ], +) +@RestApi( + baseUrl: "https://httpbin.org/", + parser: Parser.FlutterCompute, +) +abstract class ComputeTestMapBody { + @GET("/xx") + Future>> getResult(); +} + +@ShouldGenerate( + r''' + var value = Map.fromEntries(await Future.wait(_result.data!.entries.map( + (e) async => MapEntry( + e.key, + await compute(deserializeUserList, + (e.value as List).cast>()))))); + return value; +''', + contains: true, + expectedLogItems: [ + ''' +Return types should not be a map when running `Parser.FlutterCompute`, as spawning an isolate per entry is extremely intensive. +You should create a new class to encapsulate the response. +''' + ], +) +@RestApi( + baseUrl: "https://httpbin.org/", + parser: Parser.FlutterCompute, +) +abstract class NullableComputeTestMapBody { + @GET("/xx") + Future>?> getResult(); +} + +@ShouldGenerate( + r''' + var value = Map.fromEntries(await Future.wait(_result.data!.entries.map( + (e) async => MapEntry(e.key, + await compute(deserializeUser, e.value as Map))))); + return value; +''', + contains: true, + expectedLogItems: [ + ''' +Return types should not be a map when running `Parser.FlutterCompute`, as spawning an isolate per entry is extremely intensive. +You should create a new class to encapsulate the response. +''' + ], +) +@RestApi( + baseUrl: "https://httpbin.org/", + parser: Parser.FlutterCompute, +) +abstract class ComputeTestMapBody2 { + @GET("/xx") + Future> getResult(); +} + +@ShouldGenerate( + r''' + var value = _result.data == null + ? null + : Map.fromEntries(await Future.wait(_result.data!.entries.map( + (e) async => MapEntry( + e.key, + await compute( + deserializeUser, e.value as Map))))); + return value; +''', + contains: true, + expectedLogItems: [ + ''' +Return types should not be a map when running `Parser.FlutterCompute`, as spawning an isolate per entry is extremely intensive. +You should create a new class to encapsulate the response. +''' + ], +) +@RestApi( + baseUrl: "https://httpbin.org/", + parser: Parser.FlutterCompute, +) +abstract class NullableComputeTestMapBody2 { + @GET("/xx") + Future?> getResult(); +} + +@ShouldGenerate( + r''' + final queryParameters = { + r'u': await compute(serializeUser, user) + }; +''', + contains: true, +) +@RestApi( + baseUrl: "https://httpbin.org/", + parser: Parser.FlutterCompute, +) +abstract class ComputeQuery { + @GET("/xx") + Future getResult(@Query('u') User user); +} + +@ShouldGenerate( + r''' + final queryParameters = {}; + queryParameters.addAll(await compute(serializeUser, user)); +''', + contains: true, +) +@RestApi( + baseUrl: "https://httpbin.org/", + parser: Parser.FlutterCompute, +) +abstract class ComputeQueries { + @GET("/xx") + Future getResult(@Queries() User user); +} + +@ShouldGenerate( + r''' + final _data = {}; + _data.addAll(await compute(serializeUser, user)); +''', + contains: true, +) +@RestApi( + baseUrl: "https://httpbin.org/", + parser: Parser.FlutterCompute, +) +abstract class TestComputeObjectBody { + @GET("/xx") + Future getResult(@Body() User user); +} + +@ShouldGenerate( + r''' + final _data = await compute(serializeUserList, users); +''', + contains: true, +) +@RestApi( + baseUrl: "https://httpbin.org/", + parser: Parser.FlutterCompute, +) +abstract class TestComputeObjectListBody { + @GET("/xx") + Future getResult(@Body() List users); +} + +@ShouldGenerate( + r''' + final _data = {}; + _data.addAll(user == null + ? {} + : await compute(serializeUser, user)); +''', + contains: true, +) +@RestApi( + baseUrl: "https://httpbin.org/", + parser: Parser.FlutterCompute, +) +abstract class TestComputeNullableObjectBody { + @GET("/xx") + Future getResult(@Body() User? user); +} + @ShouldGenerate( '_data.removeWhere((k, v) => v == null);', contains: true,