From 4776ca1ae8b99b3354a17a81d997eb95e44165d4 Mon Sep 17 00:00:00 2001 From: Trevor Wang Date: Mon, 30 Dec 2024 15:14:32 +0800 Subject: [PATCH] Revert "Implemented Call Adapter (#729)" This reverts commit ad250f70eab63a94ba6d4e075ec13e36b17fdd44. --- README.md | 34 --- generator/CHANGELOG.md | 31 -- generator/lib/src/generator.dart | 339 +++++---------------- generator/pubspec.yaml | 9 +- generator/test/src/generator_test_src.dart | 79 +---- retrofit/CHANGELOG.md | 30 -- retrofit/lib/call_adapter.dart | 53 ---- retrofit/lib/http.dart | 2 - retrofit/lib/retrofit.dart | 1 - retrofit/pubspec.yaml | 2 +- 10 files changed, 84 insertions(+), 496 deletions(-) delete mode 100644 retrofit/lib/call_adapter.dart diff --git a/README.md b/README.md index 0743e4c3..9c9f56ca 100644 --- a/README.md +++ b/README.md @@ -242,40 +242,6 @@ dio.options.baseUrl = 'https://5d42a6e2bc64f90014a56ca0.mockapi.io/api/v1'; final client = RestClient(dio); ``` -### Call Adapter - -This feature allows you to adapt the return type of a network call from one type to another. - -For example: -Future → Future> - -This feature provides flexibility in handling API responses, enabling better integration with custom response wrappers or error handling libraries. - -The CallAdapter takes the original return type R and transforms it into a new type T. This is particularly useful when working with response wrappers like Either, Result, or ApiResponse. - -Below is an example using a custom CallAdapter with a Result wrapper: -```dart - class MyCallAdapter extends CallAdapter, Future>> { - @override - Future> adapt(Future Function() call) async { - try { - final response = await call(); - return Result.ok(response); - } catch (e) { - return Result.err(e.toString()); - } - } - } - - @RestApi(callAdapter: MyCallAdapter) - abstract class RestClient { - factory RestClient(Dio dio, {String? baseUrl}) = _RestClient; - - @GET('/') - Future> getUser(); - } -``` - ### Multiple endpoints support If you want to use multiple endpoints to your `RestClient`, you should pass your base url when you initiate `RestClient`. Any value defined in `RestApi` will be ignored. diff --git a/generator/CHANGELOG.md b/generator/CHANGELOG.md index 1e3e1f5b..a541a839 100644 --- a/generator/CHANGELOG.md +++ b/generator/CHANGELOG.md @@ -1,36 +1,5 @@ # Changelog -## 9.1.6 - -- Introduced CallAdapters, This feature allows adaptation of a Call with return type R into the type of T. - e.g. Future to Future> - - Code Example: - -```dart - class MyCallAdapter extends CallAdapter, Future>> { - @override - Future> adapt(Future Function() call) async { - try { - final response = await call(); - return Either.right(response); - } - catch (e) { - return Either.left(ApiError(e)) - } - } - } - - @RestApi() - abstract class RestClient { - factory RestClient(Dio dio, {String? baseUrl}) = _RestClient; - - @UseCallAdapter(MyCallAdapter) - @GET('/') - Future getTasks(); - } -``` - ## 9.1.5 - Add support for nested object of non-primitive types in `TypedExtras`. diff --git a/generator/lib/src/generator.dart b/generator/lib/src/generator.dart index 6fccf9c7..936cb75c 100644 --- a/generator/lib/src/generator.dart +++ b/generator/lib/src/generator.dart @@ -1,5 +1,6 @@ import 'dart:ffi'; import 'dart:io'; + import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/nullability_suffix.dart'; @@ -71,8 +72,6 @@ class RetrofitGenerator extends GeneratorForAnnotation { /// Annotation details for [retrofit.RestApi] late retrofit.RestApi clientAnnotation; - ConstantReader? clientAnnotationConstantReader; - @override String generateForAnnotatedElement( Element element, @@ -98,7 +97,6 @@ class RetrofitGenerator extends GeneratorForAnnotation { baseUrl: annotation?.peek(_baseUrlVar)?.stringValue ?? '', parser: parser ?? retrofit.Parser.JsonSerializable, ); - clientAnnotationConstantReader = annotation; final baseUrl = clientAnnotation.baseUrl; final annotateClassConsts = element.constructors .where((c) => !c.isFactory && !c.isDefaultConstructor); @@ -224,169 +222,16 @@ class RetrofitGenerator extends GeneratorForAnnotation { c.body = Block.of(block); } }); - - // Traverses a type to find a matching type argument - // e.g. given a type `List>` and a key `User`, it will return the `DartType` "User" - DartType? findMatchingTypeArgument(DartType? type, String key) { - if (type?.getDisplayString() == key) { - return type; - } - - if (type is InterfaceType) { - for (final arg in type.typeArguments) { - final match = findMatchingTypeArgument(arg, key); - if (match != null) { - return match; - } - } - } - return null; - } - - // retrieve CallAdapter from method annotation or class annotation - ConstantReader? getCallAdapterInterface(MethodElement m) { - final requestCallAdapterAnnotation = _typeChecker(retrofit.UseCallAdapter) - .firstAnnotationOf(m) - .toConstantReader(); - final rootCallAdapter = clientAnnotationConstantReader; - - final callAdapter = (requestCallAdapterAnnotation ?? rootCallAdapter) - ?.peek('callAdapter'); - - final callAdapterTypeValue = callAdapter?.typeValue as InterfaceType?; - if (callAdapterTypeValue != null) { - final typeArg = callAdapterTypeValue.typeArguments.firstOrNull; - if (typeArg == null) { - throw InvalidGenerationSource( - 'your CallAdapter subclass must accept a generic type parameter \n' - 'e.g. "class ResultAdapter extends CallAdapter..."', - ); - } - } - return callAdapter; - } - - /// get result type being adapted to e.g. Future> - /// where T is supposed to be the wrapped result type - InterfaceType? getAdaptedReturnType(ConstantReader? callAdapter) { - final callAdapterTypeVal = callAdapter?.typeValue as InterfaceType?; - final adaptedType = - callAdapterTypeVal?.superclass?.typeArguments.lastOrNull as InterfaceType?; - return adaptedType; - } - - /// extract the wrapped result type of an adapted call... - /// Usage scenario: - /// given the return type of the api method is `Future>`, - /// and the second type parameter(T) on CallAdapter is `Future>`, - /// this method basically figures out the value of 'T' which will be "UserResponse" - /// in this case - String extractWrappedResultType(String template, String actual) { - final regexPattern = RegExp( - RegExp.escape(template).replaceAll('dynamic', r'([\w<>]+)'), - ); - final match = regexPattern.firstMatch(actual); - - if (match != null && match.groupCount > 0) { - return match.group(1) ?? ''; - } - return ''; - } - // parse methods in the Api class - Iterable _parseMethods(ClassElement element) { - List methods = []; - final methodMembers = [ - ...element.methods, - ...element.mixins.expand((i) => i.methods), - ]; - for (final method in methodMembers) { - final callAdapter = getCallAdapterInterface(method); - final adaptedReturnType = getAdaptedReturnType(callAdapter); - final resultTypeInString = extractWrappedResultType( - adaptedReturnType != null ? _displayString(adaptedReturnType) : '', - _displayString(method.returnType), - ); - final typeArg = findMatchingTypeArgument(method.returnType, resultTypeInString); - final instantiatedCallAdapter = typeArg != null ? - (callAdapter?.typeValue as InterfaceType?)?.element.instantiate( - typeArguments: [typeArg], - nullabilitySuffix: NullabilitySuffix.none, - ) : null; - if (method.isAbstract) { - methods.add(_generateApiCallMethod(method, instantiatedCallAdapter)!); - } - if (callAdapter != null) { - methods.add(_generateAdapterMethod(method, instantiatedCallAdapter, resultTypeInString)); - } - } - return methods; - } - - Method _generateAdapterMethod( - MethodElement m, - InterfaceType? callAdapter, - String resultType, - ) { - return Method((methodBuilder) { - methodBuilder.returns = - refer(_displayString(m.returnType, withNullability: true)); - methodBuilder.requiredParameters.addAll( - _generateParameters( - m, - (it) => it.isRequiredPositional, - ), - ); - methodBuilder.optionalParameters.addAll( - _generateParameters( - m, - (it) => it.isOptional || it.isRequiredNamed, - optional: true, - ), - ); - methodBuilder.name = m.displayName; - methodBuilder.annotations.add(const CodeExpression(Code('override'))); - final positionalArgs = []; - final namedArgs = []; - for (final parameter in m.parameters) { - if (parameter.isRequiredPositional || parameter.isOptionalPositional) { - positionalArgs.add(parameter.displayName); - } - if (parameter.isNamed) { - namedArgs.add('${parameter.displayName}: ${parameter.displayName}'); - } - } - final args = - '${positionalArgs.map((e) => '$e,').join()} ${namedArgs.map((e) => '$e,').join()}'; - methodBuilder.body = Code(''' - return ${callAdapter?.element.name}<$resultType>().adapt( - () => _${m.displayName}($args), - ); - '''); - }); - } - - Iterable _generateParameters( - MethodElement m, - bool Function(ParameterElement) filter, { - bool optional = false, - }) { - return m.parameters.where(filter).map( - (it) => Parameter( - (p) => p - ..name = it.name - ..named = it.isNamed - ..type = refer(it.type.getDisplayString()) - ..required = optional && - it.isNamed && - it.type.nullabilitySuffix == NullabilitySuffix.none && - !it.hasDefaultValue - ..defaultTo = optional && it.defaultValueCode != null - ? Code(it.defaultValueCode!) - : null, - ), - ); - } + Iterable _parseMethods(ClassElement element) => [ + ...element.methods, + ...element.mixins.expand((i) => i.methods), + ].where((m) { + final methodAnnotation = _getMethodAnnotation(m); + return methodAnnotation != null && + m.isAbstract && + (m.returnType.isDartAsyncFuture || m.returnType.isDartAsyncStream); + }).map((m) => _generateMethod(m)!); String _generateTypeParameterizedName(TypeParameterizedElement element) => element.displayName + @@ -524,82 +369,61 @@ class RetrofitGenerator extends GeneratorForAnnotation { return _getResponseInnerType(generic); } - void _configureMethodMetadata( - MethodBuilder mm, - MethodElement m, - String returnType, - bool hasCallAdapter, - ) { - mm - ..returns = refer(returnType) - ..name = hasCallAdapter ? '_${m.displayName}' : m.displayName - ..types.addAll(m.typeParameters.map((e) => refer(e.name))) - ..modifier = _isReturnTypeFuture(returnType) - ? MethodModifier.async - : MethodModifier.asyncStar; - } - - void _addParameters(MethodBuilder mm, MethodElement m) { - mm.requiredParameters.addAll( - _generateParameters(m, (it) => it.isRequiredPositional), - ); - mm.optionalParameters.addAll( - _generateParameters(m, (it) => it.isOptional || it.isRequiredNamed, - optional: true), - ); - } - - void _addAnnotations( - MethodBuilder mm, - DartType? returnType, - bool hasCallAdapter, - ) { - if (!hasCallAdapter) { - mm.annotations.add(const CodeExpression(Code('override'))); - } - if (globalOptions.useResult ?? false) { - if (returnType is ParameterizedType && - returnType.typeArguments.first is! VoidType) { - mm.annotations.add(const CodeExpression(Code('useResult'))); - } - } - } - - // generate the method that makes the http request - Method? _generateApiCallMethod(MethodElement m, InterfaceType? callAdapter) { - final hasCallAdapter = callAdapter != null; - - if (hasCallAdapter) { - return _generatePrivateApiCallMethod(m, callAdapter); + Method? _generateMethod(MethodElement m) { + final httpMethod = _getMethodAnnotation(m); + if (httpMethod == null) { + return null; } - final httpMethod = _getMethodAnnotation(m); - if (httpMethod == null) return null; - - final returnType = m.returnType; - return Method((methodBuilder) { - _configureMethodMetadata(methodBuilder, m, - _displayString(returnType, withNullability: true), false); - _addParameters(methodBuilder, m); - _addAnnotations(methodBuilder, returnType, false); - methodBuilder.body = - _generateRequest(m, httpMethod, null); - }); - } + return Method((mm) { + mm + ..returns = + refer(_displayString(m.type.returnType, withNullability: true)) + ..name = m.displayName + ..types.addAll(m.typeParameters.map((e) => refer(e.name))) + ..modifier = m.returnType.isDartAsyncFuture + ? MethodModifier.async + : MethodModifier.asyncStar + ..annotations.add(const CodeExpression(Code('override'))); + + if (globalOptions.useResult ?? false) { + final returnType = m.returnType; + if (returnType is ParameterizedType && + returnType.typeArguments.first is! VoidType) { + mm.annotations.add(const CodeExpression(Code('useResult'))); + } + } - Method? _generatePrivateApiCallMethod( - MethodElement m, InterfaceType? callAdapter) { - final callAdapterOriginalReturnType = callAdapter?.superclass - ?.typeArguments.firstOrNull as InterfaceType?; - - final httpMethod = _getMethodAnnotation(m); - if (httpMethod == null) return null; + /// required parameters + mm.requiredParameters.addAll( + m.parameters.where((it) => it.isRequiredPositional).map( + (it) => Parameter( + (p) => p + ..name = it.name + ..named = it.isNamed + ..type = refer(it.type.getDisplayString()), + ), + ), + ); - return Method((methodBuilder) { - _configureMethodMetadata(methodBuilder, m, _displayString(callAdapterOriginalReturnType), true); - _addParameters(methodBuilder, m); - _addAnnotations(methodBuilder, m.returnType, true); - methodBuilder.body = _generateRequest(m, httpMethod, callAdapter); + /// optional positional or named parameters + mm.optionalParameters.addAll( + m.parameters.where((i) => i.isOptional || i.isRequiredNamed).map( + (it) => Parameter( + (p) => p + ..required = (it.isNamed && + it.type.nullabilitySuffix == NullabilitySuffix.none && + !it.hasDefaultValue) + ..name = it.name + ..named = it.isNamed + ..type = refer(it.type.getDisplayString()) + ..defaultTo = it.defaultValueCode == null + ? null + : Code(it.defaultValueCode!), + ), + ), + ); + mm.body = _generateRequest(m, httpMethod); }); } @@ -616,13 +440,7 @@ class RetrofitGenerator extends GeneratorForAnnotation { return literal(definePath); } - bool _isReturnTypeFuture(String type) => type.startsWith('Future<'); - - Code _generateRequest( - MethodElement m, - ConstantReader httpMethod, - InterfaceType? callAdapter, - ) { + Code _generateRequest(MethodElement m, ConstantReader httpMethod) { final returnAsyncWrapper = m.returnType.isDartAsyncFuture ? 'return' : 'yield'; final path = _generatePath(m, httpMethod); @@ -731,6 +549,8 @@ class RetrofitGenerator extends GeneratorForAnnotation { refer(receiveProgress.element.displayName); } + final wrappedReturnType = _getResponseType(m.returnType); + blocks.add( declareFinal(_optionsVar) .assign(_parseOptions(m, namedArguments, blocks, extraOptions)) @@ -739,20 +559,20 @@ class RetrofitGenerator extends GeneratorForAnnotation { final options = refer(_optionsVar).expression; - final wrappedReturnType = _getResponseType( - callAdapter != null - ? callAdapter.superclass!.typeArguments.first - : m.returnType, - ); - final isWrappedWithHttpResponseWrapper = wrappedReturnType != null - ? _typeChecker(retrofit.HttpResponse).isExactlyType(wrappedReturnType) - : false; + if (wrappedReturnType == null || 'void' == wrappedReturnType.toString()) { + blocks.add( + refer('await $_dioVar.fetch') + .call([options], {}, [refer('void')]).statement, + ); + return Block.of(blocks); + } - final returnType = isWrappedWithHttpResponseWrapper - ? _getResponseType(wrappedReturnType) - : wrappedReturnType; + final isWrapped = + _typeChecker(retrofit.HttpResponse).isExactlyType(wrappedReturnType); + final returnType = + isWrapped ? _getResponseType(wrappedReturnType) : wrappedReturnType; if (returnType == null || 'void' == returnType.toString()) { - if (isWrappedWithHttpResponseWrapper) { + if (isWrapped) { blocks ..add( refer('final $_resultVar = await $_dioVar.fetch') @@ -1184,7 +1004,7 @@ You should create a new class to encapsulate the response. ); } } - if (isWrappedWithHttpResponseWrapper) { + if (isWrapped) { blocks.add( Code(''' final httpResponse = HttpResponse($_valueVar, $_resultVar); @@ -2833,11 +2653,6 @@ extension DartObjectX on DartObject? { bool get isEnum { return this?.type?.element?.kind.name == 'ENUM'; } - - ConstantReader? toConstantReader() { - if (this == null) return null; - return ConstantReader(this); - } } extension ReferenceExt on Reference { diff --git a/generator/pubspec.yaml b/generator/pubspec.yaml index 9a71abbd..7c3d276e 100644 --- a/generator/pubspec.yaml +++ b/generator/pubspec.yaml @@ -8,7 +8,7 @@ topics: - rest - retrofit - codegen -version: 9.1.6 +version: 9.1.5 environment: sdk: '>=3.3.0 <4.0.0' @@ -20,15 +20,10 @@ dependencies: dart_style: ^2.3.0 dio: ^5.0.0 protobuf: ^3.1.0 - retrofit: ^4.4.2 + retrofit: ^4.4.0 source_gen: ^1.5.0 dev_dependencies: lints: ^4.0.0 source_gen_test: ^1.0.6 test: ^1.25.0 - -dependency_overrides: - retrofit: - path: ../retrofit - \ No newline at end of file diff --git a/generator/test/src/generator_test_src.dart b/generator/test/src/generator_test_src.dart index d43c47d0..40e28f04 100644 --- a/generator/test/src/generator_test_src.dart +++ b/generator/test/src/generator_test_src.dart @@ -1,83 +1,12 @@ import 'dart:io'; + import 'package:dio/dio.dart' hide Headers; import 'package:retrofit/retrofit.dart'; import 'package:source_gen_test/annotations.dart'; -import 'query.pb.dart'; -class Resource {} -class MockCallAdapter1 extends CallAdapter, Future>> { - @override - Future> adapt(Future Function() call) async { - return Resource(); - } -} -@ShouldGenerate( -''' - @override - Future>> getUser() { - return MockCallAdapter1>().adapt( - () => _getUser(), - ); - } -''', - contains: true, -) -@RestApi() -abstract class TestCallAdapter1 { - @UseCallAdapter(MockCallAdapter1) - @GET('path') - Future>> getUser(); -} - -class Either {} -class MockCallAdapter2 extends CallAdapter, Future>> { - @override - Future> adapt(Future Function() call) async { - return Either(); - } -} -@ShouldGenerate( -''' - @override - Future> getUser() { - return MockCallAdapter2().adapt( - () => _getUser(), - ); - } -''', - contains: true, -) -@RestApi() -abstract class TestCallAdapter2 { - @UseCallAdapter(MockCallAdapter2) - @GET('path') - Future> getUser(); -} +import 'query.pb.dart'; -class Flow {} -class MockCallAdapter3 extends CallAdapter, Flow> { - @override - Flow adapt(Future Function() call) { - return Flow(); - } -} -@ShouldGenerate( -''' - @override - Flow getUser() { - return MockCallAdapter3().adapt( - () => _getUser(), - ); - } -''', - contains: true, -) -@RestApi() -abstract class TestCallAdapter3 { - @UseCallAdapter(MockCallAdapter3) - @GET('path') - Flow getUser(); -} +enum FileType { mp4, mp3 } class Config { final String date; @@ -92,7 +21,7 @@ class Config { required this.subConfig, }); } -enum FileType { mp4, mp3 } + class DummyTypedExtras extends TypedExtras { final String id; final Config config; diff --git a/retrofit/CHANGELOG.md b/retrofit/CHANGELOG.md index 1d832e2a..5c70f2ef 100644 --- a/retrofit/CHANGELOG.md +++ b/retrofit/CHANGELOG.md @@ -1,35 +1,5 @@ # Changelog -## 4.4.2 - -- Introduced CallAdapters, This feature allows adaptation of a Call with return type R into the type of T. - e.g. Future to Future> - - Code Example: -```dart - class MyCallAdapter extends CallAdapter, Future>> { - @override - Future> adapt(Future Function() call) async { - try { - final response = await call(); - return Either.right(response); - } - catch (e) { - return Either.left(ApiError(e)) - } - } - } - - @RestApi() - abstract class RestClient { - factory RestClient(Dio dio, {String? baseUrl}) = _RestClient; - - @UseCallAdapter(MyCallAdapter) - @GET('/') - Future getTasks(); - } -``` - ## 4.4.0 - Added `@TypedExtras` to pass extra options to dio requests using custom annotations. diff --git a/retrofit/lib/call_adapter.dart b/retrofit/lib/call_adapter.dart deleted file mode 100644 index cdbf596a..00000000 --- a/retrofit/lib/call_adapter.dart +++ /dev/null @@ -1,53 +0,0 @@ -/// Adapts a Call with return type R into the type of T. -/// e.g. Future to Future> -abstract class CallAdapter { - T adapt(R Function() call); -} - - -/// By annotating a method with `@UseCallAdapter`, you can specify a custom adapter -/// class where you can adapt a call to another response wrapper -/// -/// ### Usage -/// -/// 1. Create the call adapter by extending [CallAdapter]: -/// pass in type parameters for the original call return type and adapted call return type. -/// Note: your adapter subclass must accept a single type parameter(T), where T is -/// the type of the unwrapped response from the original call. e.g. -/// "UserResponse" in "Future" -/// -/// ```dart -/// class ResultCallAdapter extends CallAdapter, Future>> { -/// @override -/// Future> adapt(Future Function() call) async { -/// try { -/// final response = await call(); -/// return Success(response); -/// } catch (e) { -/// return Error(e); -/// } -/// } -/// } - -/// ``` -/// -/// 2. Set the adapter on an API method or the entire API interface: -/// -/// - To apply the adapter to an individual method, use `@UseCallAdapter` on the method: -/// ```dart -/// @UseCallAdapter(ResultCallAdapter) -/// Future> fetchData(); -/// ``` -/// -/// - To apply it to all methods in an Api interface, pass the adapter to `@RestApi`: -/// ```dart -/// @RestApi(callAdapter: ResultCallAdapter) -/// abstract class MyApiService { -/// @GET('/data') -/// Future> fetchData(); -/// } -/// ``` -class UseCallAdapter { - const UseCallAdapter(this.callAdapter); - final Type callAdapter; -} diff --git a/retrofit/lib/http.dart b/retrofit/lib/http.dart index 59d0552a..91943ca9 100644 --- a/retrofit/lib/http.dart +++ b/retrofit/lib/http.dart @@ -67,7 +67,6 @@ class RestApi { const RestApi({ this.baseUrl, this.parser = Parser.JsonSerializable, - this.callAdapter, }); /// Set the API base URL. @@ -94,7 +93,6 @@ class RestApi { /// if you don't specify the [parser]. It will be [Parser.JsonSerializable] final Parser parser; - final Type? callAdapter; } @immutable diff --git a/retrofit/lib/retrofit.dart b/retrofit/lib/retrofit.dart index 3647e441..ca751735 100644 --- a/retrofit/lib/retrofit.dart +++ b/retrofit/lib/retrofit.dart @@ -1,4 +1,3 @@ export 'dio.dart'; export 'error_logger.dart'; export 'http.dart'; -export 'call_adapter.dart'; diff --git a/retrofit/pubspec.yaml b/retrofit/pubspec.yaml index a3d3f2c2..1b7103a1 100644 --- a/retrofit/pubspec.yaml +++ b/retrofit/pubspec.yaml @@ -8,7 +8,7 @@ topics: - rest - dio - retrofit -version: 4.4.2 +version: 4.4.0 environment: sdk: '>=2.19.0 <4.0.0'