From c3a2bfb36269ecf439a494e409dc56c4b3c1f719 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:39:21 +0000 Subject: [PATCH 01/12] Initial plan From 35fae0af0122ac482885f48a4796a3e335ed7158 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:50:40 +0000 Subject: [PATCH 02/12] Add onError parameter support to generated code Co-authored-by: trevorwang <121966+trevorwang@users.noreply.github.com> --- generator/lib/src/generator.dart | 88 +++++++++++++++++++++++++++----- 1 file changed, 74 insertions(+), 14 deletions(-) diff --git a/generator/lib/src/generator.dart b/generator/lib/src/generator.dart index fac0d8670..6cd32b8d2 100644 --- a/generator/lib/src/generator.dart +++ b/generator/lib/src/generator.dart @@ -61,6 +61,7 @@ class RetrofitGenerator extends GeneratorForAnnotation { static const _baseUrlVar = 'baseUrl'; static const _errorLoggerVar = 'errorLogger'; + static const _onErrorVar = 'onError'; static const _queryParamsVar = 'queryParameters'; static const _optionsVar = '_options'; static const _localHeadersVar = '_headers'; @@ -130,6 +131,7 @@ class RetrofitGenerator extends GeneratorForAnnotation { _buildDioField(), _buildBaseUrlField(baseUrl), _buildErrorLoggerFiled(), + _buildOnErrorField(), ]) ..constructors.addAll( annotateClassConsts.map( @@ -182,6 +184,14 @@ class RetrofitGenerator extends GeneratorForAnnotation { ..modifier = FieldModifier.final$; }); + /// Builds the onError field. + Field _buildOnErrorField() => Field((m) { + m + ..name = _onErrorVar + ..type = refer('Function?') + ..modifier = FieldModifier.final$; + }); + /// Generates the constructor. Constructor _generateConstructor( String? url, { @@ -207,6 +217,12 @@ class RetrofitGenerator extends GeneratorForAnnotation { ..name = _errorLoggerVar ..toThis = true, ), + Parameter( + (p) => p + ..named = true + ..name = _onErrorVar + ..toThis = true, + ), ]); if (superClassConst != null) { var superConstName = 'super'; @@ -354,20 +370,62 @@ class RetrofitGenerator extends GeneratorForAnnotation { : null; if (method.isAbstract) { methods.add(_generateApiCallMethod(method, instantiatedCallAdapter)!); - } - if (callAdapter != null) { - methods.add( - _generateAdapterMethod( - method, - instantiatedCallAdapter, - resultTypeInString, - ), - ); + // Always generate public wrapper for error handling + if (callAdapter != null) { + methods.add( + _generateAdapterMethod( + method, + instantiatedCallAdapter, + resultTypeInString, + ), + ); + } else { + methods.add(_generatePublicWrapperMethod(method)); + } } } return methods; } + /// Generates a public wrapper method that adds error handling. + Method _generatePublicWrapperMethod(MethodElement2 m) { + 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.formalParameters) { + 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()}'; + final privateCall = '_${m.displayName}($args)'; + methodBuilder.body = Code(''' + return ${_onErrorVar} != null + ? $privateCall.catchError($_onErrorVar) + : $privateCall; + '''); + }); + } + /// Generates a method implementation wrapped by CallAdapter. Method _generateAdapterMethod( MethodElement2 m, @@ -402,10 +460,11 @@ class RetrofitGenerator extends GeneratorForAnnotation { } final args = '${positionalArgs.map((e) => '$e,').join()} ${namedArgs.map((e) => '$e,').join()}'; + final adaptedCall = '${callAdapter?.element3.name3}<$resultType>().adapt(() => _${m.displayName}($args))'; methodBuilder.body = Code(''' - return ${callAdapter?.element3.name3}<$resultType>().adapt( - () => _${m.displayName}($args), - ); + return ${_onErrorVar} != null + ? $adaptedCall.catchError($_onErrorVar) + : $adaptedCall; '''); }); } @@ -747,15 +806,16 @@ class RetrofitGenerator extends GeneratorForAnnotation { } final returnType = m.returnType; + // Generate private method with underscore prefix return Method((methodBuilder) { _configureMethodMetadata( methodBuilder, m, _displayString(returnType, withNullability: true), - false, + true, // Mark as having CallAdapter (to add underscore prefix) ); _addParameters(methodBuilder, m); - _addAnnotations(methodBuilder, returnType, false); + _addAnnotations(methodBuilder, returnType, true); methodBuilder.body = _generateRequest(m, httpMethod, null); }); } From 3b58970e29d1a29b2d2ca46b6fb51180fc9d9892 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:53:23 +0000 Subject: [PATCH 03/12] Add tests and documentation for onError parameter Co-authored-by: trevorwang <121966+trevorwang@users.noreply.github.com> --- README.md | 31 +++++++++++++++ example/lib/example.dart | 1 + generator/test/src/generator_test_src.dart | 45 ++++++++++++++++++++++ 3 files changed, 77 insertions(+) diff --git a/README.md b/README.md index bef30db77..f0ba3874e 100644 --- a/README.md +++ b/README.md @@ -240,6 +240,37 @@ client.getTask('2').then((it) { }); ``` +#### Centralized Error Handling + +Instead of adding `.catchError()` to every API call, you can provide a centralized error handler when creating the client: + +```dart +void handleError(error) { + // Handle all API errors in one place + if (error is DioException) { + final res = error.response; + logger.e('API Error: ${res?.statusCode} -> ${res?.statusMessage}'); + } else { + logger.e('Unexpected error: $error'); + } +} + +// Create client with error handler +final client = RestClient(dio, onError: handleError); + +// All API calls will automatically use the error handler +client.getTask('2').then((it) { + logger.i(it); +}); +// No need to add .catchError() - errors are automatically handled! +``` + +This approach: +- Reduces boilerplate code by eliminating repetitive `.catchError()` calls +- Provides a single place to handle errors for all API methods +- Works with all exceptions, including `DioException`, data conversion errors, and `SocketException` +- Can be shared across multiple API clients + ### Relative API baseUrl If you want to use a relative `baseUrl` value in the `RestApi` annotation of the `RestClient`, you need to specify a `baseUrl` in `dio.options.baseUrl`. diff --git a/example/lib/example.dart b/example/lib/example.dart index fb578d7db..446e2b30a 100644 --- a/example/lib/example.dart +++ b/example/lib/example.dart @@ -14,6 +14,7 @@ abstract class RestClient { Dio dio, { String? baseUrl, ParseErrorLogger? errorLogger, + Function? onError, }) = RestClientYmlp; @GET('/tasks/{id}') diff --git a/generator/test/src/generator_test_src.dart b/generator/test/src/generator_test_src.dart index 77d12eb4e..383b3b258 100644 --- a/generator/test/src/generator_test_src.dart +++ b/generator/test/src/generator_test_src.dart @@ -2486,3 +2486,48 @@ abstract class UseResultForVoid { @GET('https://httpbin.org/') Future someGet(); } + +// Test onError parameter +@ShouldGenerate( + ''' + final Function? onError; + ''', + contains: true, +) +@RestApi() +abstract class OnErrorField { + @GET('/test') + Future getData(); +} + +@ShouldGenerate( + ''' + _TestOnErrorConstructor( + this._dio, { + this.baseUrl, + this.errorLogger, + this.onError, + }) + ''', + contains: true, +) +@RestApi() +abstract class TestOnErrorConstructor { + @GET('/test') + Future getData(); +} + +@ShouldGenerate( + ''' + @override + Future getData() { + return onError != null ? _getData().catchError(onError) : _getData(); + } + ''', + contains: true, +) +@RestApi() +abstract class TestOnErrorWrapping { + @GET('/test') + Future getData(); +} From 1b37a4469125e1599a72cd520f52826ed261e437 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:55:15 +0000 Subject: [PATCH 04/12] Add usage documentation in example Co-authored-by: trevorwang <121966+trevorwang@users.noreply.github.com> --- example/lib/example.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/example/lib/example.dart b/example/lib/example.dart index 446e2b30a..5fa664c4f 100644 --- a/example/lib/example.dart +++ b/example/lib/example.dart @@ -8,6 +8,19 @@ import 'package:retrofit_example/api_result.dart'; part 'example.g.dart'; +/// Example REST API client +/// +/// Usage with centralized error handling: +/// ```dart +/// void handleError(error) { +/// if (error is DioException) { +/// print('API Error: ${error.response?.statusCode}'); +/// } +/// } +/// +/// final client = RestClient(dio, onError: handleError); +/// // All API calls will automatically handle errors through handleError +/// ``` @RestApi(baseUrl: 'https://5d42a6e2bc64f90014a56ca0.mockapi.io/api/v1/') abstract class RestClient { factory RestClient( From 5e3e420fb85754e9e7c4606018875117137bc0fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 01:41:46 +0000 Subject: [PATCH 05/12] Fix test formatting to match generated code style Co-authored-by: trevorwang <121966+trevorwang@users.noreply.github.com> --- generator/lib/src/generator.dart | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/generator/lib/src/generator.dart b/generator/lib/src/generator.dart index 6cd32b8d2..064d32814 100644 --- a/generator/lib/src/generator.dart +++ b/generator/lib/src/generator.dart @@ -419,10 +419,8 @@ class RetrofitGenerator extends GeneratorForAnnotation { '${positionalArgs.map((e) => '$e,').join()} ${namedArgs.map((e) => '$e,').join()}'; final privateCall = '_${m.displayName}($args)'; methodBuilder.body = Code(''' - return ${_onErrorVar} != null - ? $privateCall.catchError($_onErrorVar) - : $privateCall; - '''); +return ${_onErrorVar} != null ? $privateCall.catchError($_onErrorVar) : $privateCall; +'''); }); } @@ -462,10 +460,8 @@ class RetrofitGenerator extends GeneratorForAnnotation { '${positionalArgs.map((e) => '$e,').join()} ${namedArgs.map((e) => '$e,').join()}'; final adaptedCall = '${callAdapter?.element3.name3}<$resultType>().adapt(() => _${m.displayName}($args))'; methodBuilder.body = Code(''' - return ${_onErrorVar} != null - ? $adaptedCall.catchError($_onErrorVar) - : $adaptedCall; - '''); +return ${_onErrorVar} != null ? $adaptedCall.catchError($_onErrorVar) : $adaptedCall; +'''); }); } From dcd5592cd85c0e1870e5dbb84b8f7a0e07bad87e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 02:01:12 +0000 Subject: [PATCH 06/12] Revert to simpler approach: only add onError field and constructor parameter Co-authored-by: trevorwang <121966+trevorwang@users.noreply.github.com> --- generator/lib/src/generator.dart | 70 +++++----------------- generator/test/src/generator_test_src.dart | 45 -------------- 2 files changed, 15 insertions(+), 100 deletions(-) diff --git a/generator/lib/src/generator.dart b/generator/lib/src/generator.dart index 064d32814..8cec5bf25 100644 --- a/generator/lib/src/generator.dart +++ b/generator/lib/src/generator.dart @@ -370,60 +370,20 @@ class RetrofitGenerator extends GeneratorForAnnotation { : null; if (method.isAbstract) { methods.add(_generateApiCallMethod(method, instantiatedCallAdapter)!); - // Always generate public wrapper for error handling - if (callAdapter != null) { - methods.add( - _generateAdapterMethod( - method, - instantiatedCallAdapter, - resultTypeInString, - ), - ); - } else { - methods.add(_generatePublicWrapperMethod(method)); - } + } + if (callAdapter != null) { + methods.add( + _generateAdapterMethod( + method, + instantiatedCallAdapter, + resultTypeInString, + ), + ); } } return methods; } - /// Generates a public wrapper method that adds error handling. - Method _generatePublicWrapperMethod(MethodElement2 m) { - 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.formalParameters) { - 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()}'; - final privateCall = '_${m.displayName}($args)'; - methodBuilder.body = Code(''' -return ${_onErrorVar} != null ? $privateCall.catchError($_onErrorVar) : $privateCall; -'''); - }); - } - /// Generates a method implementation wrapped by CallAdapter. Method _generateAdapterMethod( MethodElement2 m, @@ -458,10 +418,11 @@ return ${_onErrorVar} != null ? $privateCall.catchError($_onErrorVar) : $private } final args = '${positionalArgs.map((e) => '$e,').join()} ${namedArgs.map((e) => '$e,').join()}'; - final adaptedCall = '${callAdapter?.element3.name3}<$resultType>().adapt(() => _${m.displayName}($args))'; methodBuilder.body = Code(''' -return ${_onErrorVar} != null ? $adaptedCall.catchError($_onErrorVar) : $adaptedCall; -'''); + return ${callAdapter?.element3.name3}<$resultType>().adapt( + () => _${m.displayName}($args), + ); + '''); }); } @@ -802,16 +763,15 @@ return ${_onErrorVar} != null ? $adaptedCall.catchError($_onErrorVar) : $adapted } final returnType = m.returnType; - // Generate private method with underscore prefix return Method((methodBuilder) { _configureMethodMetadata( methodBuilder, m, _displayString(returnType, withNullability: true), - true, // Mark as having CallAdapter (to add underscore prefix) + false, ); _addParameters(methodBuilder, m); - _addAnnotations(methodBuilder, returnType, true); + _addAnnotations(methodBuilder, returnType, false); methodBuilder.body = _generateRequest(m, httpMethod, null); }); } diff --git a/generator/test/src/generator_test_src.dart b/generator/test/src/generator_test_src.dart index 383b3b258..77d12eb4e 100644 --- a/generator/test/src/generator_test_src.dart +++ b/generator/test/src/generator_test_src.dart @@ -2486,48 +2486,3 @@ abstract class UseResultForVoid { @GET('https://httpbin.org/') Future someGet(); } - -// Test onError parameter -@ShouldGenerate( - ''' - final Function? onError; - ''', - contains: true, -) -@RestApi() -abstract class OnErrorField { - @GET('/test') - Future getData(); -} - -@ShouldGenerate( - ''' - _TestOnErrorConstructor( - this._dio, { - this.baseUrl, - this.errorLogger, - this.onError, - }) - ''', - contains: true, -) -@RestApi() -abstract class TestOnErrorConstructor { - @GET('/test') - Future getData(); -} - -@ShouldGenerate( - ''' - @override - Future getData() { - return onError != null ? _getData().catchError(onError) : _getData(); - } - ''', - contains: true, -) -@RestApi() -abstract class TestOnErrorWrapping { - @GET('/test') - Future getData(); -} From 04cac0f5e98c15765a711789290d59e5fc9f9038 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 02:03:02 +0000 Subject: [PATCH 07/12] Add onError catchError wrapping to return statements Co-authored-by: trevorwang <121966+trevorwang@users.noreply.github.com> --- generator/lib/src/generator.dart | 50 ++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/generator/lib/src/generator.dart b/generator/lib/src/generator.dart index 8cec5bf25..2ef01b760 100644 --- a/generator/lib/src/generator.dart +++ b/generator/lib/src/generator.dart @@ -418,10 +418,9 @@ class RetrofitGenerator extends GeneratorForAnnotation { } final args = '${positionalArgs.map((e) => '$e,').join()} ${namedArgs.map((e) => '$e,').join()}'; + final adaptedCall = '${callAdapter?.element3.name3}<$resultType>().adapt(() => _${m.displayName}($args))'; methodBuilder.body = Code(''' - return ${callAdapter?.element3.name3}<$resultType>().adapt( - () => _${m.displayName}($args), - ); + return $_onErrorVar != null ? $adaptedCall.catchError($_onErrorVar) : $adaptedCall; '''); }); } @@ -973,18 +972,32 @@ class RetrofitGenerator extends GeneratorForAnnotation { : wrappedReturnType; if (returnType == null || 'void' == returnType.toString()) { if (isWrappedWithHttpResponseWrapper) { - blocks - ..add( + if (returnAsyncWrapper == 'return') { + blocks.add( refer( 'final $_resultVar = await $_dioVar.fetch', ).call([options], {}, [refer('void')]).statement, - ) - ..add( + ); + blocks.add( Code(''' final httpResponse = HttpResponse(null, $_resultVar); -$returnAsyncWrapper httpResponse; +return $_onErrorVar != null ? Future.value(httpResponse).catchError($_onErrorVar) : httpResponse; '''), ); + } else { + blocks + ..add( + refer( + 'final $_resultVar = await $_dioVar.fetch', + ).call([options], {}, [refer('void')]).statement, + ) + ..add( + Code(''' +final httpResponse = HttpResponse(null, $_resultVar); +$returnAsyncWrapper httpResponse; +'''), + ); + } } else { blocks.add( refer( @@ -1410,14 +1423,27 @@ You should create a new class to encapsulate the response. } } if (isWrappedWithHttpResponseWrapper) { - blocks.add( - Code(''' + if (returnAsyncWrapper == 'return') { + blocks.add( + Code(''' +final httpResponse = HttpResponse($_valueVar, $_resultVar); +return $_onErrorVar != null ? Future.value(httpResponse).catchError($_onErrorVar) : httpResponse; +'''), + ); + } else { + blocks.add( + Code(''' final httpResponse = HttpResponse($_valueVar, $_resultVar); $returnAsyncWrapper httpResponse; '''), - ); + ); + } } else { - blocks.add(Code('$returnAsyncWrapper $_valueVar;')); + if (returnAsyncWrapper == 'return') { + blocks.add(Code('return $_onErrorVar != null ? Future.value($_valueVar).catchError($_onErrorVar) : $_valueVar;')); + } else { + blocks.add(Code('$returnAsyncWrapper $_valueVar;')); + } } } From 58a38a45eb5ee65c216ad43116d14c3d887d7c6c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 02:04:11 +0000 Subject: [PATCH 08/12] Add test cases for onError functionality Co-authored-by: trevorwang <121966+trevorwang@users.noreply.github.com> --- generator/test/src/generator_test_src.dart | 37 ++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/generator/test/src/generator_test_src.dart b/generator/test/src/generator_test_src.dart index 77d12eb4e..75c650994 100644 --- a/generator/test/src/generator_test_src.dart +++ b/generator/test/src/generator_test_src.dart @@ -2486,3 +2486,40 @@ abstract class UseResultForVoid { @GET('https://httpbin.org/') Future someGet(); } + +// Test onError parameter +@ShouldGenerate( + ''' + final Function? onError; + ''', + contains: true, +) +@RestApi() +abstract class OnErrorField { + @GET('/test') + Future getData(); +} + +@ShouldGenerate( + ''' + _TestOnErrorConstructor(this._dio, {this.baseUrl, this.errorLogger, this.onError}); + ''', + contains: true, +) +@RestApi() +abstract class TestOnErrorConstructor { + @GET('/test') + Future getData(); +} + +@ShouldGenerate( + ''' + return onError != null ? Future.value(_value).catchError(onError) : _value; + ''', + contains: true, +) +@RestApi() +abstract class TestOnErrorWrapping { + @GET('/test') + Future getData(); +} From 28c01bb3be78d4fd3cb54cf5d7cd7ca37cfd316a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Oct 2025 05:17:24 +0000 Subject: [PATCH 09/12] Update test expectations to match onError wrapping implementation Co-authored-by: trevorwang <121966+trevorwang@users.noreply.github.com> --- generator/test/src/generator_test_src.dart | 42 +++++++++++----------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/generator/test/src/generator_test_src.dart b/generator/test/src/generator_test_src.dart index 75c650994..542f4dfa8 100644 --- a/generator/test/src/generator_test_src.dart +++ b/generator/test/src/generator_test_src.dart @@ -18,7 +18,7 @@ class MockCallAdapter1 extends CallAdapter, Future>> { @ShouldGenerate(''' @override Future>> getUser() { - return MockCallAdapter1>().adapt(() => _getUser()); + return onError != null ? MockCallAdapter1>().adapt(() => _getUser()).catchError(onError) : MockCallAdapter1>().adapt(() => _getUser()); } ''', contains: true) @RestApi() @@ -41,7 +41,7 @@ class MockCallAdapter2 @ShouldGenerate(''' @override Future> getUser() { - return MockCallAdapter2().adapt(() => _getUser()); + return onError != null ? MockCallAdapter2().adapt(() => _getUser()).catchError(onError) : MockCallAdapter2().adapt(() => _getUser()); } ''', contains: true) @RestApi() @@ -84,7 +84,7 @@ class MockCallAdapter3 extends CallAdapter, Flow> { errorLogger?.logError(e, s, _options); rethrow; } - return _value; + return onError != null ? Future.value(_value).catchError(onError) : _value; } ''', contains: true) @RestApi() @@ -549,7 +549,7 @@ abstract class UploadFileInfoPartTest { errorLogger?.logError(e, s, _options); rethrow; } - return _value; + return onError != null ? Future.value(_value).catchError(onError) : _value; ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') abstract class GenericCast { @@ -565,7 +565,7 @@ abstract class GenericCast { errorLogger?.logError(e, s, _options); rethrow; } - return _value; + return onError != null ? Future.value(_value).catchError(onError) : _value; ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') abstract class NullableGenericCast { @@ -642,7 +642,7 @@ enum FromJsonEnum { errorLogger?.logError(e, s, _options); rethrow; } - return _value; + return onError != null ? Future.value(_value).catchError(onError) : _value; ''', contains: true) @RestApi() abstract class EnumFromJsonReturnType { @@ -745,7 +745,7 @@ Map serializeUser(User object) => object.toJson(); errorLogger?.logError(e, s, _options); rethrow; } - return _value; + return onError != null ? Future.value(_value).catchError(onError) : _value; ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') abstract class GenericCastBasicType { @@ -761,7 +761,7 @@ abstract class GenericCastBasicType { errorLogger?.logError(e, s, _options); rethrow; } - return _value; + return onError != null ? Future.value(_value).catchError(onError) : _value; ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') abstract class NullableGenericCastBasicType { @@ -974,7 +974,7 @@ abstract class TestCustomObjectBody { errorLogger?.logError(e, s, _options); rethrow; } - return _value; + return onError != null ? Future.value(_value).catchError(onError) : _value; } ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') @@ -998,7 +998,7 @@ abstract class TestMapBody { errorLogger?.logError(e, s, _options); rethrow; } - return _value; + return onError != null ? Future.value(_value).catchError(onError) : _value; } ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') @@ -1017,7 +1017,7 @@ abstract class NullableTestMapBody { errorLogger?.logError(e, s, _options); rethrow; } - return _value; + return onError != null ? Future.value(_value).catchError(onError) : _value; } ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') @@ -1036,7 +1036,7 @@ abstract class TestMapBody2 { errorLogger?.logError(e, s, _options); rethrow; } - return _value; + return onError != null ? Future.value(_value).catchError(onError) : _value; } ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') @@ -1053,7 +1053,7 @@ abstract class NullableTestMapBody2 { errorLogger?.logError(e, s, _options); rethrow; } - return _value; + return onError != null ? Future.value(_value).catchError(onError) : _value; ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') abstract class TestBasicListString { @@ -1069,7 +1069,7 @@ abstract class TestBasicListString { errorLogger?.logError(e, s, _options); rethrow; } - return _value; + return onError != null ? Future.value(_value).catchError(onError) : _value; ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') abstract class NullableTestBasicListString { @@ -1085,7 +1085,7 @@ abstract class NullableTestBasicListString { errorLogger?.logError(e, s, _options); rethrow; } - return _value; + return onError != null ? Future.value(_value).catchError(onError) : _value; ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') abstract class TestBasicListBool { @@ -1101,7 +1101,7 @@ abstract class TestBasicListBool { errorLogger?.logError(e, s, _options); rethrow; } - return _value; + return onError != null ? Future.value(_value).catchError(onError) : _value; ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') abstract class NullableTestBasicListBool { @@ -1117,7 +1117,7 @@ abstract class NullableTestBasicListBool { errorLogger?.logError(e, s, _options); rethrow; } - return _value; + return onError != null ? Future.value(_value).catchError(onError) : _value; ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') abstract class TestBasicListInt { @@ -1133,7 +1133,7 @@ abstract class TestBasicListInt { errorLogger?.logError(e, s, _options); rethrow; } - return _value; + return onError != null ? Future.value(_value).catchError(onError) : _value; ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') abstract class NullableTestBasicListInt { @@ -1149,7 +1149,7 @@ abstract class NullableTestBasicListInt { errorLogger?.logError(e, s, _options); rethrow; } - return _value; + return onError != null ? Future.value(_value).catchError(onError) : _value; ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') abstract class TestBasicListDouble { @@ -1165,7 +1165,7 @@ abstract class TestBasicListDouble { errorLogger?.logError(e, s, _options); rethrow; } - return _value; + return onError != null ? Future.value(_value).catchError(onError) : _value; ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') abstract class NullableTestBasicListDouble { @@ -1248,7 +1248,7 @@ abstract class TestHttpResponseObject { rethrow; } final httpResponse = HttpResponse(_value, _result); - return httpResponse; + return onError != null ? Future.value(httpResponse).catchError(onError) : httpResponse; ''', contains: true) @RestApi() abstract class TestHttpResponseArray { From 818908dbdec21c3a72b279ce322f80c4ca953a10 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Oct 2025 05:50:52 +0000 Subject: [PATCH 10/12] Fix ternary operator type mismatch by using Future.value in both branches Co-authored-by: trevorwang <121966+trevorwang@users.noreply.github.com> --- generator/lib/src/generator.dart | 6 ++-- generator/test/src/generator_test_src.dart | 40 +++++++++++----------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/generator/lib/src/generator.dart b/generator/lib/src/generator.dart index 2ef01b760..34b8b8eed 100644 --- a/generator/lib/src/generator.dart +++ b/generator/lib/src/generator.dart @@ -981,7 +981,7 @@ class RetrofitGenerator extends GeneratorForAnnotation { blocks.add( Code(''' final httpResponse = HttpResponse(null, $_resultVar); -return $_onErrorVar != null ? Future.value(httpResponse).catchError($_onErrorVar) : httpResponse; +return $_onErrorVar != null ? Future.value(httpResponse).catchError($_onErrorVar) : Future.value(httpResponse); '''), ); } else { @@ -1427,7 +1427,7 @@ You should create a new class to encapsulate the response. blocks.add( Code(''' final httpResponse = HttpResponse($_valueVar, $_resultVar); -return $_onErrorVar != null ? Future.value(httpResponse).catchError($_onErrorVar) : httpResponse; +return $_onErrorVar != null ? Future.value(httpResponse).catchError($_onErrorVar) : Future.value(httpResponse); '''), ); } else { @@ -1440,7 +1440,7 @@ $returnAsyncWrapper httpResponse; } } else { if (returnAsyncWrapper == 'return') { - blocks.add(Code('return $_onErrorVar != null ? Future.value($_valueVar).catchError($_onErrorVar) : $_valueVar;')); + blocks.add(Code('return $_onErrorVar != null ? Future.value($_valueVar).catchError($_onErrorVar) : Future.value($_valueVar);')); } else { blocks.add(Code('$returnAsyncWrapper $_valueVar;')); } diff --git a/generator/test/src/generator_test_src.dart b/generator/test/src/generator_test_src.dart index 542f4dfa8..5eeb08a6a 100644 --- a/generator/test/src/generator_test_src.dart +++ b/generator/test/src/generator_test_src.dart @@ -84,7 +84,7 @@ class MockCallAdapter3 extends CallAdapter, Flow> { errorLogger?.logError(e, s, _options); rethrow; } - return onError != null ? Future.value(_value).catchError(onError) : _value; + return onError != null ? Future.value(_value).catchError(onError) : Future.value(_value); } ''', contains: true) @RestApi() @@ -549,7 +549,7 @@ abstract class UploadFileInfoPartTest { errorLogger?.logError(e, s, _options); rethrow; } - return onError != null ? Future.value(_value).catchError(onError) : _value; + return onError != null ? Future.value(_value).catchError(onError) : Future.value(_value); ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') abstract class GenericCast { @@ -565,7 +565,7 @@ abstract class GenericCast { errorLogger?.logError(e, s, _options); rethrow; } - return onError != null ? Future.value(_value).catchError(onError) : _value; + return onError != null ? Future.value(_value).catchError(onError) : Future.value(_value); ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') abstract class NullableGenericCast { @@ -642,7 +642,7 @@ enum FromJsonEnum { errorLogger?.logError(e, s, _options); rethrow; } - return onError != null ? Future.value(_value).catchError(onError) : _value; + return onError != null ? Future.value(_value).catchError(onError) : Future.value(_value); ''', contains: true) @RestApi() abstract class EnumFromJsonReturnType { @@ -745,7 +745,7 @@ Map serializeUser(User object) => object.toJson(); errorLogger?.logError(e, s, _options); rethrow; } - return onError != null ? Future.value(_value).catchError(onError) : _value; + return onError != null ? Future.value(_value).catchError(onError) : Future.value(_value); ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') abstract class GenericCastBasicType { @@ -761,7 +761,7 @@ abstract class GenericCastBasicType { errorLogger?.logError(e, s, _options); rethrow; } - return onError != null ? Future.value(_value).catchError(onError) : _value; + return onError != null ? Future.value(_value).catchError(onError) : Future.value(_value); ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') abstract class NullableGenericCastBasicType { @@ -974,7 +974,7 @@ abstract class TestCustomObjectBody { errorLogger?.logError(e, s, _options); rethrow; } - return onError != null ? Future.value(_value).catchError(onError) : _value; + return onError != null ? Future.value(_value).catchError(onError) : Future.value(_value); } ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') @@ -998,7 +998,7 @@ abstract class TestMapBody { errorLogger?.logError(e, s, _options); rethrow; } - return onError != null ? Future.value(_value).catchError(onError) : _value; + return onError != null ? Future.value(_value).catchError(onError) : Future.value(_value); } ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') @@ -1017,7 +1017,7 @@ abstract class NullableTestMapBody { errorLogger?.logError(e, s, _options); rethrow; } - return onError != null ? Future.value(_value).catchError(onError) : _value; + return onError != null ? Future.value(_value).catchError(onError) : Future.value(_value); } ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') @@ -1036,7 +1036,7 @@ abstract class TestMapBody2 { errorLogger?.logError(e, s, _options); rethrow; } - return onError != null ? Future.value(_value).catchError(onError) : _value; + return onError != null ? Future.value(_value).catchError(onError) : Future.value(_value); } ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') @@ -1053,7 +1053,7 @@ abstract class NullableTestMapBody2 { errorLogger?.logError(e, s, _options); rethrow; } - return onError != null ? Future.value(_value).catchError(onError) : _value; + return onError != null ? Future.value(_value).catchError(onError) : Future.value(_value); ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') abstract class TestBasicListString { @@ -1069,7 +1069,7 @@ abstract class TestBasicListString { errorLogger?.logError(e, s, _options); rethrow; } - return onError != null ? Future.value(_value).catchError(onError) : _value; + return onError != null ? Future.value(_value).catchError(onError) : Future.value(_value); ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') abstract class NullableTestBasicListString { @@ -1085,7 +1085,7 @@ abstract class NullableTestBasicListString { errorLogger?.logError(e, s, _options); rethrow; } - return onError != null ? Future.value(_value).catchError(onError) : _value; + return onError != null ? Future.value(_value).catchError(onError) : Future.value(_value); ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') abstract class TestBasicListBool { @@ -1101,7 +1101,7 @@ abstract class TestBasicListBool { errorLogger?.logError(e, s, _options); rethrow; } - return onError != null ? Future.value(_value).catchError(onError) : _value; + return onError != null ? Future.value(_value).catchError(onError) : Future.value(_value); ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') abstract class NullableTestBasicListBool { @@ -1117,7 +1117,7 @@ abstract class NullableTestBasicListBool { errorLogger?.logError(e, s, _options); rethrow; } - return onError != null ? Future.value(_value).catchError(onError) : _value; + return onError != null ? Future.value(_value).catchError(onError) : Future.value(_value); ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') abstract class TestBasicListInt { @@ -1133,7 +1133,7 @@ abstract class TestBasicListInt { errorLogger?.logError(e, s, _options); rethrow; } - return onError != null ? Future.value(_value).catchError(onError) : _value; + return onError != null ? Future.value(_value).catchError(onError) : Future.value(_value); ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') abstract class NullableTestBasicListInt { @@ -1149,7 +1149,7 @@ abstract class NullableTestBasicListInt { errorLogger?.logError(e, s, _options); rethrow; } - return onError != null ? Future.value(_value).catchError(onError) : _value; + return onError != null ? Future.value(_value).catchError(onError) : Future.value(_value); ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') abstract class TestBasicListDouble { @@ -1165,7 +1165,7 @@ abstract class TestBasicListDouble { errorLogger?.logError(e, s, _options); rethrow; } - return onError != null ? Future.value(_value).catchError(onError) : _value; + return onError != null ? Future.value(_value).catchError(onError) : Future.value(_value); ''', contains: true) @RestApi(baseUrl: 'https://httpbin.org/') abstract class NullableTestBasicListDouble { @@ -1248,7 +1248,7 @@ abstract class TestHttpResponseObject { rethrow; } final httpResponse = HttpResponse(_value, _result); - return onError != null ? Future.value(httpResponse).catchError(onError) : httpResponse; + return onError != null ? Future.value(httpResponse).catchError(onError) : Future.value(httpResponse); ''', contains: true) @RestApi() abstract class TestHttpResponseArray { @@ -2514,7 +2514,7 @@ abstract class TestOnErrorConstructor { @ShouldGenerate( ''' - return onError != null ? Future.value(_value).catchError(onError) : _value; + return onError != null ? Future.value(_value).catchError(onError) : Future.value(_value); ''', contains: true, ) From ac376bbe8498e93ea537ba955f1c216c5dc755f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Oct 2025 08:30:01 +0000 Subject: [PATCH 11/12] Merge master branch and resolve conflicts Co-authored-by: trevorwang <121966+trevorwang@users.noreply.github.com> --- README.md | 51 ++++++ example/LEAN_BUILDER.md | 106 ++++++++++++ generator/CHANGELOG.md | 8 + generator/analysis_options.yaml | 6 + generator/lib/lean_builder.dart | 66 +++++++ generator/lib/src/generator.dart | 90 +++++++--- generator/lib/src/lean_generator.dart | 46 +++++ generator/test/src/generator_test_src.dart | 189 ++++++++++++++++++++- retrofit/lib/http.dart | 23 +++ 9 files changed, 556 insertions(+), 29 deletions(-) create mode 100644 example/LEAN_BUILDER.md create mode 100644 generator/lib/lean_builder.dart create mode 100644 generator/lib/src/lean_generator.dart diff --git a/README.md b/README.md index f0ba3874e..82356bb8a 100644 --- a/README.md +++ b/README.md @@ -64,8 +64,26 @@ then run the generator ```sh # dart dart pub run build_runner build + +# for watch mode (recommended during development) +dart pub run build_runner watch +``` + +#### Lean Builder Support (Experimental) + +Retrofit now has experimental support for [lean_builder](https://pub.dev/packages/lean_builder), a faster build system for Dart. While lean_builder support is still under development, the infrastructure has been added for future use. + +**Important**: lean_builder is an **optional** dependency and is NOT required to use retrofit_generator. It's only needed if you want to try the experimental lean_builder support. + +To prepare for lean_builder support, add it to your `dev_dependencies`: + +```yaml +dev_dependencies: + lean_builder: ^0.1.2 # Optional - only if you want to use lean_builder ``` +**Note:** For now, please continue using `build_runner` as shown above. Full lean_builder integration will be available in a future release once lean_builder reaches stability. + ### Use it ```dart @@ -217,6 +235,39 @@ The HTTP methods in the below sample are supported. Future getTasks(); ``` +* Add global HTTP headers to all requests in the API + +You can define headers at the `@RestApi` level that will be automatically included in all requests: + +```dart + @RestApi( + baseUrl: 'https://api.example.com', + headers: { + 'User-Agent': 'MyApp/1.0.0', + 'X-Platform': 'mobile', + }, + ) + abstract class ApiService { + factory ApiService(Dio dio, {String? baseUrl}) = _ApiService; + + // This request will automatically include User-Agent and X-Platform headers + @GET('/users') + Future> getUsers(); + + // You can add method-specific headers that combine with global headers + @GET('/profile') + @Headers({'Authorization': 'Bearer token'}) + Future getProfile(); + + // Method-level headers override global headers with the same key + @GET('/settings') + @Headers({'X-Platform': 'web'}) + Future getSettings(); + } +``` + +**Note:** Method-level headers (via `@Headers` or `@Header` parameter) will override global headers if they have the same key. + ### Error Handling diff --git a/example/LEAN_BUILDER.md b/example/LEAN_BUILDER.md new file mode 100644 index 000000000..9c5d9f481 --- /dev/null +++ b/example/LEAN_BUILDER.md @@ -0,0 +1,106 @@ +# Lean Builder Support (Experimental) + +This document provides information about using lean_builder with retrofit_generator. + +## Status + +⚠️ **Experimental** - Lean Builder support is currently under development and not yet recommended for production use. + +## What is Lean Builder? + +[Lean Builder](https://pub.dev/packages/lean_builder) is a streamlined Dart build system that offers: +- Fast incremental builds (often under 1 second) +- Parallel processing for maximum efficiency +- Watch mode with hot reload support +- Simple, declarative builder configuration + +## Current Implementation + +The infrastructure for lean_builder support has been added to retrofit_generator, but the full implementation is pending until: +1. Lean Builder reaches a stable release (currently at v0.1.2) +2. The retrofit_generator codebase is fully adapted to lean_builder's API + +## Using build_runner (Recommended) + +For now, please continue using build_runner for code generation: + +```bash +# One-time build +dart pub run build_runner build + +# Watch mode (recommended during development) +dart pub run build_runner watch --delete-conflicting-outputs +``` + +## Future Usage (When Available) + +Once lean_builder support is fully implemented, you'll be able to use: + +```bash +# One-time build +dart run lean_builder build + +# Watch mode with hot reload +dart run lean_builder watch --dev +``` + +## Configuration + +### pubspec.yaml + +**Important**: lean_builder is an **optional** dependency. It is NOT included in retrofit_generator by default. + +When lean_builder support is ready, your `pubspec.yaml` will include: + +```yaml +dependencies: + retrofit: ^4.6.0 + dio: ^5.0.0 + json_annotation: ^4.9.0 + retrofit_generator: ^10.0.0 # For use in codegen folder + +dev_dependencies: + lean_builder: ^0.1.2 # Optional - only if you want to use lean_builder + json_serializable: ^6.10.0 +``` + +Note: retrofit_generator does not depend on lean_builder, so you won't be forced to install it unless you explicitly want to use lean_builder support. + +### build.yaml (Optional) + +You can keep your existing `build.yaml` configuration. Lean Builder will respect these settings when support is fully implemented: + +```yaml +targets: + $default: + builders: + retrofit_generator: + options: + auto_cast_response: true +``` + +## Migration Path + +When lean_builder support becomes stable: + +1. **No code changes required** - Your @RestApi annotations and API definitions remain the same +2. **Update dependencies** - Add lean_builder to dev_dependencies +3. **Switch build command** - Use `dart run lean_builder build` instead of `build_runner` +4. **Enjoy faster builds** - Experience significantly faster incremental builds + +## Contributing + +If you're interested in helping complete the lean_builder integration, please: +1. Check the [retrofit.dart GitHub repository](https://github.com/trevorwang/retrofit.dart) +2. Review the [lean_builder documentation](https://pub.dev/packages/lean_builder) +3. Submit a pull request or open an issue with your ideas + +## Support + +For questions or issues: +- **retrofit.dart**: https://github.com/trevorwang/retrofit.dart/issues +- **lean_builder**: https://github.com/Milad-Akarie/lean_builder/issues + +## Timeline + +Follow the retrofit.dart repository for updates on when lean_builder support will be fully available. The maintainers are monitoring lean_builder's development and will complete the integration when appropriate. diff --git a/generator/CHANGELOG.md b/generator/CHANGELOG.md index 0eb95ed79..02adf6c0a 100644 --- a/generator/CHANGELOG.md +++ b/generator/CHANGELOG.md @@ -1,3 +1,11 @@ +## 10.0.8 (Unreleased) + +- Add experimental lean_builder support infrastructure +- Add `lib/lean_builder.dart` entry point for lean_builder users +- Add comprehensive documentation for lean_builder support +- Note: lean_builder is now an **optional** dependency - it's not required unless you want to use lean_builder +- Note: Full lean_builder implementation is pending until lean_builder reaches stability + ## 10.0.6 - Update `protobuf` to 5.0.0 diff --git a/generator/analysis_options.yaml b/generator/analysis_options.yaml index 011741769..82d058c03 100644 --- a/generator/analysis_options.yaml +++ b/generator/analysis_options.yaml @@ -1,3 +1,9 @@ include: package:lints/recommended.yaml formatter: page_width: 80 +analyzer: + exclude: + # Exclude lean_builder support files as lean_builder is an optional dependency + # These files are only used when users explicitly add lean_builder to their project + - lib/lean_builder.dart + - lib/src/lean_generator.dart diff --git a/generator/lib/lean_builder.dart b/generator/lib/lean_builder.dart new file mode 100644 index 000000000..8ddcb4904 --- /dev/null +++ b/generator/lib/lean_builder.dart @@ -0,0 +1,66 @@ +/// Lean Builder entry point for Retrofit code generator. +/// +/// **⚠️ Experimental Feature ⚠️** +/// +/// This file provides experimental lean_builder support for retrofit_generator. +/// Lean Builder support is currently under development and not yet recommended +/// for production use. +/// +/// ## Important: Optional Dependency +/// +/// This library is **NOT** included in retrofit_generator's dependencies. +/// To use lean_builder support, you must add it to your own project: +/// +/// ```yaml +/// dependencies: +/// retrofit_generator: +/// +/// dev_dependencies: +/// lean_builder: ^0.1.2 +/// ``` +/// +/// Then, in your codegen folder (or where you define generators), import: +/// ```dart +/// import 'package:retrofit_generator/lean_builder.dart'; +/// ``` +/// +/// ## Current Status +/// +/// While the infrastructure for lean_builder support has been added, the +/// retrofit_generator package is currently optimized for build_runner. Full +/// lean_builder integration is planned for future releases once lean_builder +/// reaches a stable version. +/// +/// ## Usage (Experimental) +/// +/// 1. Add both packages to your pubspec.yaml: +/// ```yaml +/// dependencies: +/// retrofit_generator: +/// +/// dev_dependencies: +/// lean_builder: ^0.1.2 +/// ``` +/// +/// 2. For now, please continue using build_runner: +/// ```bash +/// dart pub run build_runner build +/// # or for watch mode: +/// dart pub run build_runner watch +/// ``` +/// +/// ## Future Plans +/// +/// Once lean_builder reaches stability and this integration is complete, you'll +/// be able to use: +/// ```bash +/// dart run lean_builder build +/// # or for watch mode with hot reload: +/// dart run lean_builder watch --dev +/// ``` +/// +/// For updates on lean_builder support, visit: +/// https://github.com/trevorwang/retrofit.dart +library; + +export 'package:retrofit_generator/src/lean_generator.dart'; diff --git a/generator/lib/src/generator.dart b/generator/lib/src/generator.dart index 34b8b8eed..75cf8b14a 100644 --- a/generator/lib/src/generator.dart +++ b/generator/lib/src/generator.dart @@ -17,7 +17,7 @@ import 'package:retrofit/retrofit.dart' as retrofit; import 'package:source_gen/source_gen.dart'; const _analyzerIgnores = - '// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations,unused_element_parameter'; + '// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations,unused_element_parameter,avoid_unused_constructor_parameters,unreachable_from_main'; /// Factory for the Retrofit code generator used by build_runner. Builder generatorFactoryBuilder(BuilderOptions options) => SharedPartBuilder( @@ -107,14 +107,34 @@ class RetrofitGenerator extends GeneratorForAnnotation { /// Generates the implementation class code as a string. String _implementClass(ClassElement2 element, ConstantReader annotation) { + // Reset hasCustomOptions for each class to avoid state leaking between classes + hasCustomOptions = false; final className = globalOptions.className ?? '_${element.name3}'; final enumString = annotation.peek('parser')?.revive().accessor; final parser = retrofit.Parser.values.firstWhereOrNull( (e) => e.toString() == enumString, ); + final headersMap = annotation.peek('headers')?.mapValue.map((k, v) { + dynamic val; + if (v == null) { + val = null; + } else if (v.type?.isDartCoreBool ?? false) { + val = v.toBoolValue(); + } else if (v.type?.isDartCoreString ?? false) { + val = v.toStringValue(); + } else if (v.type?.isDartCoreDouble ?? false) { + val = v.toDoubleValue(); + } else if (v.type?.isDartCoreInt ?? false) { + val = v.toIntValue(); + } else { + val = v.toStringValue(); + } + return MapEntry(k?.toStringValue() ?? 'null', val); + }); clientAnnotation = retrofit.RestApi( baseUrl: annotation.peek(_baseUrlVar)?.stringValue ?? '', parser: parser ?? retrofit.Parser.JsonSerializable, + headers: headersMap, ); clientAnnotationConstantReader = annotation; final baseUrl = clientAnnotation.baseUrl; @@ -479,8 +499,13 @@ class RetrofitGenerator extends GeneratorForAnnotation { _isInterfaceType(x) && _typeChecker(t).isAssignableFromType(x!); /// `_typeChecker(T).isSuperTypeOf(x)` - bool _isSuperOf(Type t, DartType? x) => - _isInterfaceType(x) && _typeChecker(t).isSuperTypeOf(x!); + bool _isSuperOf(Type t, DartType? x) { + // Object is the root of the type hierarchy, nothing can be its supertype + if (x?.isDartCoreObject ?? false) { + return false; + } + return _isInterfaceType(x) && _typeChecker(t).isSuperTypeOf(x!); + } /// Gets a type checker for the given type. TypeChecker _typeChecker(Type type) { @@ -1553,7 +1578,7 @@ $returnAsyncWrapper httpResponse; mapperVal = ''' (json) => json is List - ? json.map<$genericTypeString>((i) => ${genericTypeString == 'dynamic' ? ' i as Map' : '$genericTypeString.fromJson(i as Map)'}).toList() + ? json.map<$genericTypeString>((i) => ${genericTypeString == 'dynamic' ? 'i' : '$genericTypeString.fromJson(i as Map)'}).toList() : List.empty(), '''; } @@ -1913,6 +1938,20 @@ if (T != dynamic && return dartType.element3.getMethod2('toJson') != null; } + /// Gets the expression for serializing an enum value in FormData as a string. + /// Uses toJson() if available, otherwise uses .name. + String _getEnumValueExpression(DartType enumType, String variableName) { + return _hasToJson(enumType) ? '$variableName.toJson()' : '$variableName.name'; + } + + /// Gets the Reference for serializing an enum value in FormData. + /// Uses toJson() if available, otherwise uses .name. + Expression _getEnumValueReference(DartType enumType, String variableName) { + return _hasToJson(enumType) + ? refer(variableName).property('toJson').call([]) + : refer(variableName).property('name'); + } + /// Generates the query parameters code block. void _generateQueries( MethodElement2 m, @@ -2368,9 +2407,6 @@ if (T != dynamic && final contentType = r.peek('contentType')?.stringValue; if (isFileField) { - if (p.type.isNullable) { - blocks.add(Code('if (${p.displayName} != null){')); - } final fileNameValue = r.peek('fileName')?.stringValue; final fileName = fileNameValue != null ? literalString(fileNameValue) @@ -2405,7 +2441,8 @@ if (T != dynamic && ).newInstance([literal(fieldName), uploadFileInfo]), ]) .statement; - if (optionalFile) { + // Add null check if parameter is nullable OR optional + if (p.type.isNullable || optionalFile) { final condition = refer(p.displayName).notEqualTo(literalNull).code; blocks.addAll([ const Code('if('), @@ -2417,9 +2454,6 @@ if (T != dynamic && } else { blocks.add(returnCode); } - if (p.type.isNullable) { - blocks.add(const Code('}')); - } } else if (_isMultipartFile(p.type)) { if (p.type.isNullable) { blocks.add(Code('if (${p.displayName} != null){')); @@ -2497,7 +2531,7 @@ MultipartFile.fromBytes(i, _isExactly(BuiltList, innerType)))) { var value = ''; if (innerType != null && _isEnum(innerType)) { - value = 'i'; + value = _getEnumValueExpression(innerType, 'i'); } else if (_isBasicType(innerType)) { value = 'i'; if (innerType != null && !_isExactly(String, innerType)) { @@ -2595,16 +2629,7 @@ MultipartFile.fromFileSync(i.path, if (_isExactly(String, p.type)) refer(p.displayName) else if (_isEnum(p.type)) - _hasToJson(p.type) - ? refer(p.displayName) - .property('toJson') - .call([]) - .ifNullThen( - refer( - p.displayName, - ).property('toString').call([]), - ) - : refer(p.displayName).property('toString').call([]) + _getEnumValueReference(p.type, p.displayName) else refer(p.displayName).property('toString').call([]), ]), @@ -2728,7 +2753,17 @@ MultipartFile.fromFileSync(i.path, /// Generates the request headers. Map _generateHeaders(MethodElement2 m) { - final headers = _getMethodAnnotations(m, retrofit.Headers) + // Start with global headers from @RestApi annotation + final headers = {}; + final globalHeaders = clientAnnotation.headers; + if (globalHeaders != null) { + for (final entry in globalHeaders.entries) { + headers[entry.key] = literal(entry.value); + } + } + + // Method-level @Headers annotations override global headers + final methodHeaders = _getMethodAnnotations(m, retrofit.Headers) .map((e) => e.peek('value')) .map( (value) => value?.mapValue.map((k, v) { @@ -2750,6 +2785,7 @@ MultipartFile.fromFileSync(i.path, }), ) .fold>({}, (p, e) => p..addAll(e ?? {})); + headers.addAll(methodHeaders); final annotationsInParam = _getAnnotations(m, retrofit.Header); final headersInParams = annotationsInParam.map((k, v) { @@ -3033,7 +3069,13 @@ MultipartFile.fromFileSync(i.path, case retrofit.Parser.JsonSerializable: case retrofit.Parser.DartJsonMapper: final toJson = ele.lookUpMethod2(name: 'toJson', library: ele.library2); - return toJson == null; + if (toJson != null) { + return false; + } + // Check if the method exists in the interface type (includes mixins) + // This is important for Freezed-generated classes where toJson is in a mixin + final method = ele.getMethod2('toJson'); + return method == null; case retrofit.Parser.MapSerializable: case retrofit.Parser.FlutterCompute: return false; diff --git a/generator/lib/src/lean_generator.dart b/generator/lib/src/lean_generator.dart new file mode 100644 index 000000000..b9999a78a --- /dev/null +++ b/generator/lib/src/lean_generator.dart @@ -0,0 +1,46 @@ +import 'dart:async'; + +import 'package:lean_builder/builder.dart'; +import 'package:lean_builder/element.dart'; +import 'package:retrofit/retrofit.dart' as retrofit; + +/// Lean Builder generator for Retrofit. +/// +/// This generator provides lean_builder support for retrofit_generator, +/// allowing faster incremental builds with lean_builder. +/// +/// Note: This is an experimental feature. For production use, we recommend +/// using the traditional build_runner approach until lean_builder support +/// is fully stable. +/// +/// This generator is automatically discovered by lean_builder through the +/// @LeanGenerator.shared() annotation, which marks it as a shared part builder +/// (similar to build_runner's shared part builders). +@LeanGenerator.shared() +class RetrofitLeanGenerator extends GeneratorForAnnotatedClass { + RetrofitLeanGenerator(); + + @override + FutureOr generateForClass( + BuildStep buildStep, + ClassElement classElement, + ElementAnnotation annotation, + ) async { + // The retrofit generator is currently optimized for build_runner/source_gen. + // Full lean_builder support requires adapting the codebase to use lean_builder's + // analyzer abstractions instead of source_gen's API. + // + // For now, this serves as a placeholder for future lean_builder support. + // Users should continue using build_runner for code generation: + // dart pub run build_runner build + + throw UnsupportedError( + 'Lean Builder support for retrofit_generator is not yet fully implemented.\n' + 'The retrofit_generator package currently works with build_runner.\n\n' + 'To generate code, please use:\n' + ' dart pub run build_runner build\n\n' + 'For more information, see: https://github.com/trevorwang/retrofit.dart\n\n' + 'Lean Builder support is planned for a future release once lean_builder reaches stable.', + ); + } +} diff --git a/generator/test/src/generator_test_src.dart b/generator/test/src/generator_test_src.dart index 5eeb08a6a..13155785f 100644 --- a/generator/test/src/generator_test_src.dart +++ b/generator/test/src/generator_test_src.dart @@ -210,7 +210,7 @@ abstract class MultipleTypedExtrasTest { } @ShouldGenerate(''' -// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations,unused_element_parameter +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations,unused_element_parameter,avoid_unused_constructor_parameters,unreachable_from_main class _RestClient implements RestClient { _RestClient(this._dio, {this.baseUrl, this.errorLogger}); @@ -799,6 +799,18 @@ abstract class TestDynamicBody { Future createUser({@Body() dynamic user}); } +@ShouldGenerate(''' + Future ossSignNewUsingPost({required Object model}) async { +''', contains: true, expectedLogItems: [ + 'Object must provide a `toJson()` method which return a Map.\n' + 'It is programmer\'s responsibility to make sure the Object is properly serialized' +]) +@RestApi(baseUrl: 'https://httpbin.org/') +abstract class TestObjectBodyRequired { + @POST('/api/upload/ossSignNew') + Future ossSignNewUsingPost({@Body() required Object model}); +} + @ShouldGenerate(''' final _data = {'user_id': userId}; ''', contains: true) @@ -1300,6 +1312,20 @@ abstract class TestHttpResponseArray { ); } ''', contains: true) +@ShouldGenerate(''' + final _data = FormData(); + if (file != null) { + _data.files.add( + MapEntry( + 'file', + MultipartFile.fromFileSync( + file.path, + filename: file.path.split(Platform.pathSeparator).last, + ), + ), + ); + } +''', contains: true) @RestApi() abstract class TestFileList { @POST('/') @@ -1310,6 +1336,9 @@ abstract class TestFileList { @POST('/') Future testOptionalFile({@Part() File file}); + + @POST('/') + Future testNullableOptionalFile({@Part() File? file}); } // @JsonEnum() @@ -1344,12 +1373,10 @@ enum TestEnumWithToJson { final _data = FormData.fromMap(map); ''', contains: true) @ShouldGenerate(''' -_data.fields.add(MapEntry('enumValue', enumValue.toString())); +_data.fields.add(MapEntry('enumValue', enumValue.name)); ''', contains: true) @ShouldGenerate(''' - _data.fields.add( - MapEntry('enumValue', enumValue.toJson() ?? enumValue.toString()), - ); + _data.fields.add(MapEntry('enumValue', enumValue.toJson())); ''', contains: true) @ShouldGenerate(''' final _data = FormData(); @@ -1433,6 +1460,27 @@ abstract class CustomOptions { Future testOptions(@DioOptions() Options options); } +// Test that a service without @DioOptions does not get newRequestOptions method +// We check by verifying that the expected code pattern is generated without newRequestOptions +@ShouldGenerate(r''' +class _ServiceWithoutCustomOptions implements ServiceWithoutCustomOptions { + _ServiceWithoutCustomOptions(this._dio, {this.baseUrl, this.errorLogger}); + + final Dio _dio; + + String? baseUrl; + + final ParseErrorLogger? errorLogger; + + @override + Future getData() async { +''', contains: true) +@RestApi() +abstract class ServiceWithoutCustomOptions { + @GET('/data') + Future getData(); +} + @ShouldGenerate(''' late User _value; try { @@ -2207,6 +2255,48 @@ abstract class NullableDynamicInnerListGenericPrimitiveTypeShouldBeCastedRecursi Future>?> get(); } +@ShouldGenerate(''' + late GenericUser> _value; + try { + _value = GenericUser>.fromJson( + _result.data!, + (json) => json is List + ? json.map((i) => i).toList() + : List.empty(), + ); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } +''', contains: true) +@RestApi() +abstract class DynamicInnerListGenericDynamicTypeShouldBeCastedAsIs { + @PUT('/') + Future>> get(); +} + +@ShouldGenerate(''' + late GenericUser>? _value; + try { + _value = _result.data == null + ? null + : GenericUser>.fromJson( + _result.data!, + (json) => json is List + ? json.map((i) => i).toList() + : List.empty(), + ); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } +''', contains: true) +@RestApi() +abstract class NullableDynamicInnerListGenericDynamicTypeShouldBeCastedAsIs { + @PUT('/') + Future>?> get(); +} + @ShouldGenerate(''' late GenericUserWithoutGenericArgumentFactories _value; try { @@ -2523,3 +2613,92 @@ abstract class TestOnErrorWrapping { @GET('/test') Future getData(); } + +@ShouldGenerate(''' + final _headers = { + r'User-Agent': 'MyApp/1.0.0', + r'X-Platform': 'mobile', + }; +''', contains: true) +@RestApi( + headers: { + 'User-Agent': 'MyApp/1.0.0', + 'X-Platform': 'mobile', + }, +) +abstract class GlobalHeaders { + @GET('/list/') + Future list(); +} + +@ShouldGenerate(''' + final _headers = { + r'User-Agent': 'MyApp/1.0.0', + r'X-Platform': 'mobile', + r'Authorization': 'Bearer token', + }; +''', contains: true) +@RestApi( + headers: { + 'User-Agent': 'MyApp/1.0.0', + 'X-Platform': 'mobile', + }, +) +abstract class GlobalHeadersWithMethodHeaders { + @GET('/list/') + @Headers({'Authorization': 'Bearer token'}) + Future list(); +} + +@ShouldGenerate(''' + final _headers = { + r'User-Agent': 'MyApp/1.0.0', + r'X-Platform': 'override-value', + }; +''', contains: true) +@RestApi( + headers: { + 'User-Agent': 'MyApp/1.0.0', + 'X-Platform': 'mobile', + }, +) +abstract class GlobalHeadersOverriddenByMethodHeaders { + @GET('/list/') + @Headers({'X-Platform': 'override-value'}) + Future list(); +} + +@ShouldGenerate(''' + final _headers = { + r'X-Custom': 'value', + r'X-Dynamic': dynamicHeader, + }; +''', contains: true) +@RestApi( + headers: { + 'X-Custom': 'value', + }, +) +abstract class GlobalHeadersWithDynamicHeaders { + @GET('/list/') + Future list(@Header('X-Dynamic') String dynamicHeader); +} + +@ShouldGenerate(''' + final _headers = { + r'X-Count': 42, + r'X-Enabled': true, + r'X-Rate': 3.14, + }; +''', contains: true) +@RestApi( + headers: { + 'X-Count': 42, + 'X-Enabled': true, + 'X-Rate': 3.14, + }, +) +abstract class GlobalHeadersWithDifferentTypes { + @GET('/list/') + Future list(); +} diff --git a/retrofit/lib/http.dart b/retrofit/lib/http.dart index 97c3fbe8f..528f4d56f 100644 --- a/retrofit/lib/http.dart +++ b/retrofit/lib/http.dart @@ -68,6 +68,7 @@ class RestApi { this.baseUrl, this.parser = Parser.JsonSerializable, this.callAdapter, + this.headers, }); /// Set the API base URL. @@ -95,6 +96,28 @@ class RestApi { /// if you don't specify the [parser]. It will be [Parser.JsonSerializable] final Parser parser; final Type? callAdapter; + + /// Global headers to be applied to all requests within this API. + /// + /// These headers will be included in every request made through this API interface. + /// Method-level headers specified with [@Headers] will override these global headers + /// if they have the same key. + /// + /// Example: + /// ```dart + /// @RestApi( + /// baseUrl: "https://api.example.com", + /// headers: { + /// "User-Agent": "MyApp/1.0.0", + /// "X-Platform": "mobile", + /// }, + /// ) + /// abstract class ApiService { + /// @GET("/endpoint") + /// Future getData(); + /// } + /// ``` + final Map? headers; } @immutable From 4e38a0c6067af70bf31ec8188e568264b2f074dc Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Oct 2025 09:26:41 +0800 Subject: [PATCH 12/12] [WIP] Fix error handling in generator code generation (#820) * Initial plan * Fix callAdapter null interpolation and improve Future type detection - Guard _generateAdapterMethod to only be called when instantiatedCallAdapter is non-null - Change callAdapter parameter from nullable to non-nullable InterfaceType - Add null check for adapter name and throw clear error if missing - Extract adapter name safely to avoid 'null' string interpolation - Add _isDartTypeFuture helper using analyzer's type system API - Only wrap with catchError when return type is actually a Future - Use _isDartTypeFuture instead of string-based check in _generateRequest Co-authored-by: trevorwang <121966+trevorwang@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: trevorwang <121966+trevorwang@users.noreply.github.com> --- generator/lib/src/generator.dart | 43 +++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/generator/lib/src/generator.dart b/generator/lib/src/generator.dart index 75cf8b14a..db05915af 100644 --- a/generator/lib/src/generator.dart +++ b/generator/lib/src/generator.dart @@ -391,7 +391,7 @@ class RetrofitGenerator extends GeneratorForAnnotation { if (method.isAbstract) { methods.add(_generateApiCallMethod(method, instantiatedCallAdapter)!); } - if (callAdapter != null) { + if (callAdapter != null && instantiatedCallAdapter != null) { methods.add( _generateAdapterMethod( method, @@ -407,9 +407,18 @@ class RetrofitGenerator extends GeneratorForAnnotation { /// Generates a method implementation wrapped by CallAdapter. Method _generateAdapterMethod( MethodElement2 m, - InterfaceType? callAdapter, + InterfaceType callAdapter, String resultType, ) { + // Get the adapter class name - should never be null at this point + final adapterName = callAdapter.element3.name3; + if (adapterName == null) { + throw InvalidGenerationSourceError( + 'CallAdapter class must have a valid name', + element: m, + ); + } + return Method((methodBuilder) { methodBuilder.returns = refer( _displayString(m.returnType, withNullability: true), @@ -438,10 +447,19 @@ class RetrofitGenerator extends GeneratorForAnnotation { } final args = '${positionalArgs.map((e) => '$e,').join()} ${namedArgs.map((e) => '$e,').join()}'; - final adaptedCall = '${callAdapter?.element3.name3}<$resultType>().adapt(() => _${m.displayName}($args))'; - methodBuilder.body = Code(''' - return $_onErrorVar != null ? $adaptedCall.catchError($_onErrorVar) : $adaptedCall; - '''); + final adaptedCall = '$adapterName<$resultType>().adapt(() => _${m.displayName}($args))'; + + // Only wrap with catchError if onError exists and the adapted return type is a Future + final adaptedReturnType = callAdapter.superclass?.typeArguments.lastOrNull as InterfaceType?; + final shouldWrapWithCatchError = _isDartTypeFuture(adaptedReturnType); + + if (shouldWrapWithCatchError) { + methodBuilder.body = Code(''' + return $_onErrorVar != null ? $adaptedCall.catchError($_onErrorVar) : $adaptedCall; + '''); + } else { + methodBuilder.body = Code('return $adaptedCall;'); + } }); } @@ -847,6 +865,15 @@ class RetrofitGenerator extends GeneratorForAnnotation { /// Checks if the return type is Future. bool _isReturnTypeFuture(String type) => type.startsWith('Future<'); + /// Checks if a DartType represents a Future. + /// More reliable than string matching as it uses the analyzer's type system. + bool _isDartTypeFuture(DartType? type) { + if (type == null) { + return false; + } + return type.isDartAsyncFuture; + } + /// Generates the HTTP request code block. Code _generateRequest( MethodElement2 m, @@ -860,9 +887,7 @@ class RetrofitGenerator extends GeneratorForAnnotation { final callAdapterOriginalReturnType = callAdapter.superclass?.typeArguments.firstOrNull as InterfaceType?; returnAsyncWrapper = - _isReturnTypeFuture( - callAdapterOriginalReturnType?.getDisplayString() ?? '', - ) + _isDartTypeFuture(callAdapterOriginalReturnType) ? 'return' : 'yield'; }